package sk.stuba.fiit.perconik.core.ui.preferences; import java.util.Comparator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.annotation.Nullable; import com.google.common.collect.Ordering; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.DialogSettings; import org.eclipse.jface.dialogs.IDialogSettings; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.MessageDialogWithToggle; import org.eclipse.jface.dialogs.StatusDialog; import org.eclipse.jface.layout.TableColumnLayout; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.viewers.CheckboxTableViewer; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import sk.stuba.fiit.perconik.core.Registrable; import sk.stuba.fiit.perconik.core.persistence.Registration; import sk.stuba.fiit.perconik.core.ui.plugin.Activator; import sk.stuba.fiit.perconik.eclipse.core.runtime.StatusSeverity; import sk.stuba.fiit.perconik.eclipse.jface.dialogs.MapEntryDialog; import sk.stuba.fiit.perconik.eclipse.jface.viewers.ElementComparers; import sk.stuba.fiit.perconik.eclipse.jface.viewers.MapContentProvider; import sk.stuba.fiit.perconik.eclipse.jface.viewers.MapLabelProvider; import sk.stuba.fiit.perconik.eclipse.jface.viewers.RegularTableViewer; import sk.stuba.fiit.perconik.eclipse.swt.widgets.MapTableSorter; import sk.stuba.fiit.perconik.eclipse.swt.widgets.TableSorter; import sk.stuba.fiit.perconik.eclipse.swt.widgets.WidgetListener; import sk.stuba.fiit.perconik.ui.Buttons; import sk.stuba.fiit.perconik.ui.Labels; import sk.stuba.fiit.perconik.ui.TableColumns; import sk.stuba.fiit.perconik.ui.Tables; import sk.stuba.fiit.perconik.utilities.MoreMaps; import sk.stuba.fiit.perconik.utilities.configuration.Configurable; import sk.stuba.fiit.perconik.utilities.configuration.MapOptions; import sk.stuba.fiit.perconik.utilities.configuration.MapOptions.Putter; import sk.stuba.fiit.perconik.utilities.configuration.Options; import static java.lang.String.format; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.collect.Maps.immutableEntry; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Maps.newLinkedHashMap; import static com.google.common.collect.Sets.newLinkedHashSet; import static org.eclipse.jface.dialogs.IDialogConstants.CANCEL_LABEL; import static org.eclipse.jface.dialogs.IDialogConstants.PROCEED_LABEL; import static org.eclipse.jface.dialogs.MessageDialog.openError; import static sk.stuba.fiit.perconik.utilities.MoreStrings.toStringComparator; import static sk.stuba.fiit.perconik.utilities.MoreStrings.toUpperCaseFirst; import static sk.stuba.fiit.perconik.utilities.configuration.Configurables.customRawOptions; import static sk.stuba.fiit.perconik.utilities.configuration.Configurables.inheritedRawOptions; import static sk.stuba.fiit.perconik.utilities.configuration.Configurables.knownRawOptions; import static sk.stuba.fiit.perconik.utilities.configuration.Configurables.optionEquivalence; import static sk.stuba.fiit.perconik.utilities.configuration.Configurables.rawOptionType; import static sk.stuba.fiit.perconik.utilities.configuration.Configurables.unknownRawOptions; /** * TODO * * @author Pavol Zbell * @since 1.0 */ abstract class AbstractOptionsDialog<P, R extends Registration> extends StatusDialog { private P preferences; private R registration; Map<String, Object> map; CheckboxTableViewer tableViewer; MapEntryDialog<String, Object> entryDialog; Button addButton; Button updateButton; Button removeButton; Button restoreButton; AbstractOptionsDialog(final Shell parent) { super(parent); this.preferences = null; this.registration = null; this.map = null; } abstract String name(); @Override protected final Control createDialogArea(final Composite parent) { Composite composite = new Composite(parent, SWT.NONE); GridLayout parentLayout = new GridLayout(); parentLayout.numColumns = 2; parentLayout.marginHeight = 5; parentLayout.marginWidth = 5; composite.setLayout(parentLayout); composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); Composite innerParent = new Composite(composite, SWT.NONE); GridLayout innerLayout = new GridLayout(); innerLayout.numColumns = 2; innerLayout.marginHeight = 0; innerLayout.marginWidth = 0; innerParent.setLayout(innerLayout); GridData innerGrid = new GridData(GridData.FILL_BOTH); innerGrid.horizontalSpan = 2; innerParent.setLayoutData(innerGrid); Composite tableComposite = new Composite(innerParent, SWT.NONE); TableColumnLayout tableLayout = new TableColumnLayout(); GridData tableGrid = new GridData(GridData.FILL_BOTH); tableGrid.widthHint = 360; tableGrid.heightHint = this.convertHeightInCharsToPixels(10); tableComposite.setLayout(tableLayout); tableComposite.setLayoutData(tableGrid); Table table = Tables.create(tableComposite, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL); GC gc = new GC(this.getShell()); gc.setFont(JFaceResources.getDialogFont()); TableColumn keyColumn = TableColumns.create(table, tableLayout, "Key", gc, 1); TableColumn valueColumn = TableColumns.create(table, tableLayout, "Value", gc, 1); gc.dispose(); LocalMapTableSorter keySorter = new LocalMapTableSorter(table, Ordering.from(toStringComparator()).onResultOf(MoreMaps.<Entry<String, Object>, String>toKeyFunction())); LocalMapTableSorter valueSorter = new LocalMapTableSorter(table, Ordering.from(toStringComparator()).onResultOf(MoreMaps.<Entry<String, Object>, Object>toValueFunction()).compound(keySorter.getComparator())); keySorter.attach(keyColumn); valueSorter.attach(valueColumn); this.tableViewer = new RegularTableViewer(table) { { this.normalColor = SWT.COLOR_LINK_FOREGROUND; this.grayColor = SWT.COLOR_LIST_FOREGROUND; } }; this.tableViewer.setComparer(ElementComparers.fromEquivalence(rawOptionType(), optionEquivalence())); this.tableViewer.setContentProvider(new MapContentProvider()); this.tableViewer.setLabelProvider(new MapLabelProvider()); this.tableViewer.addSelectionChangedListener(new ISelectionChangedListener() { public void selectionChanged(final SelectionChangedEvent e) { updateButtons(); } }); this.tableViewer.addDoubleClickListener(new IDoubleClickListener() { public void doubleClick(final DoubleClickEvent event) { performUpdate(); } }); Composite buttons = new Composite(innerParent, SWT.NONE); buttons.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING)); parentLayout = new GridLayout(); parentLayout.marginHeight = 0; parentLayout.marginWidth = 0; buttons.setLayout(parentLayout); this.addButton = Buttons.createCentering(buttons, "Add", new WidgetListener() { public void handleEvent(final Event e) { performAdd(); } }); this.updateButton = Buttons.createCentering(buttons, "Update", new WidgetListener() { public void handleEvent(final Event e) { performUpdate(); } }); this.removeButton = Buttons.createCentering(buttons, "Remove", new WidgetListener() { public void handleEvent(final Event e) { performRemove(); } }); Labels.createButtonSeparator(buttons); this.restoreButton = Buttons.createCentering(buttons, "Restore", new WidgetListener() { public void handleEvent(final Event e) { performRestore(); } }); this.entryDialog = new CustomMapEntryDialog<>(this.getShell()); this.loadInternal(this.preferences, this.registration); Dialog.applyDialogFont(composite); innerParent.layout(); return composite; } final Map<String, Object> knownOptions() { return knownRawOptions(this.map, readFromOptions(this.options(this.defaultPreferences(), this.registration)).keySet()); } final Map<String, Object> unknownOptions() { return unknownRawOptions(this.map, readFromOptions(this.options(this.defaultPreferences(), this.registration)).keySet()); } final Map<String, Object> inheritedOptions() { return inheritedRawOptions(this.map, readFromOptions(this.options(this.defaultPreferences(), this.registration))); } final Map<String, Object> customOptions() { return customRawOptions(this.map, readFromOptions(this.options(this.defaultPreferences(), this.registration))); } final void updateButtons() { IStructuredSelection selection = (IStructuredSelection) this.tableViewer.getSelection(); int selectionCount = selection.size(); int itemCount = this.tableViewer.getTable().getItemCount(); this.updateButton.setEnabled(selectionCount == 1); this.removeButton.setEnabled(selectionCount > 0 && selectionCount <= itemCount); } final void updateTable() { assert this.tableViewer != null; this.tableViewer.setInput(this.map); this.tableViewer.refresh(); if (this.map != null) { this.tableViewer.setAllGrayed(false); this.tableViewer.setGrayedElements(this.inheritedOptions().entrySet().toArray()); } } final void sortTable() { assert this.tableViewer != null; Table table = this.tableViewer.getTable(); TableSorter.enable(table, this.map != null); TableSorter.automaticSort(table); } final class LocalMapTableSorter extends MapTableSorter<String, Object> { LocalMapTableSorter(final Table table, @Nullable final Comparator<Entry<String, Object>> comparator) { super(table, comparator); } @Override protected Map<String, Object> loadMap() { return AbstractOptionsDialog.this.map; } @Override protected void updateMap(final Map<String, Object> map) { AbstractOptionsDialog.this.map = map; updateTable(); } } abstract P defaultPreferences(); abstract Options options(P preferences, R registration); private void openOptionDialog(final Entry<String, Object> entry) { MapEntryDialog<String, Object> dialog = this.entryDialog; dialog.setEntry(entry); dialog.setTitle("Option dialog"); dialog.open(); if (dialog.getReturnCode() == Window.OK) { Entry<String, Object> result = this.entryDialog.getEntry(); if (result != null) { this.map.put(result.getKey(), result.getValue()); this.updateTable(); this.sortTable(); this.updateButtons(); } } } void performAdd() { this.openOptionDialog(immutableEntry("", null)); } void performUpdate() { IStructuredSelection selection = (IStructuredSelection) this.tableViewer.getSelection(); @SuppressWarnings("unchecked") Entry<String, Object> entry = (Entry<String, Object>) selection.getFirstElement(); this.openOptionDialog(entry); } void performRemove() { IStructuredSelection selection = (IStructuredSelection) this.tableViewer.getSelection(); Set<String> known = this.knownOptions().keySet(); Set<String> locked = newLinkedHashSet(); for (Object item: selection.toList()) { Object key = ((Entry<?, ?>) item).getKey(); if (!known.contains(key)) { checkNotNull(this.map.remove(key)); } else { locked.add(key.toString()); } } this.updateTable(); this.sortTable(); this.updateButtons(); if (!locked.isEmpty()) { String title = "Remove Options"; String message = format("Some options could not be removed since inherited from %s defaults.", this.name()); MessageDialog.openInformation(this.getShell(), title, message); } } void performRestore() { String title = "Restore Default Options"; String message = format("PerConIK Core is about to restore defaults for selected options. %s may require to be reregistered for options to take effect.", toUpperCaseFirst(this.name())); String toggle = format("Restore all configured options"); MessageDialogWithToggle dialog = new MessageDialogWithToggle(this.getShell(), title, null, message, MessageDialog.WARNING, new String[] {PROCEED_LABEL, CANCEL_LABEL}, 1, toggle, false); if (dialog.open() == 1) { return; } Map<String, Object> defaults = readFromOptions(this.options(this.defaultPreferences(), this.registration)); if (!dialog.getToggleState()) { IStructuredSelection selection = (IStructuredSelection) this.tableViewer.getSelection(); if (!selection.isEmpty()) { for (Object item: selection.toList()) { String key = ((Entry<?, ?>) item).getKey().toString(); this.map.put(key, defaults.get(key)); } } else { message = "No options selected and restore all unchecked."; MessageDialog.openError(this.getShell(), title, message); } } else { this.map = defaults; } this.updateTable(); this.sortTable(); this.updateButtons(); } final void configure() { this.applyInternal(); } abstract void apply(); abstract void load(P preferences, R registration); private void applyInternal() { try { this.apply(); } catch (RuntimeException failure) { String title = "Options"; String message = "Failed to apply options."; String reason = failure.getLocalizedMessage(); message += !isNullOrEmpty(reason) ? format("%n%n%s%n%n", reason) : " "; openError(this.getShell(), title, message + "See error log for more details."); Activator.defaultInstance().getConsole().error(failure, message); } } private void loadInternal(final P preferences, final R registration) { this.load(preferences, registration); this.updateTable(); this.sortTable(); this.updateButtons(); } final void updateStatusBy(final Registrable registrable) { StatusSeverity severity; String message; if (registrable instanceof Configurable) { severity = StatusSeverity.INFO; message = toUpperCaseFirst(this.name()) + " is configurable by default, but may require to be reregistered to apply specified options"; } else { severity = StatusSeverity.WARNING; message = toUpperCaseFirst(this.name()) + " is not configurable by default, it may completely ignore specified options"; } this.updateStatus(new Status(severity.getValue(), Activator.PLUGIN_ID, IStatus.OK, message, null)); } static final <K> Map<K, Options> updateData(final Map<K, Options> data, final K key, final Options options) { Map<K, Options> update = newHashMap(data); update.put(key, options); return update; } static final Map<String, Object> readFromOptions(final Options ... options) { Map<String, Object> map = newLinkedHashMap(); for (Options partial: options) { if (partial != null) { map.putAll(partial.toMap()); } } return map; } static final Options writeToOptions(@Nullable final Options options, final Map<String, Object> map) { checkNotNull(map); if (options != null) { try { options.fromMap(map); return options; } catch (UnsupportedOperationException e) { // ignore } if (options instanceof MapOptions) { Putter putter = ((MapOptions) options).putter(); return MapOptions.from(map, putter); } } return MapOptions.from(map); } final void setPreferences(final P preferences) { this.preferences = checkNotNull(preferences); } final void setRegistration(final R registration) { this.registration = checkNotNull(registration); } final P getPreferences() { return this.preferences; } final R getRegistration() { return this.registration; } @Override protected IDialogSettings getDialogBoundsSettings() { return DialogSettings.getOrCreateSection(Activator.defaultInstance().getDialogSettings(), AbstractOptionsDialog.class.getName()); } @Override public boolean isHelpAvailable() { return false; } @Override protected boolean isResizable() { return true; } }