/* * Copyright (C) 2012 Jan Pokorsky * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package cz.cas.lib.proarc.webapp.client.widget; import com.google.gwt.activity.shared.Activity; import com.google.gwt.activity.shared.ActivityManager; import com.google.gwt.activity.shared.ActivityMapper; import com.google.gwt.core.client.Callback; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.place.shared.Place; import com.google.gwt.place.shared.PlaceController; import com.google.gwt.user.client.ui.AcceptsOneWidget; import com.google.gwt.user.client.ui.IsWidget; import com.google.gwt.user.client.ui.Widget; import com.google.web.bindery.event.shared.SimpleEventBus; import com.smartgwt.client.data.Criteria; import com.smartgwt.client.data.DSCallback; import com.smartgwt.client.data.DSRequest; import com.smartgwt.client.data.DSResponse; import com.smartgwt.client.data.Record; import com.smartgwt.client.data.RecordList; import com.smartgwt.client.data.ResultSet; import com.smartgwt.client.data.events.DataChangedEvent; import com.smartgwt.client.data.events.DataChangedHandler; import com.smartgwt.client.types.DSOperationType; import com.smartgwt.client.util.BooleanCallback; import com.smartgwt.client.widgets.Canvas; import com.smartgwt.client.widgets.IconButton; import com.smartgwt.client.widgets.grid.ListGrid; import com.smartgwt.client.widgets.grid.ListGridField; import com.smartgwt.client.widgets.grid.ListGridRecord; import com.smartgwt.client.widgets.grid.events.DataArrivedEvent; import com.smartgwt.client.widgets.grid.events.DataArrivedHandler; import com.smartgwt.client.widgets.grid.events.RecordClickEvent; import com.smartgwt.client.widgets.grid.events.RecordClickHandler; import com.smartgwt.client.widgets.grid.events.RecordDoubleClickEvent; import com.smartgwt.client.widgets.grid.events.RecordDoubleClickHandler; import com.smartgwt.client.widgets.grid.events.RecordDropEvent; import com.smartgwt.client.widgets.grid.events.RecordDropHandler; import com.smartgwt.client.widgets.grid.events.SelectionUpdatedEvent; import com.smartgwt.client.widgets.grid.events.SelectionUpdatedHandler; import com.smartgwt.client.widgets.layout.HLayout; import com.smartgwt.client.widgets.layout.Layout; import com.smartgwt.client.widgets.layout.VLayout; import com.smartgwt.client.widgets.menu.IconMenuButton; import com.smartgwt.client.widgets.menu.Menu; import com.smartgwt.client.widgets.menu.events.ItemClickEvent; import com.smartgwt.client.widgets.menu.events.ItemClickHandler; import cz.cas.lib.proarc.webapp.client.ClientMessages; import cz.cas.lib.proarc.webapp.client.ClientUtils; import cz.cas.lib.proarc.webapp.client.action.Action; import cz.cas.lib.proarc.webapp.client.action.ActionEvent; import cz.cas.lib.proarc.webapp.client.action.Actions; import cz.cas.lib.proarc.webapp.client.action.Actions.ActionSource; import cz.cas.lib.proarc.webapp.client.action.DeleteAction; import cz.cas.lib.proarc.webapp.client.action.DeleteAction.Deletable; import cz.cas.lib.proarc.webapp.client.action.DigitalObjectCopyMetadataAction; import cz.cas.lib.proarc.webapp.client.action.DigitalObjectCopyMetadataAction.CopySelector; import cz.cas.lib.proarc.webapp.client.action.DigitalObjectFormValidateAction; import cz.cas.lib.proarc.webapp.client.action.DigitalObjectFormValidateAction.ValidatableList; import cz.cas.lib.proarc.webapp.client.action.DigitalObjectNavigateAction; import cz.cas.lib.proarc.webapp.client.action.DigitalObjectNavigateAction.ChildSelector; import cz.cas.lib.proarc.webapp.client.action.RefreshAction.Refreshable; import cz.cas.lib.proarc.webapp.client.action.SaveAction; import cz.cas.lib.proarc.webapp.client.action.Selectable; import cz.cas.lib.proarc.webapp.client.action.UrnNbnAction; import cz.cas.lib.proarc.webapp.client.ds.DigitalObjectDataSource; import cz.cas.lib.proarc.webapp.client.ds.DigitalObjectDataSource.DigitalObject; import cz.cas.lib.proarc.webapp.client.ds.MetaModelDataSource; import cz.cas.lib.proarc.webapp.client.ds.MetaModelDataSource.MetaModelRecord; import cz.cas.lib.proarc.webapp.client.ds.RelationDataSource; import cz.cas.lib.proarc.webapp.client.ds.RelationDataSource.RelationChangeEvent; import cz.cas.lib.proarc.webapp.client.ds.RelationDataSource.RelationChangeHandler; import cz.cas.lib.proarc.webapp.client.ds.RestConfig; import cz.cas.lib.proarc.webapp.client.presenter.DigitalObjectEditing; import cz.cas.lib.proarc.webapp.client.presenter.DigitalObjectEditing.DigitalObjectEditorPlace; import cz.cas.lib.proarc.webapp.client.presenter.DigitalObjectEditor; import cz.cas.lib.proarc.webapp.client.presenter.DigitalObjectEditor.OptionalEditor; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; /** * Edits children objects of the digital object. * It allows to change order of children, to edit a selected child using * particular data stream editor. * * @author Jan Pokorsky */ public final class DigitalObjectChildrenEditor implements DatastreamEditor, Refreshable, Selectable<Record>, CopySelector, ChildSelector { /** * The boolean attribute to mark the most recently selected record. Useful * in case of multiple-selection. */ public static final String LAST_CLICKED_ATTR = "__proarcLastClickedRecord"; private static final Logger LOG = Logger.getLogger(DigitalObjectChildrenEditor.class.getName()); private final ClientMessages i18n; /** A controller of the enclosing editor. */ private final PlaceController places; private final ListGrid childrenListGrid; private final DigitalObjectEditor childEditor; private final HLayout widget; private DigitalObject digitalObject; private final PlaceController childPlaces; private HandlerRegistration listDataChangedHandler; private Record[] originChildren; /** The last child selections. */ private SelectionHistory selectionHistory = new SelectionHistory(); private IconMenuButton addActionButton; private IconButton saveActionButton; private HandlerRegistration childrenSelectionHandler; private RelationDataSource relationDataSource; /** Notifies changes in list of children that should be reflected by actions. */ private final ActionSource actionSource; private final DigitalObjectNavigateAction goDownAction; private final OptionalEditor preview; private Record lastClicked; public DigitalObjectChildrenEditor(ClientMessages i18n, PlaceController places, OptionalEditor preview) { this.i18n = i18n; this.places = places; this.preview = preview; this.actionSource = new ActionSource(this); this.goDownAction = DigitalObjectNavigateAction.child(i18n, places); relationDataSource = RelationDataSource.getInstance(); childrenListGrid = initChildrenListGrid(); VLayout childrenLayout = new VLayout(); childrenLayout.setMembers(childrenListGrid); childrenLayout.setWidth("40%"); childrenLayout.setShowResizeBar(true); SimpleEventBus eventBus = new SimpleEventBus(); childPlaces = new PlaceController(eventBus); childEditor = new DigitalObjectEditor(i18n, childPlaces, true); ActivityManager activityManager = new ActivityManager( new ChildActivities(childEditor), eventBus); VLayout editorsLayout = new VLayout(); VLayout editorsOuterLayout = new VLayout(); // editorsLayout.setBorder("1px solid grey"); editorsLayout.addStyleName("defaultBorder"); editorsOuterLayout.setLayoutLeftMargin(4); editorsOuterLayout.setMembers(editorsLayout); activityManager.setDisplay(new ChildEditorDisplay(editorsLayout)); widget = new HLayout(); widget.setMembers(childrenLayout, editorsOuterLayout); relationDataSource.addRelationChangeHandler(new RelationChangeHandler() { @Override public void onRelationChange(RelationChangeEvent event) { // issue 262: isVisible seems to be always true and isAttached is always null. // Add test isDrawn that seems to change for dettached widgets. if (digitalObject != null && childrenListGrid.isVisible() && childrenListGrid.isDrawn()) { String changedPid = event.getPid(); if (changedPid != null) { Record changedRecord = childrenListGrid.getDataAsRecordList() .find(RelationDataSource.FIELD_PID, changedPid); if (changedRecord == null) { // moved object(s) // ListGrid does not remove selection of removed/moved rows // and it does not fire selection change // issue 246: clear selection of moved row childrenListGrid.deselectAllRecords(); DigitalObjectCopyMetadataAction.resetSelection(); showCopySelection(new Record[0]); return ; } } final ListGridRecord[] selection = childrenListGrid.getSelectedRecords(); relationDataSource.updateCaches(digitalObject.getPid(), new BooleanCallback() { @Override public void execute(Boolean value) { // refresh the copy selection as updated records are missing the copy attribute showCopySelection(DigitalObjectCopyMetadataAction.getSelection()); // refresh the list selection selectChildren(selection); } }); } } }); } @Override public void edit(DigitalObject digitalObject) { this.digitalObject = digitalObject; if (digitalObject == null) { return ; } detachListFromEditor(); detachListResultSet(); String pid = digitalObject.getPid(); Criteria criteria = new Criteria(RelationDataSource.FIELD_ROOT, pid); criteria.addCriteria(RelationDataSource.FIELD_PARENT, pid); DigitalObjectCopyMetadataAction.resetSelection(); ResultSet resultSet = childrenListGrid.getResultSet(); if (resultSet != null) { Boolean willFetchData = resultSet.willFetchData(criteria); // init editor for cached record when DataArrivedHandler is not called if (!willFetchData) { showCopySelection(new Record[0]); initOnEdit(); } } // use DataArrivedHandler instead of callback as it is not called // for refresh in SmartGWT 3.0 childrenListGrid.fetchData(criteria); MetaModelDataSource.getModels(false, new Callback<ResultSet, Void>() { @Override public void onFailure(Void reason) { } @Override public void onSuccess(ResultSet result) { Map<?,?> valueMap = result.getValueMap( MetaModelDataSource.FIELD_PID, MetaModelDataSource.FIELD_DISPLAY_NAME); childrenListGrid.getField(RelationDataSource.FIELD_MODEL).setValueMap(valueMap); createAddMenu(result); } }); } @Override public void focus() { childrenListGrid.focus(); } @Override @SuppressWarnings("unchecked") public <T> T getCapability(Class<T> clazz) { T c = null; if (Refreshable.class.equals(clazz) || ChildSelector.class.equals(clazz)) { c = (T) this; } return c; } @Override public Canvas[] getToolbarItems() { Action addAction = Actions.emptyAction(i18n.DigitalObjectEditor_ChildrenEditor_AddAction_Title(), "[SKIN]/actions/add.png", i18n.DigitalObjectEditor_ChildrenEditor_AddAction_Hint()); Action saveAction = new SaveAction(i18n) { @Override public void performAction(ActionEvent event) { save(); } }; DeleteAction deleteAction = new DeleteAction(new Deletable<Record>() { private final Deletable<Record> deletable = DigitalObjectDataSource.createDeletable(); @Override public void delete(Object[] items, Record options) { deletable.delete(items, options); childrenListGrid.deselectAllRecords(); DigitalObjectCopyMetadataAction.removeSelection((Record[]) items); } @Override public void delete(Object[] items) { delete(items, null); } }, DigitalObjectDataSource.createDeleteOptionsForm(), i18n); addActionButton = Actions.asIconMenuButton(addAction, this); return new Canvas[] { addActionButton, Actions.asIconButton(deleteAction, actionSource), Actions.asIconButton(DigitalObjectFormValidateAction.getInstance(i18n), new ValidatableList(childrenListGrid)), Actions.asIconButton(new UrnNbnAction(i18n), actionSource), Actions.asIconButton(new DigitalObjectCopyMetadataAction(i18n), actionSource), saveActionButton = Actions.asIconButton(saveAction, this), }; } @Override public Canvas getUI() { return widget; } @Override public void refresh() { ValidatableList.clearRowErrors(childrenListGrid); childrenListGrid.invalidateCache(); edit(digitalObject); } @Override public Record[] getSelection() { return originChildren != null ? null : childrenListGrid.getSelectedRecords(); } @Override public Record[] getChildSelection() { return getSelection(); } @Override public void showCopySelection(Record[] records) { if (records == null) { return ; } RecordList copySelection = new RecordList(records); for (int i = childrenListGrid.getRecords().length - 1; i >= 0; i--) { Record item = childrenListGrid.getRecord(i); DigitalObject listItem = DigitalObject.create(item); Record select = copySelection.find(RelationDataSource.FIELD_PID, listItem.getPid()); boolean refresh = false; if (select != null) { if (!DigitalObjectCopyMetadataAction.isSelectedCopyRecord(item)) { DigitalObjectCopyMetadataAction.selectCopyRecord(item); refresh = true; } } else { if (DigitalObjectCopyMetadataAction.isSelectedCopyRecord(item)) { DigitalObjectCopyMetadataAction.deselectCopyRecord(item); refresh = true; } } if (refresh) { childrenListGrid.refreshRow(i); } } } private void save() { if (originChildren == null) { return ; } Record[] rs = childrenListGrid.getOriginalResultSet().toArray(); String[] childPids = ClientUtils.toFieldValues(rs, RelationDataSource.FIELD_PID); relationDataSource.reorderChildren(digitalObject, childPids, new BooleanCallback() { @Override public void execute(Boolean value) { if (value != null && value) { originChildren = null; updateReorderUi(false); StatusView.getInstance().show(i18n.SaveAction_Done_Msg()); } } }); } /** * Handles a new children selection. */ private void onChildSelection(Record[] records) { actionSource.fireEvent(); if (records == null || records.length == 0 || originChildren != null) { childPlaces.goTo(Place.NOWHERE); } else { childPlaces.goTo(new DigitalObjectEditorPlace(null, records)); } if (records == null || records.length <= 1) { // in case of multiselection the preview opens the last clicked record // see RecordClickHandler. preview(records); } } private void preview(Record... records) { if (records == null || records.length == 0 || originChildren != null) { preview.open(); } else { preview.open(DigitalObject.toArray(records)); } } private ListGrid initChildrenListGrid() { final ListGrid lg = new ListGrid() { @Override protected String getCellCSSText(ListGridRecord record, int rowNum, int colNum) { // do not replace with hilites as they do not support UI refresh if (DigitalObjectCopyMetadataAction.isSelectedCopyRecord(record)) { return "color: #FF0000;"; } else { return super.getCellCSSText(record, rowNum, colNum); } } }; lg.setDataSource(relationDataSource); lg.setFields( new ListGridField(RelationDataSource.FIELD_LABEL, i18n.DigitalObjectSearchView_ListHeaderLabel_Title()), new ListGridField(RelationDataSource.FIELD_MODEL, i18n.DigitalObjectSearchView_ListHeaderModel_Title()), new ListGridField(RelationDataSource.FIELD_PID, i18n.DigitalObjectSearchView_ListHeaderPid_Title()) ); lg.getField(RelationDataSource.FIELD_PID).setHidden(true); lg.setCanSort(Boolean.FALSE); lg.setCanReorderRecords(Boolean.TRUE); lg.setShowRollOver(Boolean.FALSE); lg.setGenerateDoubleClickOnEnter(Boolean.TRUE); // ListGrid with enabled grouping prevents record reoredering by dragging! (SmartGWT 3.0) // lg.setGroupByField(RelationDataSource.FIELD_MODEL); // lg.setGroupStartOpen(GroupStartOpen.ALL); lg.addRecordDropHandler(new RecordDropHandler() { @Override public void onRecordDrop(RecordDropEvent event) { Record[] records = childrenListGrid.getOriginalResultSet().toArray(); if (originChildren == null) { // takes the list snapshot before first reorder originChildren = records; } } }); lg.addDataArrivedHandler(new DataArrivedHandler() { @Override public void onDataArrived(DataArrivedEvent event) { if (event.getStartRow() == 0) { initOnEdit(); } } }); lg.addRecordDoubleClickHandler(new RecordDoubleClickHandler() { @Override public void onRecordDoubleClick(RecordDoubleClickEvent event) { ActionEvent evt = new ActionEvent(actionSource.getSource()); goDownAction.performAction(evt); } }); lg.addRecordClickHandler(new RecordClickHandler() { @Override public void onRecordClick(RecordClickEvent event) { // NOTE: RecordClickEvent is fired after SelectionUpdatedEvent! if (!event.isCancelled() && originChildren == null) { ListGridRecord r = event.getRecord(); r.setAttribute(LAST_CLICKED_ATTR, Boolean.TRUE); if (lastClicked != null && r != lastClicked) { lastClicked.setAttribute(LAST_CLICKED_ATTR, Boolean.FALSE); } lastClicked = r; Record[] selection = getSelection(); if (selection != null && selection.length > 1) { // single selection is handled by onChildSelection preview(r); } } } }); return lg; } private void initOnEdit() { // LOG.info("initOnEdit"); originChildren = null; lastClicked = null; updateReorderUi(false); attachListResultSet(); // select first if (!childrenListGrid.getOriginalResultSet().isEmpty()) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { // defer the select as it is ignored after refresh in onDataArrived selectChildFromHistory(); } }); } } private void selectChildFromHistory() { int row = -1; Place where = places.getWhere(); if (where instanceof DigitalObjectEditorPlace) { String selectPid = ((DigitalObjectEditorPlace) where).getSelectChildPid(); if (selectPid != null) { int index = childrenListGrid.getRecordList().findIndex(RelationDataSource.FIELD_PID, selectPid); if (index > -1) { row = index; } } } if (row < 0) { String selectPid = selectionHistory.getSelection(digitalObject.getPid()); if (selectPid != null) { int index = childrenListGrid.getRecordList().findIndex(RelationDataSource.FIELD_PID, selectPid); if (index > -1) { row = index; } } } if (row < 0) { row = 0; } childrenListGrid.scrollToRow(row); childrenListGrid.selectSingleRecord(row); childrenListGrid.focus(); } private void selectChildren(Record[] selection) { RecordList rl = childrenListGrid.getRecordList(); ArrayList<Record> newSelection = new ArrayList<Record>(selection.length); for (Record r : selection) { Record found = rl.find(RelationDataSource.FIELD_PID, r.getAttribute(RelationDataSource.FIELD_PID)); if (found != null) { newSelection.add(found); } } if (!newSelection.isEmpty()) { childrenListGrid.selectRecords(newSelection.toArray(new Record[newSelection.size()])); } } /** * Opens child editor according to list selection. */ private void attachListToEditor() { if (childrenSelectionHandler != null) { childrenSelectionHandler.removeHandler(); } childrenSelectionHandler = childrenListGrid.addSelectionUpdatedHandler(new SelectionUpdatedHandler() { @Override public void onSelectionUpdated(SelectionUpdatedEvent event) { ListGridRecord[] records = childrenListGrid.getSelectedRecords(); selectionHistory.select(digitalObject.getPid(), records); onChildSelection(records); } }); } private void detachListFromEditor() { if (childrenSelectionHandler != null) { childrenSelectionHandler.removeHandler(); childrenSelectionHandler = null; childPlaces.goTo(Place.NOWHERE); preview(); } } /** * Listens to changes of original list result set (reorder, fetch). * It tracks order changes together with {@link ListGrid#addRecordDropHandler } * in {@link #initChildrenListGrid() } */ private void attachListResultSet() { listDataChangedHandler = childrenListGrid.getOriginalResultSet().addDataChangedHandler(new DataChangedHandler() { @Override public void onDataChanged(DataChangedEvent event) { if (originChildren != null) { // compare origin with new children boolean equals = RelationDataSource.equals( originChildren, childrenListGrid.getOriginalResultSet().toArray()); // enable Save button // disable remove and add updateReorderUi(!equals); } } }); } private void detachListResultSet() { if (listDataChangedHandler != null) { listDataChangedHandler.removeHandler(); listDataChangedHandler = null; } } private void updateReorderUi(boolean reordered) { actionSource.fireEvent(); saveActionButton.setVisible(reordered); addActionButton.setVisible(!reordered); if (reordered) { detachListFromEditor(); } else { attachListToEditor(); } } private void createAddMenu(ResultSet models) { Menu menuAdd = MetaModelDataSource.createMenu(models, true); addActionButton.setMenu(menuAdd); menuAdd.addItemClickHandler(new ItemClickHandler() { @Override public void onItemClick(ItemClickEvent event) { MetaModelRecord mmr = MetaModelRecord.get(event.getItem()); addChild(mmr); } }); } private void addChild(MetaModelRecord model) { Record record = new Record(); record.setAttribute(DigitalObjectDataSource.FIELD_MODEL, model.getId()); record.setAttribute(RelationDataSource.FIELD_PARENT, digitalObject.getPid()); DigitalObjectDataSource.getInstance().addData(record, new DSCallback() { @Override public void execute(DSResponse response, Object rawData, DSRequest request) { if (RestConfig.isStatusOk(response)) { Record[] data = response.getData(); final Record r = data[0]; DSRequest dsRequest = new DSRequest(); dsRequest.setOperationType(DSOperationType.ADD); RelationDataSource.getInstance().updateCaches(response, dsRequest); Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { childrenListGrid.selectSingleRecord(r); int recordIndex = childrenListGrid.getRecordIndex(r); childrenListGrid.scrollToRow(recordIndex); } }); } } }); } public static final class ChildActivities implements ActivityMapper { private final DigitalObjectEditor childEditor; public ChildActivities(DigitalObjectEditor childEditor) { this.childEditor = childEditor; } @Override public Activity getActivity(Place place) { if (place instanceof DigitalObjectEditorPlace) { DigitalObjectEditorPlace editorPlace = (DigitalObjectEditorPlace) place; return new DigitalObjectEditing(editorPlace, childEditor); } return null; } } public static final class ChildEditorDisplay implements AcceptsOneWidget { private final Layout display; public ChildEditorDisplay(Layout display) { this.display = display; } @Override public void setWidget(IsWidget w) { Widget asWidget = Widget.asWidgetOrNull(w); if (asWidget instanceof Canvas) { ClientUtils.setMembers(display, (Canvas) asWidget); } else if (asWidget == null) { display.removeMembers(display.getMembers()); } else { throw new IllegalStateException("Unsupported widget: " + asWidget.getClass()); } } } /** * Keeps the history of child selections of edited parents. */ private static final class SelectionHistory { /** * The PID to PID mapping. */ private final Map<String, String> cache = new HashMap<String, String>(); public void select(String pid, Record[] selection) { if (selection == null || selection.length == 0) { select(pid, (String) null); } else { DigitalObject dobj = DigitalObject.create(selection[0]); select(pid, dobj.getPid()); } } public void select(String pid, String selection) { cache.put(pid, selection); } public String getSelection(String pid) { return cache.get(pid); } } }