/** * This file is part of Archiv-Editor. * * The software Archiv-Editor serves as a client user interface for working with * the Person Data Repository. See: pdr.bbaw.de * * The software Archiv-Editor was developed at the Berlin-Brandenburg Academy * of Sciences and Humanities, Jägerstr. 22/23, D-10117 Berlin. * www.bbaw.de * * Copyright (C) 2010-2013 Berlin-Brandenburg Academy * of Sciences and Humanities * * The software Archiv-Editor was developed by @author: Christoph Plutte. * * Archiv-Editor is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Archiv-Editor 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Archiv-Editor. * If not, see <http://www.gnu.org/licenses/lgpl-3.0.html>. */ package org.bbaw.pdr.ae.export.swt; import java.io.File; import org.bbaw.pdr.ae.common.AEConstants; import org.bbaw.pdr.ae.common.CommonActivator; import org.bbaw.pdr.ae.common.NLMessages; import org.bbaw.pdr.ae.export.pluggable.AeExportCoreProvider; import org.eclipse.core.runtime.ILog; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.ComboViewer; import org.eclipse.jface.viewers.ContentViewer; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.wizard.IWizard; import org.eclipse.jface.wizard.IWizardContainer; import org.eclipse.jface.wizard.WizardPage; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; //TODO: doku /** * {@link AeExportCoreProvider#getFileHistory(String)} * {@link SWT#SAVE} * {@link #init(String, int)} * @author jhoeper * */ public class FileSelectionGroup extends ContentViewer implements IPdrWidgetStructure { /** * Opens a {@link FileDialog} for selection of a file from disk, * passes resulting file name to the corresponding {@link ComboViewer} and * asks {@link AeExportCoreProvider} to save it to the responsible file name history. * @author jhoeper * */ private class SelectButtonListener extends SelectionAdapter { @Override public void widgetSelected(SelectionEvent event) { String[] segments = getFileLocation(); // create file browser dialog FileDialog dialog = new FileDialog(container.getShell(), selectMode); dialog.setFilterPath(segments[0]); dialog.setFileName(segments[1]); dialog.setText(NLMessages.getString("export.fileselector.browse.caption")); dialog.setFilterExtensions( provider.getPluginFiletypes(pluginId, (selectMode == SWT.SAVE) ? AeExportCoreProvider.EXT_OUT : AeExportCoreProvider.EXT_IN, configuration)); // open dialog String dest = dialog.open(); // process resulting file name if (dest != null) { // update filename combo filenameCombo.insert(dest, 0); filenameCombo.getCombo().select(0); // save file name to history of recent files provider.addToHistory(pluginId, configuration, dest); } } } private class FilenameChangedListener implements ISelectionChangedListener, ModifyListener { public FilenameChangedListener() { filenameCombo.addSelectionChangedListener(this); filenameCombo.getCombo().addModifyListener(this); } @Override public void selectionChanged(SelectionChangedEvent event) { //System.out.println("combo selection changed"); //FIXME: viel zu früh! damit wird automatisch der wizard angesprochen, der dann rückmeldung // von allen seinen GUI-komponenten will, die aber noch gar nicht alle fertig sind! validate(); } @Override public void modifyText(ModifyEvent event) { //System.out.println("output file string changed"); validate(); } } // logger ILog log = AEConstants.ILOGGER; private Group container; private Label dsclabel; private Button checkEnable; private ComboViewer filenameCombo; private Button browseButton; //TODO: 'overwrite existent files' check button? private String pluginId; // corresponds to input/output sets in extension point private String configuration; // whatever the plugin specifies as default file for a configuration private String default_filename; private AeExportCoreProvider provider; private WizardPage wizardPage; // open/save to file private int selectMode; private boolean isvalid; private String message; //TODO doc wizard /** * <p>Creates a new group of widgets for file selection functionality.</p> * <p>The framing container will be a simple {@link Group} with a border * as defined by the SWT style {@link SWT#SHADOW_ETCHED_IN}. In order to * have all involved widgets set up, {@link #init(String, int, int)} has to * be called before use. If a custom SWT style is desired to have for * the widget group, the constructor {@link #FileSelectionGroup(Composite, * int, String)} can be used alternatively. That one also allows for * specifying the mode of file access this template is desired to provide * (<code>SWT.SAVE/SWT.OPEN</code>). The default is <code>SWT.OPEN</code>. * </p> * <p>Two arguments are required: the {@link Composite} in which the newly * created GUI elements are to be embedded, and the symbolic name of the * plugin contributing the export wizard which this file selection group * is for. The plugin name is neccessary for identifying the internal * file history, which unless further specification is the very one that is * made available by the core export plugin's {@link AeExportCoreProvider} * for every registered export plugin.</p> * <p>Export plugins that wish to use this GUI template can retrieve their * own plugin id by calling * <blockquote><code>FrameworkUtil.getBundle(getClass()).getSymbolicName(); * </code></blockquote> * The plugin's standard file history can be and is in fact by this * construtor obtained via {@link * AeExportCoreProvider#getFileHistory(String)}. * </p> * @param parent Composite into which the newly created group will be * embedded * @param plugin The symbolic name of the plugin contributing the dialog * that wishes to use this template */ public FileSelectionGroup(String plugin, WizardPage page) { this(plugin, page, SWT.BORDER | SWT.SHADOW_ETCHED_IN | SWT.OPEN); } //TODO doc wizard /** * <p>Creates a new group of widgets for file selection functionality.</p> * <p>Customizable version of {@link #FileSelectionGroup(Composite, String)}. * Both constructors do exactly the same. The main reason for this one to * exist is the extra information that the <code>style</code> argument can * deliver. If in the binary representation of <code>style</code>, the bit * for the style flag {@link SWT#CHECK} is set, the template will be * equipped with an extra check box button for enabling/disabling * the entire ensemble. With the flag {@link SWT#SAVE} set, the template * will act as a selection tool for output files. Without, its default * behaviour as a widget ensemble for opening existent files is kept.</p> * <p>On further requirements, please read the base constructor {@link * #FileSelectionGroup(Composite, String)}'s documentation.</p> * @param parent Composite into which the newly created group will be * embedded * @param style Combination of desired SWT style flags by bitwise OR. If * <code>style</code> contains the bit for {@link SWT#CHECK}, the template * will be equipped with an extra check box button for enabling/disabling * the entire ensemble. * @param plugin The symbolic name of the plugin contributing the dialog * that wishes to use this template */ public FileSelectionGroup(String plugin, WizardPage page, Composite parent, int style) { container = new Group(parent, style | SWT.SHADOW_ETCHED_IN); container.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); container.setLayout(new GridLayout(3, false)); this.wizardPage=page; pluginId = plugin; selectMode = (style & SWT.SAVE) == SWT.SAVE ? SWT.SAVE : SWT.OPEN; configuration = AeExportCoreProvider.DEF_SET; } //TODO doc /** * Creates an instance for the given plugin and wizard page and applies * style to it * @param plugin * @param page * @param style */ public FileSelectionGroup(String plugin, WizardPage page, int style) { this(plugin, page, (Composite)page.getControl(), style); } /** * Initializes the embedded widgets serving as the components of this * file selector template. * <p>The arrangement of the template components will be more or less like * this: * <blockquote> * <table style="border:1px solid;"><tr> * <td style="border:1px solid;">label</td> * <td style="border:1px solid;">combo viewer</td> * <td style="border:1px solid;">button</td></tr></table> * </blockquote> * </p> * @param label Descriptive text label * @param colspan Number of columns the template is desired to cover within * its parent's layout * @see #init(String, String, int) */ public void init(String label, int colspan) { // initialize connection to central export functionalities provider = AeExportCoreProvider.getInstance(); ((GridData)container.getLayoutData()).horizontalSpan=colspan; if ((container.getStyle() & SWT.CHECK) == SWT.CHECK) { checkEnable = new Button(container, SWT.CHECK); checkEnable.setText(label); //TODO: enabler listener } else { dsclabel = new Label(container, SWT.LEFT); dsclabel.setText(label); } filenameCombo = new ComboViewer(container, SWT.ALL); Combo combo = filenameCombo.getCombo(); combo.setLayoutData( new GridData(SWT.FILL, SWT.CENTER, true, false)); combo.setEnabled(true); filenameCombo.setLabelProvider(new LabelProvider()); filenameCombo.setContentProvider(ArrayContentProvider.getInstance()); default_filename = provider.getWizardProvider( wizardPage.getWizard()).getDefaultFilename(configuration); log.log(new Status(IStatus.INFO, CommonActivator.PLUGIN_ID, " Default file name for configuration '"+configuration+"': "+default_filename)); String[] recent = provider.getHistoryAsArray(pluginId, configuration); log.log(new Status(IStatus.INFO, CommonActivator.PLUGIN_ID, " Recent file list for configuration '"+configuration+"': "+recent.length)); // if history could be restored if (recent.length>0) filenameCombo.setInput(recent); else if (default_filename != null) filenameCombo.setInput(new String[]{default_filename}); if (combo.getItemCount() > 0) combo.setText(combo.getItem(0)); //validate(); new FilenameChangedListener(); //TODO: listener on losing focus!!! browseButton = new Button(container, SWT.PUSH); browseButton.setLayoutData( new GridData(SWT.FILL, SWT.CENTER, false, false)); browseButton.setText(NLMessages.getString("export.fileselector.browse.button")); browseButton.addSelectionListener( new SelectButtonListener() ); // check if everything is correct. validate(); } //TODO: doc public void init(String label, int colspan, String filetypeSetName) { this.configuration = filetypeSetName; init(label, colspan); } /** * Does exactly the same as {@link #init(String, int)}, but also sets * a text label for the widget group itself, meaning that there will be * an additional description displayed, most likely, but dependent on the * running platform's look and feel, being placed on the surrounding border. * @param boxlabel Top-level description label for this widget group * @param label Descriptive label for the functionality this template * provides * @param colspan Number of columns this template should cover in its * parent's layout */ public void init(String boxlabel, String label, int colspan) { container.setText(boxlabel); init(label, colspan); } //TODO:doc public void init(String boxlabel, String label, int colspan, String filetypeSetName) { this.configuration = filetypeSetName; init(boxlabel, label, colspan); } /** * Returns a <code>String[]</code> array of length 2, the elements of * which represent a directory path and a base filename. * <p>If the {@link ComboViewer} widget of this {@link FileSelectionGroup} * currently contains a valid file system path, the text content of the * combo is segmented into an absolute directory and a relative file name. * TODO * If no directory segment can be derived from the combo's textual content, * but a valid file name is available, then the latter will be understood * relatively to the directory specified by {@link AEConstants#AE_HOME}</p> * @return <code>String[]{directory, filename}</code> */ public String[] getFileLocation() { File file = resolvPath(); IPath path = new Path(file.getAbsolutePath()); String filename = path.lastSegment(); String dir = path.removeLastSegments(1).toOSString(); /* System.out.println("File location:"); System.out.println(dir); System.out.println(filename);*/ return new String[]{dir, filename}; } /** * Return a resolved {@link File} representation of the widget's current * content. If this {@link FileSelectionGroup} is configured as an output * file selector and points to an existing file, a dialog is raised * to let the user decide whether to overwrite the file. * @return null if the file exists and the user doesn't agree in * overwriting it, {@link File} otherwise */ public File getFile() { File file = resolvPath(); // raise dialog in case file is to be overwritten if (selectMode == SWT.SAVE) if (file.exists()) return MessageDialog.openConfirm(container.getShell(), NLMessages.getString("export.fileselector.file_exists.caption"), file.getAbsolutePath()+NLMessages.getString("Export_Dialog_FileExistsQuestion")) ? file : null; return file; } /** * determines whether the current widget content represents a file usable * as a input resource. */ private void validateIn(File file) { if (file.isDirectory()) setInvalid(NLMessages.getString("export.fileselector.no_filename")); // Export_Filename_ErrorNoName else if (!file.exists()) setInvalid(NLMessages.getString("export.fileselector.no_file")); else if (!file.canRead()) setInvalid(NLMessages.getString("export.fileselector.not_permitted")); else setValid(); } /** * determines whether the current widget content can be used as an output * file locator */ private void validateOut(File file) { //System.out.println("Validate output file location "+file.getPath()); if (file.isDirectory()) { setInvalid(NLMessages.getString("export.fileselector.no_filename")); } else if (file.exists()) { if (!file.canWrite()) setInvalid(NLMessages.getString("export.fileselector.not_permitted")); else setValid(); } else if (!file.getParentFile().canWrite()) setInvalid(NLMessages.getString("export.fileselector.not_permitted")); else setValid(); } /** * Evaluates the current contents of the involved widgets and decides on * overall validity. */ private void validate() { //System.out.println("validating input"); File file = resolvPath(); if (file == null) { setInvalid(NLMessages.getString("export.fileselector.invalid_filename")); return; } if (selectMode == SWT.OPEN) validateIn(file); else if (selectMode == SWT.SAVE) validateOut(file); } //TODO /** * Tries to resolve the user input to a valid, absolute file location * identifier which then is returned as a {@link File} object. * <p>This is really only about having a clean file identifier. There is no * validation whatsoever involved in terms of read/write permissions, * file/directory resource or filetype extension.</p> * @return {@link File} object representing the resolved user input, * {@link AEConstants#AE_HOME} if said input cannot be resolved */ public File resolvPath() { IPath path = Path.EMPTY; String input = filenameCombo.getCombo().getText(); //TODO: return directory? is that smart? given that this will be //forwarded to method that rely on getting a proper file? /*if (!path.isValidPath(input)) return new File(AEConstants.AE_HOME + AEConstants.FS);*/ path = new Path(input); if (!path.isAbsolute()) { path = new Path(AEConstants.AE_HOME).append(path); path.makeAbsolute(); } return new File(path.toOSString()); } /** * Indicates whether or not this widget ensemble is in an overall state * that allows for i.e. involved wizard dialogs to continue. * @return <code>true</code> if the widget group's contents are legal */ @Override public boolean isValid() { //System.out.println(this.getClass().getCanonicalName()+(isvalid ? " is valid." : " is not valid")); return isvalid; } /** * Mark this widget valid. Also make wizardcontainer update its controls, which * means that the responsible wizard is made to reevaluate its * {@link IWizard#canFinish()}, which can be overwritten in order to consider * the {@link IPdrWidgetStructure#isValid()} status of further widgets involved. */ private void setValid() { isvalid = true; this.message = null; IWizardContainer wizardContainer = this.wizardPage.getWizard().getContainer(); try { wizardContainer.updateButtons(); wizardContainer.updateMessage(); } catch (Exception e) { log.log(new Status(IStatus.WARNING, CommonActivator.PLUGIN_ID, " Wizard noch nicht bereit? "+wizardPage.getClass().getCanonicalName())); } } /** * Mark this widget invalid. * @param message Complementary error message. */ private void setInvalid(String message) { //System.out.println("Mark file selector as invalid"); isvalid = false; this.message = message; IWizardContainer wizardContainer = this.wizardPage.getWizard().getContainer(); wizardContainer.updateButtons(); wizardContainer.updateMessage(); } @Override public String getMessage() { return message; } @Override public Control getControl() { if (filenameCombo != null) return filenameCombo.getControl(); else return null; } @Override public ISelection getSelection() { if (filenameCombo != null) return new StructuredSelection(filenameCombo.getCombo().getText()); return null; } @Override public void refresh() { // TODO Auto-generated method stub } @Override public void setSelection(ISelection selection, boolean reveal) { // TODO Auto-generated method stub } @Override public void performFinish() { if (filenameCombo != null) { provider.addToHistory(pluginId, configuration, resolvPath().getAbsolutePath()); String[] locators = getFileLocation(); provider.getSettings(pluginId).put(AeExportCoreProvider.DEF_DIR, locators[0]); provider.getSettings(pluginId).put(AeExportCoreProvider.DEF_FILE, locators[1]); } } }