diff --git a/README.md b/README.md index e378f16ae..f2a9adc0c 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ This project contains the open source core of [RapidMiner Studio](https://rapidm ## Getting Started -* [Install](https://rapidminer.com/studio) RapidMiner Studio -* Have a look at our [Getting Started](http://docs.rapidminer.com/studio/getting-started/) Videos +* [Install](https://rapidminer.com/products/studio/) RapidMiner Studio +* Have a look at our [Getting Started Central](https://rapidminer.com/getting-started-central/) * You miss something? There might be an [Extension](https://marketplace.rapidminer.com) for it * Have questions? Check out our official [community](https://community.rapidminer.com) and [documentation](https://docs.rapidminer.com) @@ -95,9 +95,9 @@ class CliLauncher { ## Diving in -* Create your own [Extension](http://docs.rapidminer.com/developers/creating-your-own-extension/) -* [Integrate](http://community.rapidminer.com/t5/Become-a-RapidMiner-Developer/Frequently-Asked-Questions-Development/m-p/19782) RapidMiner Studio Core into your project -* And much more at our [Developer Board](http://community.rapidminer.com/t5/Become-a-RapidMiner-Developer/bd-p/BARDDBoard) +* Create your own [Extension](https://docs.rapidminer.com/latest/developers/creating-your-own-extension/) +* [Integrate](https://community.rapidminer.com/t5/Become-a-RapidMiner-Developer/Frequently-Asked-Questions-Development/m-p/19782) RapidMiner Studio Core into your project +* And much more at our [Developer Board](https://community.rapidminer.com/t5/Become-a-RapidMiner-Developer/bd-p/BARDDBoard) ## License diff --git a/build.gradle b/build.gradle index 268aa8f03..f049d3b16 100644 --- a/build.gradle +++ b/build.gradle @@ -20,11 +20,11 @@ repositories { dependencies { // OS X adapter to add platform specific UI - compile 'com.rapidminer.studio:rapidminer-studio-osx-adapter:1.0.1' + compile 'com.rapidminer.studio:rapidminer-studio-osx-adapter:1.0.2' // RapidMiner license framework for license management - compile 'com.rapidminer.license:rapidminer-license-api:4.0.0' - compile('com.rapidminer.license:rapidminer-license-commons:4.0.0'){ + compile 'com.rapidminer.license:rapidminer-license-api:4.0.2' + compile('com.rapidminer.license:rapidminer-license-commons:4.0.2'){ exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind' } @@ -36,7 +36,7 @@ dependencies { compile 'com.rapidminer.external:alphanumeric-sorting:1.0.1' // VLDocking as docking framework (https://code.google.com/p/vldocking/) - compile 'com.rapidminer.external:vldocking:1.2.1' + compile 'com.rapidminer.external:vldocking:1.2.2' // Freehep for vector graphic export (http://java.freehep.org/) compile('org.freehep:freehep-graphicsio-ps:2.3') { diff --git a/gradle.properties b/gradle.properties index 8fb880578..369fdfb63 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=8.1.0 +version=8.2.0 group=com.rapidminer.studio diff --git a/src/main/java/com/rapidminer/FileProcessLocation.java b/src/main/java/com/rapidminer/FileProcessLocation.java index 7539fa363..2a64a25f9 100644 --- a/src/main/java/com/rapidminer/FileProcessLocation.java +++ b/src/main/java/com/rapidminer/FileProcessLocation.java @@ -18,13 +18,6 @@ */ package com.rapidminer; -import com.rapidminer.io.process.XMLImporter; -import com.rapidminer.io.process.XMLTools; -import com.rapidminer.tools.LogService; -import com.rapidminer.tools.ProgressListener; -import com.rapidminer.tools.Tools; -import com.rapidminer.tools.XMLException; - import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; @@ -35,6 +28,13 @@ import org.w3c.dom.Document; +import com.rapidminer.io.process.XMLImporter; +import com.rapidminer.io.process.XMLTools; +import com.rapidminer.tools.LogService; +import com.rapidminer.tools.ProgressListener; +import com.rapidminer.tools.Tools; +import com.rapidminer.tools.XMLException; + /** * @@ -42,6 +42,8 @@ */ public class FileProcessLocation implements ProcessLocation { + private static final String FILE_PROCESS_ICON = "hard_drive.png"; + private final File file; public FileProcessLocation(File file) { @@ -129,4 +131,9 @@ public String toString() { public String getShortName() { return file.getName(); } + + @Override + public String getIconName() { + return FILE_PROCESS_ICON; + } } diff --git a/src/main/java/com/rapidminer/Process.java b/src/main/java/com/rapidminer/Process.java index 5e1d62c39..d905732b4 100644 --- a/src/main/java/com/rapidminer/Process.java +++ b/src/main/java/com/rapidminer/Process.java @@ -37,6 +37,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.FileHandler; import java.util.logging.Handler; import java.util.logging.Level; @@ -175,7 +176,7 @@ public class Process extends AbstractObservable implements Cloneable { private final List breakpointListeners = Collections.synchronizedList(new LinkedList<>()); /** The list of filters called between each operator */ - private final List processFlowFilters = Collections.synchronizedList(new LinkedList<>()); + private final CopyOnWriteArrayList processFlowFilters = new CopyOnWriteArrayList<>(); /** The listeners for logging (data tables). */ private final List loggingListeners = Collections.synchronizedList(new LinkedList<>()); @@ -307,9 +308,10 @@ public Process(final InputStream in) throws IOException, XMLException { /** Reads an process configuration from the given URL. */ public Process(final URL url) throws IOException, XMLException { initContext(); - Reader in = new InputStreamReader(WebServiceTools.openStreamFromURL(url), getEncoding(null)); - readProcess(in); - in.close(); + try (Reader in = new InputStreamReader(WebServiceTools.openStreamFromURL(url), getEncoding(null))) { + readProcess(in); + } + } protected Logger makeLogger() { @@ -730,9 +732,7 @@ public void addProcessFlowFilter(ProcessFlowFilter filter) { if (filter == null) { throw new IllegalArgumentException("filter must not be null!"); } - if (!processFlowFilters.contains(filter)) { - processFlowFilters.add(filter); - } + processFlowFilters.addIfAbsent(filter); } /** @@ -768,9 +768,13 @@ public void fireProcessFlowBeforeOperator(Operator previousOperator, Operator ne if (input == null) { input = Collections.emptyList(); } - synchronized (processFlowFilters) { - for (ProcessFlowFilter filter : processFlowFilters) { + for (ProcessFlowFilter filter : processFlowFilters) { + try { filter.preOperator(previousOperator, nextOperator, input); + } catch (OperatorException oe) { + throw oe; + } catch (Exception e) { + getLogger().log(Level.WARNING, "com.rapidminer.Process.process_flow_filter_failed", e); } } } @@ -796,9 +800,13 @@ public void fireProcessFlowAfterOperator(Operator previousOperator, Operator nex if (output == null) { output = Collections.emptyList(); } - synchronized (processFlowFilters) { - for (ProcessFlowFilter filter : processFlowFilters) { + for (ProcessFlowFilter filter : processFlowFilters) { + try { filter.postOperator(previousOperator, nextOperator, output); + } catch (OperatorException oe) { + throw oe; + } catch (Exception e) { + getLogger().log(Level.WARNING, "com.rapidminer.Process.process_flow_filter_failed", e); } } } @@ -815,11 +823,8 @@ public void copyProcessFlowListenersToOtherProcess(Process otherProcess) { if (otherProcess == null) { throw new IllegalArgumentException("otherProcess must not be null!"); } - - synchronized (processFlowFilters) { - for (ProcessFlowFilter filter : processFlowFilters) { - otherProcess.addProcessFlowFilter(filter); - } + for (ProcessFlowFilter filter : processFlowFilters) { + otherProcess.addProcessFlowFilter(filter); } } diff --git a/src/main/java/com/rapidminer/ProcessLocation.java b/src/main/java/com/rapidminer/ProcessLocation.java index 54c805154..5351292fb 100644 --- a/src/main/java/com/rapidminer/ProcessLocation.java +++ b/src/main/java/com/rapidminer/ProcessLocation.java @@ -18,11 +18,11 @@ */ package com.rapidminer; +import java.io.IOException; + import com.rapidminer.tools.ProgressListener; import com.rapidminer.tools.XMLException; -import java.io.IOException; - /** * A place where a process can be saved. Basically, this is either a file or a location in a @@ -33,27 +33,35 @@ public interface ProcessLocation { /** Reads the process and returns it. */ - public Process load(ProgressListener listener) throws IOException, XMLException; + Process load(ProgressListener listener) throws IOException, XMLException; /** Stores the process at the referenced location. */ - public void store(Process process, ProgressListener listener) throws IOException; + void store(Process process, ProgressListener listener) throws IOException; /** The toString representation is used, e.g. in the welcome screen dialog, */ @Override - public String toString(); + String toString(); /** * Reads the contents of the referenced resource and returns the XML without parsing it. Used if * process file is broken. */ - public String getRawXML() throws IOException; + String getRawXML() throws IOException; /** Returns a string saved to the history file. */ - public String toHistoryFileString(); + String toHistoryFileString(); /** Returns a string as it is displayed in the recent files menu. */ - public String toMenuString(); + String toMenuString(); /** Returns a short name, e.g. the last component of the path. */ - public String getShortName(); + String getShortName(); + + /** + * The icon name without size modifier that visualizes where this process is stored, e.g. RM Server repo, local repo, Cloud, ... + * + * @return the name of the icon to visualize the origin of this recent process + * @since 8.2 + */ + String getIconName(); } diff --git a/src/main/java/com/rapidminer/RepositoryProcessLocation.java b/src/main/java/com/rapidminer/RepositoryProcessLocation.java index e2eed1c14..9480cb9b7 100644 --- a/src/main/java/com/rapidminer/RepositoryProcessLocation.java +++ b/src/main/java/com/rapidminer/RepositoryProcessLocation.java @@ -31,6 +31,7 @@ import com.rapidminer.repository.internal.remote.RemoteContentManager; import com.rapidminer.repository.internal.remote.RemoteProcessEntry; import com.rapidminer.repository.internal.remote.RemoteRepository; +import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.PasswordInputCanceledException; import com.rapidminer.tools.ProgressListener; @@ -48,8 +49,8 @@ public class RepositoryProcessLocation implements ProcessLocation { /** A simple {@link UserData} object to pass {@link Boolean} values */ public static class SimpleBooleanUserData implements UserData { - private boolean value; + private boolean value; public SimpleBooleanUserData(boolean value) { this.value = value; } @@ -63,8 +64,11 @@ public boolean isSet() { return value; } + } + private static final String GENERIC_PROCESS_ICON = I18N.getGUILabel("getting_started.open_recent.icon"); + private final RepositoryLocation repositoryLocation; public RepositoryProcessLocation(RepositoryLocation location) { @@ -194,4 +198,14 @@ public int hashCode() { public String getShortName() { return repositoryLocation.getName(); } + + @Override + public String getIconName() { + try { + return getRepositoryLocation().getRepository().getIconName(); + } catch (Exception e) { + // can happen if a repository was removed, do not log anything in those cases + return GENERIC_PROCESS_ICON; + } + } } diff --git a/src/main/java/com/rapidminer/example/AttributeError.java b/src/main/java/com/rapidminer/example/AttributeError.java new file mode 100644 index 000000000..fdfc08a4e --- /dev/null +++ b/src/main/java/com/rapidminer/example/AttributeError.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ + +package com.rapidminer.example; + +import java.io.Serializable; + +import com.rapidminer.operator.OperatorException; +import com.rapidminer.operator.UserError; + + +/** + * POJO to capture the information on an attribute related runtime exception. Can create an {@link OperatorException} + * from the captured information. + * + * @author Jan Czogalla + * @since 8.2 + */ +class AttributeError implements Serializable { + + String baseKey; + String name; + boolean isRole; + boolean isUserError; + + /** + * Creates an {@link OperatorException} and associated the specified stacktrace with it. Can differentiate between + * {@link UserError} and {@link OperatorException}. + * + * @param stackTrace + * the stacktrace to be attached to the returned excpetion + * @return an operator exception or user error + */ + OperatorException toOperatorException(StackTraceElement[] stackTrace) { + String actualKey = baseKey + (isRole ? "_role" : ""); + OperatorException exception; + if (isUserError) { + exception = new UserError(null, actualKey, name); + } else { + exception = new OperatorException(actualKey, null, name); + } + exception.setStackTrace(stackTrace); + return exception; + } +} diff --git a/src/main/java/com/rapidminer/example/Attributes.java b/src/main/java/com/rapidminer/example/Attributes.java index 4aab56446..5594cf01f 100644 --- a/src/main/java/com/rapidminer/example/Attributes.java +++ b/src/main/java/com/rapidminer/example/Attributes.java @@ -272,9 +272,9 @@ public interface Attributes extends Iterable, Cloneable, Serializable /** * Finds the {@link AttributeRole} belonging to the attribute with the given name (both regular - * and special). If the search is performed case sensitive depends on the boolean parameter. - * Attention: Case insensitive search is not optimized and takes linear time with number of - * attributes. + * and special). Whether the search is performed case sensitive depends on the boolean parameter. + *

+ * Attention: Case insensitive search is not optimized and takes linear time with number of attributes. */ public AttributeRole findRoleByName(String name, boolean caseSensitive); @@ -282,9 +282,10 @@ public interface Attributes extends Iterable, Cloneable, Serializable public AttributeRole findRoleBySpecialName(String specialName); /** - * Finds the {@link AttributeRole} with the given special name (both regular and special). If - * the search is performed case sensitive depends on the boolean parameter. Attention: Case - * insensitive search is not optimized and takes linear time with number of attributes. + * Finds the {@link AttributeRole} with the given special name (both regular and special). Whether + * the search is performed case sensitive depends on the boolean parameter. + *

+ * Attention: Case insensitive search is not optimized and takes linear time with number of attributes. */ public AttributeRole findRoleBySpecialName(String specialName, boolean caseSensitive); diff --git a/src/main/java/com/rapidminer/example/DuplicateAttributeException.java b/src/main/java/com/rapidminer/example/DuplicateAttributeException.java new file mode 100644 index 000000000..4cc62196e --- /dev/null +++ b/src/main/java/com/rapidminer/example/DuplicateAttributeException.java @@ -0,0 +1,74 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ + +package com.rapidminer.example; + +import com.rapidminer.operator.OperatorException; +import com.rapidminer.operator.OperatorRuntimeException; + + +/** + * Exception class that indicates that an {@link Attribute} or {@link AttributeRole} does already exist in a context. + * Uses an {@link AttributeError} object to store the information and provide an appropriate {@link com.rapidminer.operator.UserError UserError} + * when caught in an {@link com.rapidminer.operator.Operator Operator}. + *

+ * This extends {@link IllegalArgumentException} for legacy reasons and implements {@link OperatorRuntimeException} to + * counter unwanted side effects. + * + * @author Jan Czogalla + * @since 8.2 + */ +public class DuplicateAttributeException extends IllegalArgumentException implements OperatorRuntimeException { + + private final AttributeError error; + + /** + * Creates an exception to indicate an existing attribute with the given name. Same as + * {@link #DuplicateAttributeException(String, boolean) DuplicateAttributeException(name, false)}. + */ + public DuplicateAttributeException(String name) { + this(name, false); + } + + /** + * Creates an exception to indicate an existing attribute or attribute role with the given name. + * + * @param name + * the name of the attribute (role) + * @param isRole + * whether the name corresponds to an attribute role or not + */ + public DuplicateAttributeException(String name, boolean isRole) { + this.error = new AttributeError(); + error.baseKey = "duplicate_attribute"; + error.name = name; + error.isRole = isRole; + error.isUserError = true; + } + + @Override + public String getMessage() { + return toOperatorException().getMessage(); + } + + @Override + public OperatorException toOperatorException() { + return error.toOperatorException(getStackTrace()); + } +} diff --git a/src/main/java/com/rapidminer/example/NoSuchAttributeException.java b/src/main/java/com/rapidminer/example/NoSuchAttributeException.java new file mode 100644 index 000000000..17ff997ea --- /dev/null +++ b/src/main/java/com/rapidminer/example/NoSuchAttributeException.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ + +package com.rapidminer.example; + +import java.util.NoSuchElementException; + +import com.rapidminer.operator.OperatorException; +import com.rapidminer.operator.OperatorRuntimeException; + + +/** + * Exception class that indicates that an {@link Attribute} or {@link AttributeRole} does not exist in a context. + * Uses an {@link AttributeError} object to store the information and provide an appropriate {@link com.rapidminer.operator.UserError UserError} + * when caught in an {@link com.rapidminer.operator.Operator Operator}. + *

+ * This extends {@link NoSuchElementException} for legacy reasons and implements {@link OperatorRuntimeException} to + * counter unwanted side effects. + * + * @author Jan Czogalla + * @since 8.2 + */ +public class NoSuchAttributeException extends NoSuchElementException implements OperatorRuntimeException { + + private final AttributeError error; + + /** + * Creates an exception to indicate a nonexistent attribute with the given name. Same as + * {@link #NoSuchAttributeException(String, boolean) NoSuchAttributeException(name, false)}. + */ + public NoSuchAttributeException(String name) { + this(name, false); + } + + /** + * Creates an exception to indicate a nonexistent attribute or attribute role with the given name. + * + * @param name + * the name of the attribute (role) + * @param isRole + * whether the name corresponds to an attribute role or not + */ + public NoSuchAttributeException(String name, boolean isRole) { + this.error = new AttributeError(); + error.baseKey = "no_such_attribute"; + error.name = name; + error.isRole = isRole; + error.isUserError = true; + } + + @Override + public String getMessage() { + return toOperatorException().getMessage(); + } + + @Override + public OperatorException toOperatorException() { + return error.toOperatorException(getStackTrace()); + } +} diff --git a/src/main/java/com/rapidminer/example/SimpleAttributes.java b/src/main/java/com/rapidminer/example/SimpleAttributes.java index c8754b358..499e472aa 100644 --- a/src/main/java/com/rapidminer/example/SimpleAttributes.java +++ b/src/main/java/com/rapidminer/example/SimpleAttributes.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.NoSuchElementException; /** @@ -121,13 +120,11 @@ public void remove() { private void register(AttributeRole attributeRole, boolean onlyMaps) { String name = attributeRole.getAttribute().getName(); if (nameToAttributeRoleMap.containsKey(name)) { - throw new IllegalArgumentException("Duplicate attribute name: " + name); + throw new DuplicateAttributeException(name); } String specialName = attributeRole.getSpecialName(); - if (specialName != null) { - if (specialNameToAttributeRoleMap.containsKey(specialName)) { - throw new IllegalArgumentException("Duplicate attribute role: " + specialName); - } + if (specialName != null && specialNameToAttributeRoleMap.containsKey(specialName)) { + throw new DuplicateAttributeException(name, true); } this.nameToAttributeRoleMap.put(name, attributeRole); if (specialName != null) { @@ -167,8 +164,7 @@ public void rename(AttributeRole attributeRole, String newSpecialName) { if (attributeRole.getSpecialName() != null) { AttributeRole role = specialNameToAttributeRoleMap.get(attributeRole.getSpecialName()); if (role == null) { - throw new NoSuchElementException( - "Cannot rename attribute role. No such attribute role: " + attributeRole.getSpecialName()); + throw new NoSuchAttributeException(attributeRole.getSpecialName(), true); } if (role != attributeRole) { throw new RuntimeException("Broken attribute role map."); @@ -184,11 +180,11 @@ public void rename(AttributeRole attributeRole, String newSpecialName) { @Override public void rename(Attribute attribute, String newName) { if (nameToAttributeRoleMap.containsKey(newName)) { - throw new IllegalArgumentException("Cannot rename attribute. Duplicate name: " + newName); + throw new DuplicateAttributeException(newName); } AttributeRole role = nameToAttributeRoleMap.get(attribute.getName()); if (role == null) { - throw new NoSuchElementException("Cannot rename attribute. No such attribute: " + attribute.getName()); + throw new NoSuchAttributeException(attribute.getName()); } if (role.getAttribute() != attribute) { // this cannot happen @@ -210,27 +206,37 @@ public boolean remove(AttributeRole attributeRole) { @Override public AttributeRole findRoleByName(String name, boolean caseSensitive) { - if (caseSensitive) { - return nameToAttributeRoleMap.get(name); - } else { - String lowerSearchTerm = name.toLowerCase(); - for (Entry entry : nameToAttributeRoleMap.entrySet()) { - if (lowerSearchTerm.equals(entry.getKey().toLowerCase())) { - return entry.getValue(); - } - } - return null; - } + return findRole(name, caseSensitive, nameToAttributeRoleMap); } @Override public AttributeRole findRoleBySpecialName(String specialName, boolean caseSensitive) { - if (caseSensitive) { - return specialNameToAttributeRoleMap.get(specialName); + return findRole(specialName, caseSensitive, specialNameToAttributeRoleMap); + } + + /** + * Finds the {@link AttributeRole} with the given key. The key will either be an attribute name or attribute role name + * (both regular and special). The key will be searched in the specified {@code roleMap} which should either be + * {@link #nameToAttributeRoleMap} or {@link #specialNameToAttributeRoleMap}. Whether the search is performed case + * sensitive depends on the boolean parameter. + *

+ * Attention: Case insensitive search is not optimized and takes linear time with number of attributes. + * + * @param key + * the key to search for + * @param caseSensitive + * whether the search should be case sensitive + * @param roleMap + * the map to search in + * @return the attribute role or {@code null} if none was found + * @since 8.2 + */ + private static AttributeRole findRole(String key, boolean caseSensitive, Map roleMap) { + if (caseSensitive || key == null) { + return roleMap.get(key); } else { - String lowerSearchTerm = specialName.toLowerCase(); - for (Entry entry : specialNameToAttributeRoleMap.entrySet()) { - if (lowerSearchTerm.equals(entry.getKey().toLowerCase())) { + for (Entry entry : roleMap.entrySet()) { + if (key.equalsIgnoreCase(entry.getKey())) { return entry.getValue(); } } diff --git a/src/main/java/com/rapidminer/example/set/CustomFilter.java b/src/main/java/com/rapidminer/example/set/CustomFilter.java index f20fd3521..2599da9b7 100644 --- a/src/main/java/com/rapidminer/example/set/CustomFilter.java +++ b/src/main/java/com/rapidminer/example/set/CustomFilter.java @@ -33,6 +33,7 @@ import com.rapidminer.example.Example; import com.rapidminer.example.ExampleSet; import com.rapidminer.operator.nio.model.DataResultSet.ValueType; +import com.rapidminer.parameter.ParameterTypeDateFormat; import com.rapidminer.parameter.ParameterTypeTupel; import com.rapidminer.tools.I18N; import com.rapidminer.tools.Ontology; @@ -377,7 +378,7 @@ public boolean isSpecialConditionFulfilled(final double input) { public static final String DATE_TIME_FORMAT_STRING_OLD = "MM/dd/yy h:mm:ss a"; /** the format string for date */ - public static final String DATE_FORMAT_STRING = "MM/dd/yyyy"; + public static final String DATE_FORMAT_STRING = ParameterTypeDateFormat.DATE_FORMAT_MM_DD_YYYY; /** the old (bugged) format string for date */ public static final String DATE_FORMAT_STRING_OLD = "MM/dd/yy"; diff --git a/src/main/java/com/rapidminer/example/set/SimilarityExampleSet.java b/src/main/java/com/rapidminer/example/set/SimilarityExampleSet.java index f6c6342dd..fd56d364c 100644 --- a/src/main/java/com/rapidminer/example/set/SimilarityExampleSet.java +++ b/src/main/java/com/rapidminer/example/set/SimilarityExampleSet.java @@ -28,10 +28,15 @@ import com.rapidminer.example.table.ExampleTable; import com.rapidminer.example.table.NominalMapping; import com.rapidminer.operator.Annotations; +import com.rapidminer.operator.UserError; +import com.rapidminer.operator.WrapperOperatorRuntimeException; import com.rapidminer.operator.similarity.ExampleSet2SimilarityExampleSet; import com.rapidminer.tools.Ontology; import com.rapidminer.tools.math.similarity.DistanceMeasure; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.util.Iterator; @@ -43,6 +48,28 @@ */ public class SimilarityExampleSet extends AbstractExampleSet { + /** + * {@link InvocationHandler} for {@link ExampleTable} that can take care of + * {@link com.rapidminer.operator.execution.FlowCleaner} calls. Will throw a {@link UserError} + * for all other calls. + * + * @author Jan Czogalla + * @since 8.2 + */ + private static class SimilarityHandler implements InvocationHandler { + + private static final String ATTRIBUTE_COUNT_METHOD_NAME = "getAttributeCount"; + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().equals(ATTRIBUTE_COUNT_METHOD_NAME)) { + return 0; + } + throw new WrapperOperatorRuntimeException(new UserError(null, "similarity_example_set_not_extendable")); + } + + } + private static final long serialVersionUID = 4757975818441794105L; private static class IndexExampleReader extends AbstractExampleReader { @@ -178,7 +205,9 @@ public Iterator iterator() { @Override public ExampleTable getExampleTable() { - return null;// this.parent.getExampleTable(); + // return a proxy object so the flow cleaner is happy + return (ExampleTable) Proxy.newProxyInstance(this.getClass().getClassLoader(), + new Class[]{ExampleTable.class}, new SimilarityHandler()); } @Override diff --git a/src/main/java/com/rapidminer/gui/DockableMenu.java b/src/main/java/com/rapidminer/gui/DockableMenu.java index ff582a159..788ba4870 100644 --- a/src/main/java/com/rapidminer/gui/DockableMenu.java +++ b/src/main/java/com/rapidminer/gui/DockableMenu.java @@ -1,170 +1,260 @@ -/** - * Copyright (C) 2001-2018 by RapidMiner and the contributors - * - * Complete list of developers available at our web site: - * - * http://rapidminer.com - * - * This program is free software: you can redistribute it and/or modify it under the terms of the - * GNU Affero 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 - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License along with this program. - * If not, see http://www.gnu.org/licenses/. -*/ -package com.rapidminer.gui; - -import java.util.Arrays; -import java.util.Comparator; -import java.util.LinkedList; -import java.util.List; -import javax.swing.JCheckBoxMenuItem; -import javax.swing.event.MenuEvent; -import javax.swing.event.MenuListener; - -import com.rapidminer.gui.processeditor.ProcessLogTab; -import com.rapidminer.gui.processeditor.results.ResultDisplay; -import com.rapidminer.gui.processeditor.results.ResultTab; -import com.rapidminer.gui.tools.ResourceDockKey; -import com.rapidminer.gui.tools.ResourceMenu; -import com.rapidminer.tools.SystemInfoUtilities; -import com.rapidminer.tools.SystemInfoUtilities.OperatingSystem; -import com.vlsolutions.swing.docking.DockKey; -import com.vlsolutions.swing.docking.DockableState; -import com.vlsolutions.swing.docking.DockingContext; -import com.vlsolutions.swing.docking.DummyDockable; - - -/** - * - * @author Simon Fischer - */ -public class DockableMenu extends ResourceMenu { - - private static final long serialVersionUID = -5602297374075268751L; - - private static final List HIDE_IN_DOCKABLE_MENU_PREFIX_REGISTRY = new LinkedList<>(); - - private static final String DOCKABLE_HTML = "

%s
%s
"; - - /** - * Here you can register prefixes that will be used to test if a {@link DockableState} start - * with the provided prefix and thus won't be shown in the created {@link DockableMenu}. - * - * @param prefix - * the prefix of {@link DockableState} {@link DockKey}s to hide - */ - public static void registerHideInDockableMenuPrefix(String prefix) { - HIDE_IN_DOCKABLE_MENU_PREFIX_REGISTRY.add(prefix); - } - - static { - registerHideInDockableMenuPrefix(ResultTab.DOCKKEY_PREFIX); - registerHideInDockableMenuPrefix(ProcessLogTab.DOCKKEY_PREFIX); - } - - // private DockingDesktop dockingDesktop; - private final DockingContext dockingContext; - - public DockableMenu(DockingContext dockingContext) { - super("show_view"); - this.dockingContext = dockingContext; - addMenuListener(new MenuListener() { - - @Override - public void menuCanceled(MenuEvent e) {} - - @Override - public void menuDeselected(MenuEvent e) {} - - @Override - public void menuSelected(MenuEvent e) { - fill(); - } - }); - } - - private void fill() { - removeAll(); - DockableState[] dockables = dockingContext.getDesktopList().get(0).getDockables(); - List sorted = new LinkedList<>(); - sorted.addAll(Arrays.asList(dockables)); - sorted.sort(Comparator.comparing(o -> o.getDockable().getDockKey().getName())); - for (final DockableState state : sorted) { - if (state.getDockable() instanceof DummyDockable) { - continue; - } - DockKey dockKey = state.getDockable().getDockKey(); - boolean cont = false; - for (String prefix : HIDE_IN_DOCKABLE_MENU_PREFIX_REGISTRY) { - if (dockKey.getKey().startsWith(prefix)) { - cont = true; - break; - } - } - if (cont) { - continue; - } - String description = null; - if (dockKey instanceof ResourceDockKey) { - description = ((ResourceDockKey) dockKey).getShortDescription(); - } - description = description != null ? description : ""; - String text = dockKey.getName(); - if (SystemInfoUtilities.getOperatingSystem() != OperatingSystem.OSX) { - // OS X cannot use html in menus so only do it for other OS - text = String.format(DOCKABLE_HTML, dockKey.getName(), description); - } - JCheckBoxMenuItem item = new JCheckBoxMenuItem(text, dockKey.getIcon()); - - item.setSelected(!state.isClosed()); - item.addActionListener(e -> { - - if (state.isClosed()) { - dockingContext.getDesktopList().get(0).addDockable(state.getDockable()); - } else { - dockingContext.getDesktopList().get(0).close(state.getDockable()); - } - }); - - // special handling for results overview dockable in Results perspective - // this dockable is not allowed to be closed so we disable this item while in said - // perspective - if (RapidMinerGUI.getMainFrame().getPerspectiveController().getModel().getSelectedPerspective().getName() - .equals(PerspectiveModel.RESULT) - && ResultDisplay.RESULT_DOCK_KEY.equals(state.getDockable().getDockKey().getKey())) { - item.setEnabled(false); - } - - add(item); - } - - } - - public DockingContext getDockingContext() { - return dockingContext; - } - - - /** - * Checks if the given dock key belongs to a dockable that should not appear in the dockable menu. - * - * @param dockKey - * the key to test - * @return {@code true} if the key belongs to a dockable that is hidden from the menu; {@code false} otherwise - * @since 8.1 - */ - public static boolean isDockableHiddenFromMenu(final DockKey dockKey) { - for (String prefix : HIDE_IN_DOCKABLE_MENU_PREFIX_REGISTRY) { - if (dockKey.getKey().startsWith(prefix)) { - return true; - } - } - - return false; - } -} +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.gui; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Level; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenu; +import javax.swing.JPopupMenu; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; + +import com.rapidminer.gui.processeditor.ProcessLogTab; +import com.rapidminer.gui.processeditor.results.ResultDisplay; +import com.rapidminer.gui.processeditor.results.ResultTab; +import com.rapidminer.gui.tools.ResourceDockKey; +import com.rapidminer.gui.tools.ResourceMenu; +import com.rapidminer.gui.tools.ScrollableJPopupMenu; +import com.rapidminer.tools.LogService; +import com.rapidminer.tools.SystemInfoUtilities; +import com.rapidminer.tools.SystemInfoUtilities.OperatingSystem; +import com.vlsolutions.swing.docking.DockKey; +import com.vlsolutions.swing.docking.DockableState; +import com.vlsolutions.swing.docking.DockingContext; +import com.vlsolutions.swing.docking.DockingDesktop; +import com.vlsolutions.swing.docking.DummyDockable; + + +/** + * This menu is specialized to show all {@link com.vlsolutions.swing.docking.Dockable Dockables}, better known as Panels, + * except some which are registered to be hidden via {@link DockableMenu#registerHideInDockableMenuPrefix(String)}. The + * {@link javax.swing.JPopupMenu} which contains the entries is scrollable. + * + * @author Simon Fischer, Andreas Timm + */ +public class DockableMenu extends ResourceMenu { + + private static final long serialVersionUID = -5602297374075268751L; + + private static final List HIDE_IN_DOCKABLE_MENU_PREFIX_REGISTRY = new LinkedList<>(); + + private static final String DOCKABLE_HTML = "
%s
%s
"; + + /** + * This definition of maximum visible entries will be used to calculate the height of the popup + */ + private static final int MAX_SHOWN_ITEMS = 18; + + /** + * Here you can register prefixes that will be used to test if a {@link DockableState} start + * with the provided prefix and thus won't be shown in the created {@link DockableMenu}. + * + * @param prefix + * the prefix of {@link DockableState} {@link DockKey}s to hide + */ + public static void registerHideInDockableMenuPrefix(String prefix) { + HIDE_IN_DOCKABLE_MENU_PREFIX_REGISTRY.add(prefix); + } + + static { + registerHideInDockableMenuPrefix(ResultTab.DOCKKEY_PREFIX); + registerHideInDockableMenuPrefix(ProcessLogTab.DOCKKEY_PREFIX); + } + + private final DockingContext dockingContext; + + public DockableMenu(DockingContext dockingContext) { + super("show_view"); + this.dockingContext = dockingContext; + ensurePopupMenuCreated(); + configureScrollable(); + } + + /** + * Tries to set the popup menu to an instance of {@link ScrollableJPopupMenu} using reflection. If that fails, + * the default {@link JPopupMenu} will be used. + *

+ * This is inspired by {@link JMenu#ensurePopupMenuCreated()} which sadly is private. + * + * @since 8.2 + */ + private void ensurePopupMenuCreated() { + // OSX special handling + if (SystemInfoUtilities.getOperatingSystem() == OperatingSystem.OSX) { + // get popup menu once to trigger creation in parent + getPopupMenu(); + return; + } + Field popupMenuField; + ScrollableJPopupMenu popupMenu = new ScrollableJPopupMenu(ScrollableJPopupMenu.SIZE_HUGE); + popupMenu.setInvoker(this); + WinListener popupListener = new WinListener(popupMenu); + // set listener first; if something goes wrong later, the private field will jsut be replaced in super method + try { + Field popupListenerField = JMenu.class.getDeclaredField("popupListener"); + popupListenerField.setAccessible(true); + popupListenerField.set(this, popupListener); + } catch (NoSuchFieldException | IllegalAccessException e) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.DockableMenu.listener_error_make_popup_scrollable"); + return; + } + // set popup second; if the set operation does not succeed, the private field will stay at null, + // since this is invoked from the constructor before any call to the super method could be made. + try { + popupMenuField = JMenu.class.getDeclaredField("popupMenu"); + popupMenuField.setAccessible(true); + popupMenuField.set(this, popupMenu); + } catch (NoSuchFieldException | IllegalAccessException e) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.DockableMenu.popup_setting_error"); + } + } + + /** + * Configures the {@link JScrollPane} of the {@link ScrollableJPopupMenu} if set, + * namely forbids the usage of the horizontal scrollbar. + */ + private void configureScrollable() { + JPopupMenu popupMenu = getPopupMenu(); + if (!(popupMenu instanceof ScrollableJPopupMenu)) { + return; + } + JScrollPane scrollPane = ((ScrollableJPopupMenu) popupMenu).getScrollPane(); + scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + + void fill() { + removeAll(); + + DockableState[] dockables = dockingContext.getDesktopList().get(0).getDockables(); + List sorted = new LinkedList<>(); + sorted.addAll(Arrays.asList(dockables)); + sorted.sort(Comparator.comparing(o -> o.getDockable().getDockKey().getName())); + for (final DockableState state : sorted) { + if (state.getDockable() instanceof DummyDockable) { + continue; + } + DockKey dockKey = state.getDockable().getDockKey(); + boolean cont = false; + for (String prefix : HIDE_IN_DOCKABLE_MENU_PREFIX_REGISTRY) { + if (dockKey.getKey().startsWith(prefix)) { + cont = true; + break; + } + } + if (cont) { + continue; + } + String description = null; + if (dockKey instanceof ResourceDockKey) { + description = ((ResourceDockKey) dockKey).getShortDescription(); + } + description = description != null ? description : ""; + String text = dockKey.getName(); + if (SystemInfoUtilities.getOperatingSystem() != OperatingSystem.OSX) { + // OS X cannot use html in menus so only do it for other OS + text = String.format(DOCKABLE_HTML, dockKey.getName(), description); + } + JCheckBoxMenuItem item = new JCheckBoxMenuItem(text, dockKey.getIcon()); + item.setSelected(!state.isClosed()); + item.addActionListener(e -> toggleState(state)); + + // special handling for results overview dockable in Results perspective + // this dockable is not allowed to be closed so we disable this item while in said + // perspective + if (RapidMinerGUI.getMainFrame().getPerspectiveController().getModel().getSelectedPerspective().getName() + .equals(PerspectiveModel.RESULT) + && ResultDisplay.RESULT_DOCK_KEY.equals(state.getDockable().getDockKey().getKey())) { + item.setEnabled(false); + } + add(item); + ensurePopupHeight(); + } + } + + /** + * Ensures that the correct maximum height is set for the popup menu and sets the scroll increment. + * Will only take effect with the first item added. Makes sure the height is set to {@link #MAX_SHOWN_ITEMS}*itemHeight. + * + * @since 8.2 + */ + private void ensurePopupHeight() { + JPopupMenu popupMenu = getPopupMenu(); + if (popupMenu.getSubElements().length != 1 || !(popupMenu instanceof ScrollableJPopupMenu)) { + return; + } + ScrollableJPopupMenu scrollablePopup = (ScrollableJPopupMenu) popupMenu; + int itemHeight = scrollablePopup.getComponentsInsideScrollpane()[0].getPreferredSize().height; + int maxHeight = MAX_SHOWN_ITEMS * itemHeight; + maxHeight = Math.min(maxHeight, ScrollableJPopupMenu.SIZE_HUGE); + scrollablePopup.setMaxHeight(maxHeight); + JScrollPane scrollPane = scrollablePopup.getScrollPane(); + JScrollBar verticalScrollBar = scrollPane.getVerticalScrollBar(); + verticalScrollBar.setUnitIncrement(itemHeight); + verticalScrollBar.setBlockIncrement(maxHeight); + } + + /** + * Toggle the state of this {@link DockableState} component + * + * @param state + * component to toggle the state of + * @since 8.2 + */ + private void toggleState(DockableState state) { + if (state == null) { + return; + } + final DockingDesktop dockingDesktop = dockingContext.getDesktopList().get(0); + if (state.isClosed()) { + dockingDesktop.addDockable(state.getDockable()); + } else { + dockingDesktop.close(state.getDockable()); + } + } + + + public DockingContext getDockingContext() { + return dockingContext; + } + + /** + * Checks if the given dock key belongs to a dockable that should not appear in the dockable menu. + * + * @param dockKey + * the key to test + * @return {@code true} if the key belongs to a dockable that is hidden from the menu; {@code false} otherwise + * @since 8.1 + */ + public static boolean isDockableHiddenFromMenu(final DockKey dockKey) { + for (String prefix : HIDE_IN_DOCKABLE_MENU_PREFIX_REGISTRY) { + if (dockKey.getKey().startsWith(prefix)) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/com/rapidminer/gui/MainFrame.java b/src/main/java/com/rapidminer/gui/MainFrame.java index dd42699cf..f355b3349 100644 --- a/src/main/java/com/rapidminer/gui/MainFrame.java +++ b/src/main/java/com/rapidminer/gui/MainFrame.java @@ -42,6 +42,8 @@ import javax.swing.JToolBar; import javax.swing.SwingUtilities; import javax.swing.Timer; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; import com.rapidminer.BreakpointListener; import com.rapidminer.Process; @@ -120,6 +122,7 @@ import com.rapidminer.operator.IOContainer; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorChain; +import com.rapidminer.operator.ProcessSetupError; import com.rapidminer.operator.UnknownParameterInformation; import com.rapidminer.operator.ports.Port; import com.rapidminer.parameter.ParameterType; @@ -772,6 +775,22 @@ public void quit() { viewMenu.add(new PerspectiveMenu(perspectiveController)); viewMenu.add(NEW_PERSPECTIVE_ACTION); viewMenu.add(dockableMenu = new DockableMenu(dockingContext)); + viewMenu.addMenuListener(new MenuListener() { + @Override + public void menuSelected(MenuEvent e) { + dockableMenu.fill(); + } + + @Override + public void menuDeselected(MenuEvent e) { + // ignore + } + + @Override + public void menuCanceled(MenuEvent e) { + // ignore + } + }); viewMenu.add(RESTORE_PERSPECTIVE_ACTION); @@ -1351,21 +1370,16 @@ public void exit(final boolean relaunch) { public void updateRecentFileList() { recentFilesMenu.removeAll(); List recentFiles = RapidMinerGUI.getRecentFiles(); - int j = 1; for (final ProcessLocation recentLocation : recentFiles) { - JMenuItem menuItem = new JMenuItem(j + " " + recentLocation.toMenuString()); - menuItem.setMnemonic('0' + j); - menuItem.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(final ActionEvent e) { - if (RapidMinerGUI.getMainFrame().close()) { - com.rapidminer.gui.actions.OpenAction.open(recentLocation, true); - } + // whitespaces to create a gap between icon and text as #setIconTextGap(int) sets a gap to both sides of the icon... + JMenuItem menuItem = new JMenuItem(" " + recentLocation.toMenuString()); + menuItem.setIcon(SwingTools.createIcon("16/" + recentLocation.getIconName())); + menuItem.addActionListener(e -> { + if (RapidMinerGUI.getMainFrame().close()) { + com.rapidminer.gui.actions.OpenAction.open(recentLocation, true); } }); recentFilesMenu.add(menuItem); - j++; } } @@ -1720,7 +1734,7 @@ private boolean doesProcessContainShowstoppers() { // if any port needs data but is not connected. As it cannot predict execution behavior // (e.g. Branch operators), this may turn up problems which would not occur during // process execution - Port missingInputPort = ProcessTools.getPortWithoutMandatoryConnection(process); + Pair missingInputPort = ProcessTools.getPortWithoutMandatoryConnection(process); if (missingInputPort != null) { // if there is already one of these, kill if (missingInputBubble != null) { diff --git a/src/main/java/com/rapidminer/gui/OperatorDocumentationBrowser.java b/src/main/java/com/rapidminer/gui/OperatorDocumentationBrowser.java index ee092b23e..bfb613b0c 100644 --- a/src/main/java/com/rapidminer/gui/OperatorDocumentationBrowser.java +++ b/src/main/java/com/rapidminer/gui/OperatorDocumentationBrowser.java @@ -22,6 +22,8 @@ import java.awt.Component; import java.awt.Desktop; import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; import java.io.IOException; import java.io.StringWriter; import java.net.URI; @@ -30,9 +32,11 @@ import java.util.Collection; import java.util.List; import java.util.logging.Level; - +import javax.swing.BorderFactory; import javax.swing.JEditorPane; +import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JScrollPane; import javax.swing.SwingUtilities; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkEvent.EventType; @@ -59,6 +63,7 @@ import com.rapidminer.gui.tools.ResourceDockKey; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.UpdateQueue; +import com.rapidminer.gui.tools.components.FeedbackForm; import com.rapidminer.gui.tools.dialogs.ConfirmDialog; import com.rapidminer.io.process.XMLTools; import com.rapidminer.operator.Operator; @@ -82,32 +87,56 @@ */ public class OperatorDocumentationBrowser extends JPanel implements Dockable, ProcessEditor { - final JEditorPane editor = new JEditorPane("text/html", "-"); + private static final long serialVersionUID = 1L; - private ExtendedJScrollPane scrollPane = new ExtendedJScrollPane(); + private static final String DOCUMENTATION_ROOT = "core/"; + private static final Dimension MINIMUM_DOCUMENTATION_SIZE = new Dimension(100, 100); + private static final String FEEDBACK_KEY_DOCUMENTATION = "operator_documentation"; - public static final String DOCUMENTATION_ROOT = "core/"; + public static final String OPERATOR_HELP_DOCK_KEY = "operator_help"; - public Operator displayedOperator = null; - public URL currentResourceURL = null; + private JEditorPane editor; - private boolean ignoreSelections = false; + private Operator displayedOperator = null; - public static final String OPERATOR_HELP_DOCK_KEY = "operator_help"; + private URL currentResourceURL = null; - private final DockKey DOCK_KEY = new ResourceDockKey(OPERATOR_HELP_DOCK_KEY); + private boolean ignoreSelections = false; - private static final long serialVersionUID = 1L; + private final DockKey DOCK_KEY = new ResourceDockKey(OPERATOR_HELP_DOCK_KEY); private UpdateQueue documentationUpdateQueue = new UpdateQueue("documentation_update_queue"); + private GridBagConstraints contentGbc; + private JPanel contentPanel; + private FeedbackForm feedbackForm; + + /** * Prepares the dockable and its elements. */ public OperatorDocumentationBrowser() { setLayout(new BorderLayout()); + contentGbc = new GridBagConstraints(); + + contentPanel = new JPanel(new GridBagLayout()) { + + @Override + public Dimension getPreferredSize() { + return new Dimension(getParent().getWidth(), super.getPreferredSize().height); + } + }; + + editor = new JEditorPane("text/html", "-") { + + @Override + public Dimension getPreferredSize() { + return new Dimension(getParent().getWidth(), super.getPreferredSize().height); + } + }; + // Instantiate Editor and set Settings editor.addHyperlinkListener(new OperatorHelpLinkListener()); editor.setEditable(false); @@ -118,14 +147,31 @@ public OperatorDocumentationBrowser() { editor.setContentType("text/html"); // add editor to scrollPane - scrollPane = new ExtendedJScrollPane(editor); - - scrollPane.setMinimumSize(new Dimension(100, 100)); - scrollPane.setPreferredSize(new Dimension(100, 100)); + JScrollPane scrollPane = new ExtendedJScrollPane(contentPanel); + scrollPane.setBorder(null); + scrollPane.setMinimumSize(MINIMUM_DOCUMENTATION_SIZE); + scrollPane.setPreferredSize(MINIMUM_DOCUMENTATION_SIZE); + + contentGbc.gridx = 0; + contentGbc.gridy = 0; + contentGbc.weightx = 1.0f; + contentGbc.fill = GridBagConstraints.HORIZONTAL; + contentPanel.add(editor, contentGbc); + + // add filler at bottom + contentGbc.gridy += 1; + contentGbc.weighty = 1.0f; + contentGbc.fill = GridBagConstraints.BOTH; + contentPanel.add(new JLabel(), contentGbc); + + // prepare contentGbc for feedback form + contentGbc.gridy += 1; + contentGbc.weighty = 0.0f; + contentGbc.fill = GridBagConstraints.HORIZONTAL; // add scrollPane to Dockable - scrollPane.setBorder(null); this.add(scrollPane, BorderLayout.CENTER); + this.setVisible(true); this.validate(); @@ -285,14 +331,11 @@ public void hyperlinkUpdate(HyperlinkEvent e) { desktop.browse(uri); } catch (URISyntaxException e1) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.desktop.browse.malformed_url", e1); - return; } catch (IOException e1) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.desktop.browse.open_browser", e1); - return; } } else { LogService.getRoot().log(Level.WARNING, "com.rapidminer.tools.desktop.browse.not_supported"); - return; } } } @@ -302,26 +345,24 @@ public void hyperlinkUpdate(HyperlinkEvent e) { /** * Refreshes the documentation text. * - * @param resourceURL - * url to the xml resource + * @param operator + * the operator for which to load the documentation */ private void changeDocumentation(final Operator operator) { - documentationUpdateQueue.execute(new Runnable() { - - @Override - public void run() { + documentationUpdateQueue.execute(() -> { - final String finalHtml = OperatorDocLoader.getDocumentation(operator); - SwingUtilities.invokeLater(new Runnable() { + final String finalHtml = OperatorDocLoader.getDocumentation(operator); + SwingUtilities.invokeLater(() -> { + editor.setText("" + finalHtml + ""); + editor.setCaretPosition(0); - @Override - public void run() { - editor.setText("" + finalHtml + ""); - editor.setCaretPosition(0); - } - }); - - } + if (feedbackForm != null) { + contentPanel.remove(feedbackForm); + } + feedbackForm = new FeedbackForm(FEEDBACK_KEY_DOCUMENTATION, operator.getOperatorDescription().getKey()); + feedbackForm.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Colors.TAB_BORDER)); + contentPanel.add(feedbackForm, contentGbc); + }); }); } @@ -362,11 +403,11 @@ private StyleSheet createStyleSheet(StyleSheet css) { * Sets the operator for which the operator documentation is shown. * * @param operator + * the operator for which to show the documentation */ public void setDisplayedOperator(Operator operator) { - if (operator != null && !operator.getOperatorDescription().isDeprecated() - && (this.displayedOperator == null || this.displayedOperator != null && !operator.getOperatorDescription() - .getName().equals(this.displayedOperator.getOperatorDescription().getName()))) { + if (operator != null && !operator.getOperatorDescription().isDeprecated() && (this.displayedOperator == null || !operator.getOperatorDescription() + .getName().equals(this.displayedOperator.getOperatorDescription().getName()))) { assignDocumentation(operator); } } @@ -397,8 +438,7 @@ public static URL getDocResourcePath(Operator op) { String key = op.getOperatorDescription().getKeyWithoutPrefix(); String opDescXMLResourcePath = documentationRoot + groupPath + key + ".xml"; - URL resourceURL = Plugin.getMajorClassLoader().getResource(opDescXMLResourcePath); - return resourceURL; + return Plugin.getMajorClassLoader().getResource(opDescXMLResourcePath); } } diff --git a/src/main/java/com/rapidminer/gui/RapidMinerGUI.java b/src/main/java/com/rapidminer/gui/RapidMinerGUI.java index 8b287d387..66200da29 100644 --- a/src/main/java/com/rapidminer/gui/RapidMinerGUI.java +++ b/src/main/java/com/rapidminer/gui/RapidMinerGUI.java @@ -67,6 +67,7 @@ import com.rapidminer.gui.look.RapidLookAndFeel; import com.rapidminer.gui.look.fc.BookmarkIO; import com.rapidminer.gui.look.ui.RapidDockingUISettings; +import com.rapidminer.gui.osx.OSXAdapter; import com.rapidminer.gui.plotter.PlotterPanel; import com.rapidminer.gui.processeditor.search.OperatorGlobalSearch; import com.rapidminer.gui.processeditor.search.OperatorGlobalSearchGUIProvider; @@ -105,6 +106,7 @@ import com.rapidminer.tools.LaunchListener.RemoteControlHandler; import com.rapidminer.tools.LogService; import com.rapidminer.tools.ParameterService; +import com.rapidminer.tools.PlatformUtilities; import com.rapidminer.tools.SystemInfoUtilities; import com.rapidminer.tools.SystemInfoUtilities.OperatingSystem; import com.rapidminer.tools.Tools; @@ -267,7 +269,7 @@ public class RapidMinerGUI extends RapidMiner { // ---------------------------------------------------------- } - private static final int NUMBER_OF_RECENT_FILES = 8; + private static final int NUMBER_OF_RECENT_FILES = 10; private static MainFrame mainFrame; @@ -321,6 +323,8 @@ public synchronized void run(final String openLocation) throws Exception { DockingUISettings.setInstance(new RapidDockingUISettings()); DockableContainerFactory.setFactory(new RapidDockableContainerFactory()); + LogService.getRoot().log(Level.INFO, "com.rapidminer.gui.RapidMinerGUI.start_version", new Object[] {RapidMiner.getLongVersion(), PlatformUtilities.getReleasePlatform()} ); + // inform listeners that splash is about to be shown for (GUIStartupListener sl : startupListener) { try { @@ -407,6 +411,8 @@ public void run() { RapidMiner.splashMessage("plugin_gui"); Plugin.initPluginGuis(mainFrame); + // reload the sample repository to include plugin resources + RepositoryManager.refreshSampleRepository(); // only load the perspective here after all plugins have registered their dockables // otherwise you may end up breaking the perspective and VLDocking exists in invalid states @@ -518,7 +524,7 @@ private void setupToolTipManager() { // setup tool tip text manager ToolTipManager manager = ToolTipManager.sharedInstance(); manager.setDismissDelay(25000); // original: 4000 - manager.setInitialDelay(1500); // original: 750 + manager.setInitialDelay(1125); // original: 750 manager.setReshowDelay(50); // original: 500 } @@ -751,7 +757,9 @@ private static void loadGUIProperties(final MainFrame mainFrame) { mainFrame.getPropertyPanel().setShowParameterHelp(true); } else { mainFrame.getPropertyPanel().setShowParameterHelp(Boolean.valueOf(showHelpProperty).booleanValue()); - + } + if (SystemInfoUtilities.getOperatingSystem() == OperatingSystem.OSX) { + OSXAdapter.checkForFullScreen(mainFrame); } } catch (NumberFormatException e) { setDefaultGUIProperties(); diff --git a/src/main/java/com/rapidminer/gui/actions/Actions.java b/src/main/java/com/rapidminer/gui/actions/Actions.java index 05369e2cc..509fbbbfa 100644 --- a/src/main/java/com/rapidminer/gui/actions/Actions.java +++ b/src/main/java/com/rapidminer/gui/actions/Actions.java @@ -36,6 +36,8 @@ import com.rapidminer.gui.actions.OperatorActionFactory.ResourceEntry; import com.rapidminer.gui.dnd.OperatorTransferHandler; import com.rapidminer.gui.flow.AutoWireThread; +import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel; +import com.rapidminer.gui.flow.processrendering.view.ProcessRendererView; import com.rapidminer.gui.operatormenu.OperatorMenu; import com.rapidminer.gui.operatortree.actions.ActionUtil; import com.rapidminer.gui.operatortree.actions.DeleteOperatorAction; @@ -92,7 +94,7 @@ public void loggedActionPerformed(ActionEvent e) { public final ToggleBreakpointItem TOGGLE_BREAKPOINT[] = { new ToggleBreakpointItem(this, BreakpointListener.BREAKPOINT_BEFORE), - new ToggleBreakpointItem(this, BreakpointListener.BREAKPOINT_AFTER) }; + new ToggleBreakpointItem(this, BreakpointListener.BREAKPOINT_AFTER)}; public final ToggleAllBreakpointsItem TOGGLE_ALL_BREAKPOINTS = new ToggleAllBreakpointsItem(); @@ -189,37 +191,32 @@ public void addToOperatorPopupMenu(JPopupMenu menu, Action renameAction, Action. final Operator op = getFirstSelectedOperator(); final boolean singleSelection = getSelectedOperators().size() == 1; - if (op != null && !singleSelection) { - if (!(op instanceof ProcessRootOperator) && op.getParent() != null) { + OperatorChain displayedChain = mainFrame.getProcessPanel().getProcessRenderer().getModel().getDisplayedChain(); + if (op != null) { + if (!singleSelection && !(op instanceof ProcessRootOperator) && op.getParent() != null) { // enable / disable operator menu.add(TOGGLE_ACTIVATION_ITEM.createMultipleActivationItem()); - } - } - - if (op != null && singleSelection) { - if (mainFrame.getProcessPanel().getProcessRenderer().getModel().getDisplayedChain() != op) { - menu.add(INFO_OPERATOR_ACTION); - menu.add(TOGGLE_ACTIVATION_ITEM.createMenuItem()); - if (renameAction != null) { - menu.add(renameAction); - } else { - menu.add(RENAME_OPERATOR_ACTION); - } - if (!op.getErrorList().isEmpty()) { - menu.add(SHOW_PROBLEM_ACTION); - } - menu.addSeparator(); - if (op instanceof OperatorChain && ((OperatorChain) op).getAllInnerOperators().size() > 0) { - menu.add(OperatorMenu.REPLACE_OPERATORCHAIN_MENU); - } else { - menu.add(OperatorMenu.REPLACE_OPERATOR_MENU); + } else { + if (singleSelection && displayedChain != op) { + menu.add(INFO_OPERATOR_ACTION); + menu.add(TOGGLE_ACTIVATION_ITEM.createMenuItem()); + menu.add(renameAction != null ? renameAction : RENAME_OPERATOR_ACTION); + if (!op.getErrorList().isEmpty()) { + menu.add(SHOW_PROBLEM_ACTION); + } + menu.addSeparator(); + if (op instanceof OperatorChain && ((OperatorChain) op).getAllInnerOperators().size() > 0) { + menu.add(OperatorMenu.REPLACE_OPERATORCHAIN_MENU); + } else { + menu.add(OperatorMenu.REPLACE_OPERATOR_MENU); + } } } - } - // add new operator and building block menu - if (mainFrame.getProcessPanel().getProcessRenderer().getModel().getDisplayedChain() == op) { - menu.add(OperatorMenu.NEW_OPERATOR_MENU); + // add new operator and building block menu + if (displayedChain == op) { + menu.add(OperatorMenu.NEW_OPERATOR_MENU); + } } // populate menu with registered operator actions (if any) @@ -240,7 +237,7 @@ public void addToOperatorPopupMenu(JPopupMenu menu, Action renameAction, Action. } menu.addSeparator(); - boolean enableCutCopy = mainFrame.getProcessPanel().getProcessRenderer().getModel().getDisplayedChain() != op; + boolean enableCutCopy = displayedChain != op; OperatorTransferHandler.installMenuItems(menu, enableCutCopy); // add further actions here @@ -287,13 +284,7 @@ public List getSelectedOperators() { */ public void enableActions() { synchronized (process) { - SwingTools.invokeLater(new Runnable() { - - @Override - public void run() { - enableActionsNow(); - } - }); + SwingTools.invokeLater(this::enableActionsNow); } updateCheckboxStates(); } @@ -358,27 +349,33 @@ public void insert(List newOperators) { if (selectedNode == null) { SwingTools.showVerySimpleErrorMessage("cannot_insert_operator"); return; - } else if (mainFrame.getProcessPanel().getProcessRenderer().getModel().getDisplayedChain() == selectedNode) { - for (Operator newOperator : newOperators) { - int index = mainFrame.getProcessPanel().getProcessRenderer().getProcessIndexUnder( - mainFrame.getProcessPanel().getProcessRenderer().getModel().getCurrentMousePosition()); - if (index == -1) { - index = 0; - } - ((OperatorChain) selectedNode).getSubprocess(index).addOperator(newOperator); + } + ProcessRendererView processRenderer = mainFrame.getProcessPanel().getProcessRenderer(); + ProcessRendererModel model = processRenderer.getModel(); + ExecutionUnit process; + int i = -1; + if (model.getDisplayedChain() == selectedNode) { + int index = processRenderer.getProcessIndexUnder(model.getCurrentMousePosition()); + if (index == -1) { + index = 0; } + process = ((OperatorChain) selectedNode).getSubprocess(index); } else { - int i = 0; Operator selectedOperator = (Operator) selectedNode; - ExecutionUnit process = selectedOperator.getExecutionUnit(); - int parentIndex = process.getOperators().indexOf(selectedOperator) + 1; - for (Operator newOperator : newOperators) { - process.addOperator(newOperator, parentIndex + i); - i++; - } + process = selectedOperator.getExecutionUnit(); + i = process.getOperators().indexOf(selectedOperator) + 1; } + for (Operator newOperator : newOperators) { + if (i < 0) { + process.addOperator(newOperator); + } else { + process.addOperator(newOperator, i++); + } + } AutoWireThread.autoWireInBackground(newOperators, true); + // call autofit so each operator has a (valid) location + processRenderer.getAutoFitAction().actionPerformed(null); mainFrame.selectOperators(newOperators); } diff --git a/src/main/java/com/rapidminer/gui/actions/SaveAction.java b/src/main/java/com/rapidminer/gui/actions/SaveAction.java index 236ae7640..681b9c280 100644 --- a/src/main/java/com/rapidminer/gui/actions/SaveAction.java +++ b/src/main/java/com/rapidminer/gui/actions/SaveAction.java @@ -67,6 +67,23 @@ public void loggedActionPerformed(ActionEvent e) { * @return true on success, false on failure */ public static boolean save(final Process process) { + return save(process, false); + } + + /** + * Saves the specified process to its {@link ProcessLocation}. If it has none, will open the + * SaveAs dialog.
+ * Note: This call executes in the calling thread. In other words, this would + * block the GUI if called from the EDT! + * + * @param process + * the {@link Process} to save + * @param refreshProcessMetaData + * if {@code true}, the meta data of the process will be recalculated after successful save + * @return true on success, false on failure + * @since 8.2 + */ + public static boolean save(final Process process, final boolean refreshProcessMetaData) { if (process.hasSaveDestination()) { if (confirmOverwriteWithNewVersion(process)) { @@ -81,8 +98,12 @@ public static boolean save(final Process process) { if (process.hasSaveDestination()) { RapidMinerGUI.useProcessFile(process); RapidMinerGUI.getMainFrame().processHasBeenSaved(); - } + // after successful save, if desired, force recalculation of meta data + if (refreshProcessMetaData) { + process.getRootOperator().transformMetaData(); + } + } } catch (IOException e) { successful = false; SwingTools @@ -110,6 +131,22 @@ public static boolean save(final Process process) { * the {@link Process} to save */ public static void saveAsync(final Process process) { + saveAsync(process, false); + } + + /** + * Saves the specified process to its {@link ProcessLocation}. If it has none, will open the + * SaveAs dialog.
+ * Note: This call executes in a {@link ProgressThread} with the key + * {@link #SAVE_PROGRESS_KEY}. In other words, this method can return immediately! + * + * @param process + * the {@link Process} to save + * @param refreshProcessMetaData + * if {@code true}, the meta data of the process will be recalculated after successful save + * @since 8.2 + */ + public static void saveAsync(final Process process, final boolean refreshProcessMetaData) { if (process.hasSaveDestination()) { if (confirmOverwriteWithNewVersion(process)) { // user wants to save @@ -122,18 +159,14 @@ public static void saveAsync(final Process process) { public void run() { try { process.save(); + try { - SwingUtilities.invokeAndWait(new Runnable() { - - @Override - public void run() { - // check if process has really been saved or user has - // pressed - // cancel in saveAs dialog - if (process.hasSaveDestination()) { - RapidMinerGUI.useProcessFile(process); - RapidMinerGUI.getMainFrame().processHasBeenSaved(); - } + SwingUtilities.invokeAndWait(() -> { + + // check if process has really been saved or user has pressed cancel in saveAs dialog + if (process.hasSaveDestination()) { + RapidMinerGUI.useProcessFile(process); + RapidMinerGUI.getMainFrame().processHasBeenSaved(); } }); } catch (InvocationTargetException | InterruptedException e) { @@ -141,6 +174,11 @@ public void run() { // in the called methods), print the stacktrace e.printStackTrace(); } + + // after successful save, if desired, force recalculation of meta data + if (refreshProcessMetaData && process.hasSaveDestination()) { + process.getRootOperator().transformMetaData(); + } } catch (IOException ex) { SwingTools.showSimpleErrorMessage("cannot_save_process", ex, process.getProcessLocation(), ex.getMessage()); diff --git a/src/main/java/com/rapidminer/gui/actions/SaveAsAction.java b/src/main/java/com/rapidminer/gui/actions/SaveAsAction.java index 98f5c8e35..650b1fd8e 100644 --- a/src/main/java/com/rapidminer/gui/actions/SaveAsAction.java +++ b/src/main/java/com/rapidminer/gui/actions/SaveAsAction.java @@ -68,12 +68,12 @@ public static void saveAs(Process process) { /** * Opens a location choser for the user to select where the save the process. - * + * * @param process - * the process to be saved + * the process to be saved * @param async - * if true, will save the process asynchronously after the user has - * selected a location; if false saves it synchronously. + * if true, will save the process asynchronously after the user has + * selected a location; if false saves it synchronously. * @return true on success, false on failure, and null if async=true */ public static Boolean saveAs(Process process, boolean async) { @@ -101,10 +101,10 @@ public static Boolean saveAs(Process process, boolean async) { } if (async) { - SaveAction.saveAsync(process); + SaveAction.saveAsync(process, true); return null; } else { - return SaveAction.save(process); + return SaveAction.save(process, true); } } return false; diff --git a/src/main/java/com/rapidminer/gui/actions/search/ActionsGlobalSearchManager.java b/src/main/java/com/rapidminer/gui/actions/search/ActionsGlobalSearchManager.java index d9b82b2bb..94ee66bd7 100644 --- a/src/main/java/com/rapidminer/gui/actions/search/ActionsGlobalSearchManager.java +++ b/src/main/java/com/rapidminer/gui/actions/search/ActionsGlobalSearchManager.java @@ -332,7 +332,7 @@ private Document createDocument(final ResourceAction action) { private Document createDocumentFromPerspective(final Perspective perspective) { WorkspaceAction action = RapidMinerGUI.getMainFrame().getPerspectiveController().createPerspectiveAction(perspective); // make label a bit more descriptive - String name = perspective.getName().replaceAll("_", " "); + String name = action.getValue(ResourceAction.NAME) != null ? String.valueOf(action.getValue(ResourceAction.NAME)) : perspective.getName().replaceAll("_", " "); action.putValue(ResourceAction.NAME, I18N.getMessage(I18N.getGUIBundle(), "gui.action.global_search.action.perspective.name", SwingTools.capitalizeString(name))); return createDocument(action); } diff --git a/src/main/java/com/rapidminer/gui/dialog/NewOperatorDialog.java b/src/main/java/com/rapidminer/gui/dialog/NewOperatorDialog.java index 603530b2b..d8f1c7c8d 100644 --- a/src/main/java/com/rapidminer/gui/dialog/NewOperatorDialog.java +++ b/src/main/java/com/rapidminer/gui/dialog/NewOperatorDialog.java @@ -66,6 +66,7 @@ import com.rapidminer.tools.GroupTree; import com.rapidminer.tools.I18N; import com.rapidminer.tools.OperatorService; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; /** @@ -495,6 +496,7 @@ private void add() { Operator operator = getOperator(); if (operator != null) { actions.insert(Collections.singletonList(operator)); + ActionStatisticsCollector.INSTANCE.log(ActionStatisticsCollector.TYPE_NEW_OPERATOR_DIALOG, "inserted", operator.getOperatorDescription().getKey()); } } catch (Exception ex) { ex.printStackTrace(); diff --git a/src/main/java/com/rapidminer/gui/dnd/OperatorTransferHandler.java b/src/main/java/com/rapidminer/gui/dnd/OperatorTransferHandler.java index 7c9181165..1464e0691 100644 --- a/src/main/java/com/rapidminer/gui/dnd/OperatorTransferHandler.java +++ b/src/main/java/com/rapidminer/gui/dnd/OperatorTransferHandler.java @@ -101,16 +101,15 @@ public Transferable createTransferable(JComponent c) { return null; } return new TransferableOperator(operators.toArray(new Operator[operators.size()])); - } else { - // no operators, try workflow annotations - WorkflowAnnotation anno = getDraggedAnnotation(); - // neither operators nor annotations, give up - if (anno == null) { - return null; - } - - return new TransferableAnnotation(anno); } + // no operators, try workflow annotations + WorkflowAnnotation anno = getDraggedAnnotation(); + // neither operators nor annotations, give up + if (anno == null) { + return null; + } + + return new TransferableAnnotation(anno); } @Override diff --git a/src/main/java/com/rapidminer/gui/dnd/ReceivingOperatorTransferHandler.java b/src/main/java/com/rapidminer/gui/dnd/ReceivingOperatorTransferHandler.java index d58ee8e26..5d21cbfcb 100644 --- a/src/main/java/com/rapidminer/gui/dnd/ReceivingOperatorTransferHandler.java +++ b/src/main/java/com/rapidminer/gui/dnd/ReceivingOperatorTransferHandler.java @@ -20,13 +20,20 @@ import java.awt.Point; import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; import java.io.File; +import java.io.IOException; import java.io.StringReader; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Objects; +import java.util.function.BooleanSupplier; import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.swing.SwingUtilities; import org.w3c.dom.Document; @@ -45,7 +52,6 @@ import com.rapidminer.io.process.XMLTools; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorCreationException; -import com.rapidminer.operator.UnknownParameterInformation; import com.rapidminer.operator.internal.ProcessEmbeddingOperator; import com.rapidminer.operator.io.RepositorySource; import com.rapidminer.operator.nio.file.LoadFileOperator; @@ -59,6 +65,7 @@ import com.rapidminer.tools.LogService; import com.rapidminer.tools.OperatorService; import com.rapidminer.tools.Tools; +import com.rapidminer.tools.usagestats.UsageLoggable; /** @@ -133,8 +140,9 @@ public boolean importData(TransferSupport ts) { } Object transferData; + Transferable transferable = ts.getTransferable(); try { - transferData = ts.getTransferable().getTransferData(acceptedFlavor); + transferData = transferable.getTransferData(acceptedFlavor); } catch (Exception e1) { LogService.getRoot() .log(Level.WARNING, @@ -154,21 +162,17 @@ public boolean importData(TransferSupport ts) { try { Operator processEmbedder = OperatorService.createOperator(ProcessEmbeddingOperator.OPERATOR_KEY); processEmbedder.setParameter(ProcessEmbeddingOperator.PARAMETER_PROCESS_FILE, file.getAbsolutePath()); - newOperators = Collections. singletonList(processEmbedder); + newOperators = Collections.singletonList(processEmbedder); } catch (Exception e) { SwingTools.showSimpleErrorMessage("cannot_create_process_embedder", e); dropEnds(); return false; } } else { - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - DataImportWizardBuilder importWizardBuilder = new DataImportWizardBuilder(); - importWizardBuilder.forFile(file.toPath()).build(RapidMinerGUI.getMainFrame()).getDialog() - .setVisible(true); - } + SwingUtilities.invokeLater(() -> { + DataImportWizardBuilder importWizardBuilder = new DataImportWizardBuilder(); + importWizardBuilder.forFile(file.toPath()).build(RapidMinerGUI.getMainFrame()).getDialog() + .setVisible(true); }); dropEnds(); return true; @@ -184,82 +188,74 @@ public void run() { return false; } } else if (acceptedFlavor.equals(DataFlavor.stringFlavor)) { - if (transferData instanceof String) { + if (!(transferData instanceof String)) { + LogService.getRoot().log(Level.WARNING, + "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.expected_string", acceptedFlavor); + dropEnds(); + return false; + } + try { + Process process = new Process(((String) transferData).trim()); + newOperators = process.getRootOperator().getSubprocess(0).getOperators(); + } catch (Exception e) { try { - Process process = new Process(((String) transferData).trim()); - newOperators = process.getRootOperator().getSubprocess(0).getOperators(); - } catch (Exception e) { - try { - Document document = XMLTools.createDocumentBuilder() - .parse(new InputSource(new StringReader((String) transferData))); - NodeList opElements = document.getDocumentElement().getChildNodes(); - Operator newOp = null; - for (int i = 0; i < opElements.getLength(); i++) { - Node child = opElements.item(i); - if (child instanceof Element) { - Element elem = (Element) child; - if ("operator".equals(elem.getTagName())) { - newOp = new XMLImporter(null).parseOperator(elem, RapidMiner.getVersion(), getProcess(), - new LinkedList()); - break; - } + Document document = XMLTools.createDocumentBuilder().parse(new InputSource(new StringReader((String) transferData))); + NodeList opElements = document.getDocumentElement().getChildNodes(); + Operator newOp = null; + for (int i = 0; i < opElements.getLength(); i++) { + Node child = opElements.item(i); + if (child instanceof Element) { + Element elem = (Element) child; + if ("operator".equals(elem.getTagName())) { + newOp = new XMLImporter(null).parseOperator(elem, RapidMiner.getVersion(), getProcess(), + new LinkedList<>()); + break; } } - if (newOp == null) { - LogService.getRoot().log(Level.WARNING, - "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.parsing_operator_from_clipboard_error", - transferData); - dropEnds(); - return false; - } - newOperators = Collections.singletonList(newOp); - } catch (SAXParseException e1) { - LogService.getRoot().log(Level.WARNING, - "com.rapidminer.gui.processeditor.XMLEditor.failed_to_parse_process"); - dropEnds(); - return false; - } catch (Exception e1) { + } + if (newOp == null) { LogService.getRoot().log(Level.WARNING, - I18N.getMessage(LogService.getRoot().getResourceBundle(), - "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.parsing_operator_from_clipboard_error_exception", - e1, transferData), - e1); + "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.parsing_operator_from_clipboard_error", + transferData); dropEnds(); return false; } + newOperators = Collections.singletonList(newOp); + } catch (SAXParseException e1) { + LogService.getRoot().log(Level.WARNING, + "com.rapidminer.gui.processeditor.XMLEditor.failed_to_parse_process"); + dropEnds(); + return false; + } catch (Exception e1) { + LogService.getRoot().log(Level.WARNING, + I18N.getMessage(LogService.getRoot().getResourceBundle(), + "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.parsing_operator_from_clipboard_error_exception", + e1, transferData), e1); + dropEnds(); + return false; } - } else { - LogService.getRoot().log(Level.WARNING, - "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.expected_string", acceptedFlavor); - dropEnds(); - return false; } } else if (acceptedFlavor.equals(TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR)) { - if (transferData instanceof RepositoryLocation) { - RepositoryLocation repositoryLocation = (RepositoryLocation) transferData; - newOperators = Collections.singletonList(createOperator(repositoryLocation)); - if (newOperators == null) { - return false; - } - } else { + if (!(transferData instanceof RepositoryLocation)) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.expected_repositorylocation", acceptedFlavor); dropEnds(); return false; } + RepositoryLocation repositoryLocation = (RepositoryLocation) transferData; + Operator newOp = createOperator(repositoryLocation); + if (newOp == null) { + dropEnds(); + return false; + } + newOperators = Collections.singletonList(newOp); } else if (acceptedFlavor.equals(TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_LIST_FLAVOR)) { + Stream repoLocations; if (transferData instanceof RepositoryLocationList) { - RepositoryLocationList repositoryLocationList = (RepositoryLocationList) transferData; - newOperators = new LinkedList<>(); - for (RepositoryLocation loc : repositoryLocationList.getAll()) { - List list = Collections.singletonList(createOperator(loc)); - if (list == null) { - return false; - } else { - newOperators.addAll(list); - } - } + repoLocations = ((RepositoryLocationList) transferData).getAll().stream(); + } else if (transferData instanceof RepositoryLocation[]) { + repoLocations = Arrays.stream((RepositoryLocation[]) transferData); } else { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.expected_repositorylocationlist", @@ -267,6 +263,11 @@ public void run() { dropEnds(); return false; } + newOperators = repoLocations.map(this::createOperator).filter(Objects::nonNull).collect(Collectors.toList()); + if (newOperators.isEmpty()) { + dropEnds(); + return false; + } } else if (acceptedFlavor.equals(TransferableAnnotation.LOCAL_OPERATOR_ANNOTATION_FLAVOR) || acceptedFlavor.equals(TransferableAnnotation.LOCAL_PROCESS_ANNOTATION_FLAVOR)) { newOperators = new LinkedList<>(); @@ -283,73 +284,62 @@ public void run() { if (!dropLocationOk) { dropEnds(); return false; - } else { - if (ts.getDropAction() == MOVE) { - for (Operator operator : newOperators) { - operator.removeAndKeepConnections(newOperators); - } + } + if (ts.getDropAction() == MOVE) { + for (Operator operator : newOperators) { + operator.removeAndKeepConnections(newOperators); } - newOperators = Tools.cloneOperators(newOperators); + } + newOperators = Tools.cloneOperators(newOperators); - boolean result; + boolean result = false; + try { + result = dropNow(newOperators, ts.isDrop() ? loc : null); + } catch (RuntimeException e) { + LogService.getRoot().log(Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), + "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.error_in_drop", e), e); + SwingTools.showVerySimpleErrorMessage("error_in_paste", e.getMessage(), e.getMessage()); + } + dropEnds(); + if (result) { try { - result = dropNow(newOperators, ts.isDrop() ? loc : null); - } catch (RuntimeException e) { - LogService.getRoot().log(Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), - "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.error_in_drop", e), e); - SwingTools.showVerySimpleErrorMessage("error_in_paste", e.getMessage(), e.getMessage()); - dropEnds(); - return false; + // log usage stats if applicable; ignore exceptions on unsupported flavor exception + transferable.getTransferData(UsageLoggable.USAGE_FLAVOR); + } catch (UnsupportedFlavorException | IOException ignored) { + // ignore, no logging implemented } - dropEnds(); - return result; } + return result; } else { // paste + BooleanSupplier dropNow; if (acceptedFlavor.equals(DataFlavor.stringFlavor)) { - // handle XML String pasting differently - boolean result; - try { - result = dropNow(String.valueOf(transferData)); - } catch (RuntimeException e) { - LogService.getRoot().log(Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), - "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.error_in_paste", e), e); - SwingTools.showVerySimpleErrorMessage("error_in_paste", e.getMessage(), e.getMessage()); - dropEnds(); - return false; - } - dropEnds(); - return result; + dropNow = () -> dropNow(String.valueOf(transferData)); } else if (acceptedFlavor.equals(TransferableAnnotation.LOCAL_PROCESS_ANNOTATION_FLAVOR) || acceptedFlavor.equals(TransferableAnnotation.LOCAL_OPERATOR_ANNOTATION_FLAVOR)) { - boolean result; - try { - result = dropNow((WorkflowAnnotation) transferData, null); - } catch (RuntimeException e) { - LogService.getRoot().log(Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), - "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.error_in_paste", e), e); - SwingTools.showVerySimpleErrorMessage("error_in_paste", e.getMessage(), e.getMessage()); - dropEnds(); - return false; - } - dropEnds(); - return result; + dropNow = () -> dropNow((WorkflowAnnotation) transferData, null); } else { - // paste an existing Operator - newOperators = Tools.cloneOperators(newOperators); - boolean result; + List droppedOperators = Tools.cloneOperators(newOperators); + dropNow = () -> dropNow(droppedOperators, null); + } + boolean result = false; + try { + result = dropNow.getAsBoolean(); + } catch (RuntimeException e) { + LogService.getRoot().log(Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), + "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.error_in_paste", e), e); + SwingTools.showVerySimpleErrorMessage("error_in_paste", e.getMessage(), e.getMessage()); + } + dropEnds(); + if (result) { try { - result = dropNow(newOperators, null); - } catch (RuntimeException e) { - LogService.getRoot().log(Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), - "com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler.error_in_paste", e), e); - SwingTools.showVerySimpleErrorMessage("error_in_paste", e.getMessage(), e.getMessage()); - dropEnds(); - return false; + // log usage stats if applicable; ignore exceptions on unsupported flavor exception + transferable.getTransferData(UsageLoggable.USAGE_FLAVOR); + } catch (UnsupportedFlavorException | IOException ignored) { + // ignore, no logging implemented } - dropEnds(); - return result; } + return result; } } diff --git a/src/main/java/com/rapidminer/gui/dnd/RepositoryLocationList.java b/src/main/java/com/rapidminer/gui/dnd/RepositoryLocationList.java index 796d7dc45..d1926293f 100644 --- a/src/main/java/com/rapidminer/gui/dnd/RepositoryLocationList.java +++ b/src/main/java/com/rapidminer/gui/dnd/RepositoryLocationList.java @@ -28,7 +28,10 @@ * Container class for handling of multiple {@link RepositoryLocation}s * * @author Adrian Wilke + * @deprecated since 8.1.2 + * @see TransferableRepositoryEntry */ +@Deprecated public class RepositoryLocationList { private List repositoryLocations = new LinkedList<>(); diff --git a/src/main/java/com/rapidminer/gui/dnd/TransferableOperator.java b/src/main/java/com/rapidminer/gui/dnd/TransferableOperator.java index 203914ed1..e35e7be4f 100644 --- a/src/main/java/com/rapidminer/gui/dnd/TransferableOperator.java +++ b/src/main/java/com/rapidminer/gui/dnd/TransferableOperator.java @@ -27,6 +27,8 @@ import com.rapidminer.operator.Operator; import com.rapidminer.tools.Tools; +import com.rapidminer.tools.usagestats.DefaultUsageLoggable; +import com.rapidminer.tools.usagestats.UsageLoggable; /** @@ -35,7 +37,7 @@ * @see com.rapidminer.gui.operatortree.OperatorTree * @author Helge Homburg, Michael Knopf, Adrian Wilke */ -public class TransferableOperator implements Transferable { +public class TransferableOperator extends DefaultUsageLoggable implements Transferable { public static final DataFlavor LOCAL_TRANSFERRED_OPERATORS_FLAVOR = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType + ";class=" + Operator.class.getName(), "RapidMiner operator"); @@ -63,7 +65,11 @@ public TransferableOperator(Operator[] operators) { @Override public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { - + if (flavor.equals(UsageLoggable.USAGE_FLAVOR)){ + // trigger usage stats if applicable + logUsageStats(); + return null; + } if (flavor.equals(LOCAL_TRANSFERRED_OPERATORS_FLAVOR)) { return this.clonedOperators; } @@ -73,9 +79,8 @@ public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorExcepti b.append(op.getXML(false)); } return b.toString(); - } else { - throw new UnsupportedFlavorException(flavor); } + throw new UnsupportedFlavorException(flavor); } @Override @@ -94,4 +99,5 @@ public DataFlavor[] getTransferDataFlavors() { protected Operator[] getOperators() { return this.originalOperators; } + } diff --git a/src/main/java/com/rapidminer/gui/dnd/TransferableRepositoryEntry.java b/src/main/java/com/rapidminer/gui/dnd/TransferableRepositoryEntry.java index 91538f484..d4343a4ed 100644 --- a/src/main/java/com/rapidminer/gui/dnd/TransferableRepositoryEntry.java +++ b/src/main/java/com/rapidminer/gui/dnd/TransferableRepositoryEntry.java @@ -24,6 +24,8 @@ import java.util.Arrays; import com.rapidminer.repository.RepositoryLocation; +import com.rapidminer.tools.usagestats.DefaultUsageLoggable; +import com.rapidminer.tools.usagestats.UsageLoggable; /** @@ -32,20 +34,21 @@ * @author Marco Boeck * @since 8.1 */ -public class TransferableRepositoryEntry implements Transferable { +public class TransferableRepositoryEntry extends DefaultUsageLoggable implements Transferable { public static final DataFlavor LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR = new DataFlavor( DataFlavor.javaJVMLocalObjectMimeType + ";class=" + RepositoryLocation.class.getName(), "repository location"); public static final DataFlavor LOCAL_TRANSFERRED_REPOSITORY_LOCATION_LIST_FLAVOR = new DataFlavor( - DataFlavor.javaJVMLocalObjectMimeType + ";class=" + RepositoryLocationList.class.getName(), + DataFlavor.javaJVMLocalObjectMimeType + ";class=\"" + RepositoryLocation[].class.getName() + "\"", "repository locations"); private static final DataFlavor[] DATA_FLAVORS = { LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR, LOCAL_TRANSFERRED_REPOSITORY_LOCATION_LIST_FLAVOR, DataFlavor.stringFlavor }; private final RepositoryLocation[] location; + private final DataFlavor contentFlavor; /** * The transferable location(s). @@ -54,38 +57,41 @@ public class TransferableRepositoryEntry implements Transferable { * the location(s) of the entry/entries which should be drag & dropped */ public TransferableRepositoryEntry(RepositoryLocation... location) { - if (location == null) { - throw new IllegalArgumentException("location must not be null!"); + if (location == null || location.length == 0) { + throw new IllegalArgumentException("location must not be null or empty!"); } this.location = location; + this.contentFlavor = location.length == 1? LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR : LOCAL_TRANSFERRED_REPOSITORY_LOCATION_LIST_FLAVOR; } @Override public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { + if (flavor.equals(UsageLoggable.USAGE_FLAVOR)){ + // trigger usage stats if applicable + logUsageStats(); + return null; + } if (flavor.equals(LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR)) { return location[0]; - } else if (flavor.equals(LOCAL_TRANSFERRED_REPOSITORY_LOCATION_LIST_FLAVOR)) { + } + if (flavor.equals(LOCAL_TRANSFERRED_REPOSITORY_LOCATION_LIST_FLAVOR)) { return location; - } else if (flavor.equals(DataFlavor.stringFlavor)) { - if (location.length == 1) { - return location[0].toString(); - } else { - return Arrays.toString(location); - } - } else { - throw new UnsupportedFlavorException(flavor); } + if (flavor.equals(DataFlavor.stringFlavor)) { + return location.length == 1 ? location[0].toString() : Arrays.toString(location); + } + throw new UnsupportedFlavorException(flavor); } @Override public boolean isDataFlavorSupported(DataFlavor flavor) { - return Arrays.asList(DATA_FLAVORS).contains(flavor); + return flavor.equals(contentFlavor) || flavor.equals(DataFlavor.stringFlavor); } @Override public DataFlavor[] getTransferDataFlavors() { - return DATA_FLAVORS; + return new DataFlavor[]{contentFlavor, DataFlavor.stringFlavor}; } } diff --git a/src/main/java/com/rapidminer/gui/docking/DetachedDockViewAsTab.java b/src/main/java/com/rapidminer/gui/docking/DetachedDockViewAsTab.java index 738cafd9a..1d529f5e4 100644 --- a/src/main/java/com/rapidminer/gui/docking/DetachedDockViewAsTab.java +++ b/src/main/java/com/rapidminer/gui/docking/DetachedDockViewAsTab.java @@ -89,9 +89,9 @@ public void loggedActionPerformed(ActionEvent e) { SmartIconJButton[] iconsArray = icons.toArray(new SmartIconJButton[0]); smartIcon = new JTabbedPaneSmartIcon(dockKey.getIcon(), dockKey.getName(), null, null, true, iconsArray); smartIcon.setIconForTabbedPane(tabHeader); - tabHeader.addTab("", smartIcon, getDockable().getComponent()); + tabHeader.addTab("", smartIcon, getDockable().getComponent(), dockKey.getTooltip()); } else { - tabHeader.addTab(dockKey.getName(), dockKey.getIcon(), getDockable().getComponent()); + tabHeader.addTab(dockKey.getName(), dockKey.getIcon(), getDockable().getComponent(), dockKey.getTooltip()); } } @@ -149,7 +149,6 @@ protected void scanDrop(DockEvent event, boolean drop) { ((DockDragEvent) event).acceptDrag(lastDropGeneralPath); } else { GeneralPath path = buildPathForTab(bounds); - ; lastDropShape = r2d; lastDropGeneralPath = path; ((DockDragEvent) event).acceptDrag(lastDropGeneralPath); diff --git a/src/main/java/com/rapidminer/gui/flow/NewProcessUndoManager.java b/src/main/java/com/rapidminer/gui/flow/NewProcessUndoManager.java index 2fe109fa4..935273ee9 100644 --- a/src/main/java/com/rapidminer/gui/flow/NewProcessUndoManager.java +++ b/src/main/java/com/rapidminer/gui/flow/NewProcessUndoManager.java @@ -22,7 +22,10 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; import java.util.stream.Collectors; import com.rapidminer.Process; @@ -44,6 +47,17 @@ */ public class NewProcessUndoManager { + /** Regex to filter operator's height attribute from XML */ + private static final Pattern REGEX_OPERATOR_PATTERN = Pattern.compile("(?i)( XML_COMPARE_WIHOUT_HEIGHT = (a, b) -> { + a = REGEX_OPERATOR_PATTERN.matcher(a).replaceAll(REGEX_OPERATOR_REPLACEMENT); + b = REGEX_OPERATOR_PATTERN.matcher(b).replaceAll(REGEX_OPERATOR_REPLACEMENT); + return a.compareTo(b); + }; + /** * Simple storage construct for a {@link Process} state. * @@ -52,10 +66,10 @@ public class NewProcessUndoManager { */ private static class ProcessUndoState { - String processXML; - String displayedChain; - List selectedOperators; - List viewUserData; + private String processXML; + private String displayedChain; + private List selectedOperators; + private List viewUserData; } @@ -108,7 +122,8 @@ public void removeFirst() { public boolean snapshotDiffers() { ProcessUndoState last = lastSnapshot; ProcessUndoState current = snapshot; - return last != null && last != current && !last.processXML.equals(current.processXML); + return last != null && last != current + && XML_COMPARE_WIHOUT_HEIGHT.compare(last.processXML, current.processXML) != 0; } /** @@ -124,7 +139,6 @@ public boolean snapshotDiffers() { */ public boolean add(boolean useCurrent) { ProcessUndoState state = useCurrent ? snapshot : lastSnapshot; - // ProcessUndoState state = lastSnapshot; if (state != null && state.processXML != null) { undoList.add(state); return true; @@ -145,7 +159,7 @@ public void takeSnapshot(String processXML, OperatorChain displayedChain, Collec ProcessUndoState state = new ProcessUndoState(); state.processXML = processXML; state.displayedChain = displayedChain == null ? null : displayedChain.getName(); - state.selectedOperators = selectedOperators.stream().map(op -> op.getName()).collect(Collectors.toList()); + state.selectedOperators = selectedOperators.stream().map(Operator::getName).collect(Collectors.toList()); state.viewUserData = extractUserData(allOperators); lastSnapshot = snapshot; snapshot = state; @@ -206,8 +220,7 @@ public List restoreSelectedOperators(Process p, int index) { } catch (IndexOutOfBoundsException e) { return null; } - List selected = state.selectedOperators.stream().map(p::getOperator).filter(op -> op != null) - .collect(Collectors.toList()); + List selected = state.selectedOperators.stream().map(p::getOperator).filter(Objects::nonNull).collect(Collectors.toList()); return selected.isEmpty() ? null : selected; } diff --git a/src/main/java/com/rapidminer/gui/flow/processrendering/draw/ProcessDrawUtils.java b/src/main/java/com/rapidminer/gui/flow/processrendering/draw/ProcessDrawUtils.java index 6935f0db1..e48ebe795 100644 --- a/src/main/java/com/rapidminer/gui/flow/processrendering/draw/ProcessDrawUtils.java +++ b/src/main/java/com/rapidminer/gui/flow/processrendering/draw/ProcessDrawUtils.java @@ -28,12 +28,15 @@ import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.function.BiFunction; import java.util.logging.Level; - import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.UIManager; +import com.rapidminer.Process; import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel; import com.rapidminer.gui.flow.processrendering.view.ProcessRendererView; import com.rapidminer.gui.tools.SwingTools; @@ -46,6 +49,7 @@ import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.OutputPort; import com.rapidminer.operator.ports.Port; +import com.rapidminer.operator.ports.PortOwner; import com.rapidminer.operator.ports.metadata.CollectionMetaData; import com.rapidminer.operator.ports.metadata.CompatibilityLevel; import com.rapidminer.operator.ports.metadata.MetaData; @@ -58,7 +62,7 @@ /** * Contains utility methods which are helpful for drawing a process via Java2D. * - * @author Marco Boeck + * @author Marco Boeck, Jan Czogalla * @since 6.4.0 * */ @@ -95,13 +99,13 @@ private ProcessDrawUtils() { * This method adds the colors of the given property file to the global group colors. * * @param groupProperties - * the properties group name + * the properties group name * @param pluginName - * the name of the plugin + * the name of the plugin * @param classLoader - * the classloader responsible who registered the colors + * the classloader responsible who registered the colors * @param provider - * the extension for the registered group color + * the extension for the registered group color */ public static void registerAdditionalGroupColors(final String groupProperties, final String pluginName, final ClassLoader classLoader, final Plugin provider) { @@ -112,13 +116,13 @@ public static void registerAdditionalGroupColors(final String groupProperties, f * This method adds the colors of the given property file to the io object colors. * * @param groupProperties - * the properties group name + * the properties group name * @param pluginName - * the name of the plugin + * the name of the plugin * @param classLoader - * the classloader responsible who registered the colors + * the classloader responsible who registered the colors * @param provider - * the extension to registered IOObjects for + * the extension to registered IOObjects for */ public static void registerAdditionalObjectColors(final String groupProperties, final String pluginName, final ClassLoader classLoader, final Plugin provider) { @@ -134,7 +138,7 @@ public static void registerAdditionalObjectColors(final String groupProperties, * Returns the color specified for the given type of metadata. * * @param md - * the metadata or {@code null} + * the metadata or {@code null} * @return a color, never {@code null} */ public static Color getColorFor(final MetaData md) { @@ -157,11 +161,11 @@ public static Color getColorFor(final MetaData md) { * Returns a color for the given {@link Port} depending on the metadata at the port. * * @param port - * the port for which a color should be retrieved + * the port for which a color should be retrieved * @param defaultColor - * the default color if the port has no data + * the default color if the port has no data * @param enabled - * if the port is enabled + * if the port is enabled * @return a color, never {@code null} */ public static Color getColorFor(final Port port, final Color defaultColor, final boolean enabled) { @@ -202,7 +206,7 @@ public static Color getColorFor(final Port port, final Color defaultColor, final * not check if snapping is enabled! * * @param point - * the point which should snap to the nearest possible grid position + * the point which should snap to the nearest possible grid position * @return the nearest position on the grid, never {@code null} */ public static Point snap(final Point2D point) { @@ -212,12 +216,10 @@ public static Point snap(final Point2D point) { int snappedX = (int) point.getX() - ProcessDrawer.GRID_X_OFFSET; int factor = (snappedX + ProcessDrawer.GRID_WIDTH / 2) / ProcessDrawer.GRID_WIDTH; - snappedX /= ProcessDrawer.GRID_WIDTH; snappedX = factor * ProcessDrawer.GRID_WIDTH + ProcessDrawer.GRID_X_OFFSET; int snappedY = (int) point.getY() - ProcessDrawer.GRID_Y_OFFSET; factor = (snappedY + ProcessDrawer.GRID_HEIGHT / 2) / ProcessDrawer.GRID_HEIGHT; - snappedY /= ProcessDrawer.GRID_HEIGHT; snappedY = factor * ProcessDrawer.GRID_HEIGHT + ProcessDrawer.GRID_Y_OFFSET; return new Point(snappedX, snappedY); @@ -227,9 +229,9 @@ public static Point snap(final Point2D point) { * Calculates a {@link Rectangle2D} from the specified start and end point. * * @param start - * the starting point + * the starting point * @param end - * the end point + * the end point * @return the rectangle or {@code null} if the start/end point was {@code null} */ public static Rectangle2D createRectangle(final Point start, final Point end) { @@ -245,9 +247,9 @@ public static Rectangle2D createRectangle(final Point start, final Point end) { * Draws a shadow around the given rectangle. * * @param rect - * the rectangle which should get a shadow + * the rectangle which should get a shadow * @param g2 - * the graphics context to draw the shadow on + * the graphics context to draw the shadow on */ public static void drawShadow(final Rectangle2D rect, final Graphics2D g2) { Graphics2D g2S = (Graphics2D) g2.create(); @@ -281,13 +283,12 @@ public static void drawShadow(final Rectangle2D rect, final Graphics2D g2) { * Creates a spline connector shape from one {@link Port} to another. * * @param fromPort - * the origin port + * the origin port * @param toPort - * the end port + * the end port * @param model - * the model required to create port locations - * @return the shape representing the connection or {@code null} if the ports have no location - * yet + * the model required to create port locations + * @return the shape representing the connection or {@code null} if the ports have no location yet */ public static Shape createConnector(final OutputPort fromPort, final Port toPort, final ProcessRendererModel model) { Point2D from = ProcessDrawUtils.createPortLocation(fromPort, model); @@ -305,9 +306,9 @@ public static Shape createConnector(final OutputPort fromPort, final Port toPort * Creates a spline connector shape from one {@link Point2D} to another. * * @param from - * the starting point + * the starting point * @param to - * the end point + * the end point * @return the shape representing the connection, never {@code null} */ public static Shape createConnectionSpline(final Point2D from, final Point2D to) { @@ -334,16 +335,23 @@ public static Shape createConnectionSpline(final Point2D from, final Point2D to) * Returns the location of the given {@link Port}. * * @param port - * the location for this port will be returned + * the location for this port will be returned * @param model - * the model required to create port locations + * the model required to create port locations * @return the point or {@code null} if the port has no location yet */ public static Point createPortLocation(final Port port, final ProcessRendererModel model) { if (port.getPorts() == null) { return new Point(0, 0); } - Operator op = port.getPorts().getOwner().getOperator(); + PortOwner portOwner = port.getPorts().getOwner(); + Operator op = portOwner.getOperator(); + boolean isDisplayed = op == model.getDisplayedChain(); + Rectangle2D operatorRect = model.getOperatorRect(op); + if (!isDisplayed && operatorRect == null) { + // called before notification of added operator was received, no location set yet + return null; + } int index = port.getPorts().getAllPorts().indexOf(port); int addOffset = 0; int xOffset = 0; @@ -351,54 +359,35 @@ public static Point createPortLocation(final Port port, final ProcessRendererMod addOffset += model.getPortSpacing(port.getPorts().getPortByIndex(i)); } - ExecutionUnit process; - Point point; - if (op == model.getDisplayedChain()) { + int x; + int y; + if (isDisplayed) { // this is an inner port - process = port.getPorts().getOwner().getConnectionContext(); - if (port instanceof OutputPort) { - point = new Point(0 + xOffset, ProcessDrawer.OPERATOR_MIN_HEIGHT / 2 + ProcessDrawer.PORT_OFFSET - + index * ProcessDrawer.PORT_SIZE * 3 / 2 + addOffset); - } else { - point = new Point((int) Math.ceil(model.getProcessWidth(process) * (1 / model.getZoomFactor()) - xOffset), - ProcessDrawer.OPERATOR_MIN_HEIGHT / 2 + ProcessDrawer.PORT_OFFSET - + index * ProcessDrawer.PORT_SIZE * 3 / 2 + addOffset); - } + ExecutionUnit process = portOwner.getConnectionContext(); + x = port instanceof OutputPort ? xOffset : (int) Math.ceil(model.getProcessWidth(process) * (1 / model.getZoomFactor()) - xOffset); + y = ProcessDrawer.OPERATOR_MIN_HEIGHT / 2 + ProcessDrawer.PORT_OFFSET + index * ProcessDrawer.PORT_SIZE * 3 / 2 + addOffset; } else { // this is an outer port of a nested operator - process = op.getExecutionUnit(); - Rectangle2D opRect = model.getOperatorRect(op); - - // called before notifaction of added operator was received, no location set yet - if (opRect == null) { - return null; - } - if (port instanceof InputPort) { - point = new Point((int) Math.ceil(opRect.getX()), (int) Math.ceil( - opRect.getY() + ProcessDrawer.PORT_OFFSET + index * ProcessDrawer.PORT_SIZE * 3 / 2 + addOffset)); - } else { - point = new Point((int) Math.ceil(opRect.getMaxX()), (int) Math.ceil( - opRect.getY() + ProcessDrawer.PORT_OFFSET + index * ProcessDrawer.PORT_SIZE * 3 / 2 + addOffset)); - } + y = (int) Math.ceil(operatorRect.getY() + ProcessDrawer.PORT_OFFSET + index * ProcessDrawer.PORT_SIZE * 3 / 2d + addOffset); + x = (int) Math.ceil(port instanceof InputPort ? operatorRect.getX() : operatorRect.getMaxX()); } - return point; + return new Point(x, y); } /** * Returns the absolute point for a given point in a process. * * @param p - * the relative point which is relative to the displayed process + * the relative point which is relative to the displayed process * @param processIndex - * the index of the process in question + * the index of the process in question * @param model - * the model required to calculate the absolute location - * @return the absolute point (relative to the process renderer view) or {@code null} if the - * process index is invalid + * the model required to calculate the absolute location + * @return the absolute point (relative to the process renderer view) or {@code null} if the process index is invalid */ public static Point convertToAbsoluteProcessPoint(final Point p, final int processIndex, - final ProcessRendererModel model) { + final ProcessRendererModel model) { double xOffset = 0; for (int i = 0; i < model.getProcesses().size(); i++) { if (i == processIndex) { @@ -413,13 +402,12 @@ public static Point convertToAbsoluteProcessPoint(final Point p, final int proce * Returns the relative point for a given absolute point. * * @param p - * the relative point which is relative to the displayed process + * the relative point which is relative to the displayed process * @param processIndex - * the index of the process in question + * the index of the process in question * @param model - * the model required to calculate the relative location - * @return the relative point which is relative to the displayed process or {@code null} if the - * process index is invalid + * the model required to calculate the relative location + * @return the relative point which is relative to the displayed process or {@code null} if the process index is invalid */ public static Point convertToRelativePoint(final Point p, final int processIndex, final ProcessRendererModel model) { double xOffset = 0; @@ -436,11 +424,11 @@ public static Point convertToRelativePoint(final Point p, final int processIndex * Abbreviates the string using {@code ...} if necessary. * * @param string - * the string to shorten + * the string to shorten * @param g2 - * the graphics context + * the graphics context * @param maxWidth - * the max width in px the string is allowed to use + * the max width in px the string is allowed to use * @return the shorted string, never {@code null} */ public static String fitString(String string, final Graphics2D g2, final int maxWidth) { @@ -468,9 +456,8 @@ public static String fitString(String string, final Graphics2D g2, final int max * Returns whether the specified {@link Operator} has at least one free input and output port. * * @param operator - * the operator in question - * @return {@code true} if the operator has one free input and output port ; {@code false} - * otherwise + * the operator in question + * @return {@code true} if the operator has one free input and output port ; {@code false} otherwise */ public static boolean hasOperatorFreePorts(final Operator operator) { if (operator == null) { @@ -501,9 +488,8 @@ public static boolean hasOperatorFreePorts(final Operator operator) { * free input and output port. * * @param operator - * the operator in question - * @return {@code true} if the operator has one free input and output port ; {@code false} - * otherwise + * the operator in question + * @return {@code true} if the operator has one free input and output port ; {@code false} otherwise */ public static boolean canOperatorBeInsertedIntoConnection(final ProcessRendererModel model, final Operator operator) { if (operator == null) { @@ -511,6 +497,12 @@ public static boolean canOperatorBeInsertedIntoConnection(final ProcessRendererM } OutputPort hoveringConnectionSource = model.getHoveringConnectionSource(); + // prevent loops to self + if (hoveringConnectionSource != null && (operator.getOutputPorts().containsPort(hoveringConnectionSource) || + operator.getInputPorts().containsPort(hoveringConnectionSource.getDestination()))) { + return false; + } + MetaData md = null; try { md = hoveringConnectionSource != null ? hoveringConnectionSource.getMetaData(MetaData.class) : null; @@ -520,16 +512,9 @@ public static boolean canOperatorBeInsertedIntoConnection(final ProcessRendererM } boolean hasFreeInput = false; for (InputPort port : operator.getInputPorts().getAllPorts()) { - if (!port.isConnected()) { - if (md != null) { - if (port.isInputCompatible(md, CompatibilityLevel.PRE_VERSION_5)) { - hasFreeInput = true; - break; - } - } else { - hasFreeInput = true; - break; - } + if (!port.isConnected() && (md == null || port.isInputCompatible(md, CompatibilityLevel.PRE_VERSION_5))) { + hasFreeInput = true; + break; } } if (!hasFreeInput) { @@ -547,24 +532,23 @@ public static boolean canOperatorBeInsertedIntoConnection(final ProcessRendererM * Returns the height for the specified {@link Operator}. * * @param operator - * the operator in question + * the operator in question * @return */ public static double calcHeighForOperator(Operator operator) { double calcHeight = 40 + ProcessRendererModel.PORT_SIZE * 3 / 2 * Math.max(operator.getInputPorts().getNumberOfPorts(), operator.getOutputPorts().getNumberOfPorts()); - double height = Math.max(ProcessRendererModel.MIN_OPERATOR_HEIGHT, calcHeight); - return height; + return Math.max(ProcessRendererModel.MIN_OPERATOR_HEIGHT, calcHeight); } /** * Returns the given icon in an appropriate enabled/disabled state. * * @param operator - * if the operator is enabled, returns the passed icon directly, otherwise it is - * displayed as disabled + * if the operator is enabled, returns the passed icon directly, otherwise it is + * displayed as disabled * @param icon - * the icon + * the icon * @return the original icon (if the given operator is enabled) or the icon in a disabled state */ public static ImageIcon getIcon(final Operator operator, final ImageIcon icon) { @@ -586,20 +570,153 @@ public static ImageIcon getIcon(final Operator operator, final ImageIcon icon) { *

* * @param model - * the model required to calculate the size + * the model required to calculate the size * @param unit - * the process for which the initial size should be created + * the process for which the initial size should be created * @param width - * the total width which is available + * the total width which is available * @param height - * the total height which is available + * the total height which is available * @return the size, never {@code null} * @since 7.5 */ - public static Dimension calculatePreferredSize(final ProcessRendererModel model, final ExecutionUnit unit, int width, - int height) { + public static Dimension calculatePreferredSize(final ProcessRendererModel model, final ExecutionUnit unit, int width, int height) { int processes = model.getProcesses().size(); int wallSpace = (processes - 1) * 2 * ProcessDrawer.WALL_WIDTH; return new Dimension((width - wallSpace) / processes, height); } + + /** + * Ensures that each operator in the given {@link Process} has a location. Uses the given + * {@link ProcessRendererModel} to determine layout properties. + * + * @param process + * the process in question + * @param model + * the reference process model + * @return the list of operators that did not have a location and now have one + * @see #ensureOperatorsHaveLocation(ExecutionUnit, ProcessRendererModel) + * @since 8.2 + */ + public static List ensureOperatorsHaveLocation(Process process, ProcessRendererModel model) { + return ensureOperatorsHaveLocation(process.getRootOperator().getSubprocess(0), model); + } + + /** + * Ensures that each operator in the given {@link ExecutionUnit} has a location. Uses the given + * {@link ProcessRendererModel} to determine layout properties. + * + * @param unit + * the execution unit in question + * @param model + * the reference process model + * @return the list of operators that did not have a location and now have one + * @since 8.2 + */ + public static List ensureOperatorsHaveLocation(ExecutionUnit unit, ProcessRendererModel model) { + List movedOperators = new LinkedList<>(); + for (Operator op : unit.getAllInnerOperators()) { + + // check if all operators have positions, if not, set them now + Rectangle2D rect = model.getOperatorRect(op); + if (rect == null) { + rect = createOperatorPosition(op, model); + model.setOperatorRect(op, rect); + movedOperators.add(op); + } + } + return movedOperators; + } + + /** + * Returns a {@link Rectangle2D} representing an {@link Operator}. First tries to look up the + * data from the model, if that fails it determines a position automatically. Uses the given + * {@link ProcessRendererModel} to determine layout properties. + * + * @param op + * the operator for which a rectangle should be created + * @param model + * the reference process model + * @return the rectangle representing the operator, never {@code null} + * @since 8.2 + */ + public static Rectangle2D createOperatorPosition(Operator op, ProcessRendererModel model) { + return createOperatorPosition(op, model, null); + } + + /** + * Returns a {@link Rectangle2D} representing an {@link Operator}. First tries to look up the + * data from the model, if that fails it determines a position automatically. Uses the given + * {@link ProcessRendererModel} to determine layout properties. May use a supplied {@link BiFunction} + * to check some dependencies + * + * @param op + * the operator for which a rectangle should be created + * @param model + * the reference process model + * @param checkDependencies + * an optional dependency check + * @return the rectangle representing the operator, never {@code null} + * @since 8.2 + */ + public static Rectangle2D createOperatorPosition(Operator op, ProcessRendererModel model, BiFunction checkDependencies) { + Rectangle2D rect = model.getOperatorRect(op); + if (rect != null) { + return rect; + } + + // if connected (e.g. because inserted by quick fix), place in the middle + if (op.getInputPorts().getNumberOfPorts() > 0 && op.getOutputPorts().getNumberOfPorts() > 0 + && op.getInputPorts().getPortByIndex(0).isConnected() + && op.getOutputPorts().getPortByIndex(0).isConnected() + // see ProcessRendererController#isDependenciesOk + && (checkDependencies == null || checkDependencies.apply(op, model))) { + Point2D sourcePos = ProcessDrawUtils.createPortLocation(op.getInputPorts().getPortByIndex(0).getSource(), model); + Point2D destPos = ProcessDrawUtils.createPortLocation(op.getOutputPorts().getPortByIndex(0).getDestination(), + model); + if (sourcePos != null && destPos != null) { + double x = Math.floor((sourcePos.getX() + destPos.getX()) / 2d - ProcessDrawer.OPERATOR_WIDTH / 2d); + double y = Math.floor((sourcePos.getY() + destPos.getY()) / 2d - ProcessDrawer.PORT_OFFSET); + return new Rectangle2D.Double(x, y, ProcessDrawer.OPERATOR_WIDTH, calcHeighForOperator(op)); + } + } + // otherwise, or, if positions were not known in previous approach, position according to index + int index = 0; + ExecutionUnit unit = op.getExecutionUnit(); + if (unit != null) { + index = unit.getOperators().indexOf(op); + } + return autoPosition(op, index, model); + } + + /** + * Calculates the position of an operator. Will never set the rectangle in the operator. Uses + * the given {@link ProcessRendererModel} to determine layout properties. + * + * @param index + * the operators index in the surrounding {@link ExecutionUnit} + * @param model + * the reference process model + * @return the position and size of the operator, never {@code null} + * @since 8.2 + */ + public static Rectangle2D autoPosition(Operator op, int index, ProcessRendererModel model) { + int maxPerRow = (int) Math.max(1, + Math.floor(model.getProcessWidth(op.getExecutionUnit()) / (ProcessDrawer.GRID_AUTOARRANGE_WIDTH + 10))); + int col = index % maxPerRow; + int row = index / maxPerRow; + Rectangle2D old = model.getOperatorRect(op); + + double x = col * ProcessDrawer.GRID_AUTOARRANGE_WIDTH + ProcessDrawer.GRID_X_OFFSET; + double y = ProcessDrawer.GRID_AUTOARRANGE_HEIGHT * row + ProcessDrawer.GRID_Y_OFFSET; + double width = Math.floor(old != null ? old.getWidth() : ProcessDrawer.OPERATOR_WIDTH); + double height = Math.floor(old != null ? old.getHeight() : ProcessDrawer.OPERATOR_MIN_HEIGHT); + + if (model.isSnapToGrid()) { + Point snappedPoint = snap(new Point2D.Double(x, y)); + x = snappedPoint.getX(); + y = snappedPoint.getY(); + } + return new Rectangle2D.Double(x, y, width, height); + } } diff --git a/src/main/java/com/rapidminer/gui/flow/processrendering/draw/ProcessDrawer.java b/src/main/java/com/rapidminer/gui/flow/processrendering/draw/ProcessDrawer.java index 9f2f5da93..cb1c46bf8 100644 --- a/src/main/java/com/rapidminer/gui/flow/processrendering/draw/ProcessDrawer.java +++ b/src/main/java/com/rapidminer/gui/flow/processrendering/draw/ProcessDrawer.java @@ -41,7 +41,6 @@ import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; - import javax.swing.ImageIcon; import com.rapidminer.BreakpointListener; @@ -95,6 +94,9 @@ public final class ProcessDrawer { public static final int PORT_OFFSET = OPERATOR_FONT.getSize() + 6 + PORT_SIZE; public static final int WALL_WIDTH = 3; + public static final int PROCESS_BOX_SHADOW_OFFSET_X = 4; + public static final int PROCESS_BOX_SHADOW_OFFSET_Y = 4; + public static final int GRID_WIDTH = OPERATOR_WIDTH * 3 / 4; public static final int GRID_HEIGHT = OPERATOR_MIN_HEIGHT * 3 / 4; public static final int GRID_X_OFFSET = OPERATOR_WIDTH / 2; @@ -134,11 +136,11 @@ public final class ProcessDrawer { private static final Stroke PORT_STROKE = new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Stroke PORT_HIGHLIGHT_STROKE = new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Stroke SELECTION_RECT_STROKE = new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, - 5f, new float[] { 2f, 2f }, 0f); + 5f, new float[]{2f, 2f}, 0f); private static final Paint SELECTION_RECT_PAINT = Color.GRAY; private static final Color PROCESS_TITLE_COLOR = SHADOW_COLOR; private static final Stroke BORDER_DRAG_STROKE = new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER, - 10.0f, new float[] { 10.0f }, 0.0f); + 10.0f, new float[]{10.0f}, 0.0f); private static final int DRAG_BORDER_PADDING = 30; private static final int DRAG_BORDER_CORNER = 15; @@ -228,10 +230,10 @@ public final class ProcessDrawer { * Creates a new drawer instance which can be used to draw the process specified in the model. * * @param model - * the model containing the data needed to draw the process. See - * {@link ProcessRendererModel} for a minimal configuration + * the model containing the data needed to draw the process. See + * {@link ProcessRendererModel} for a minimal configuration * @param drawHighlight - * if {@code true} will highlight drop area in the process during drag & drop + * if {@code true} will highlight drop area in the process during drag & drop */ public ProcessDrawer(final ProcessRendererModel model, final boolean drawHighlight) { if (model == null) { @@ -256,9 +258,9 @@ public ProcessDrawer(final ProcessRendererModel model, final boolean drawHighlig * decorators are called during their respective {@link RenderPhase}s. * * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon * @param printing - * if {@code true} we are printing instead of drawing to the screen + * if {@code true} we are printing instead of drawing to the screen */ public void draw(final Graphics2D g2, final boolean printing) { Graphics2D g = (Graphics2D) g2.create(); @@ -380,9 +382,9 @@ public void draw(final Graphics2D g2, final boolean printing) { * units. * * @param process - * the process to draw the wall for + * the process to draw the wall for * @param g2 - * the graphics context to draw on + * the graphics context to draw on */ private void renderProcessWall(ExecutionUnit process, Graphics2D g2) { double width = model.getProcessWidth(process); @@ -401,11 +403,11 @@ private void renderProcessWall(ExecutionUnit process, Graphics2D g2) { * calls all registered {@link ProcessDrawDecorator}s for the background render phase. * * @param process - * the process to draw the background for + * the process to draw the background for * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon * @param printing - * if {@code true} we are printing instead of drawing to the screen + * if {@code true} we are printing instead of drawing to the screen */ public void drawBackground(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { Graphics2D gBG = (Graphics2D) g2.create(); @@ -418,11 +420,11 @@ public void drawBackground(final ExecutionUnit process, final Graphics2D g2, fin * annotations render phase. * * @param process - * the process to draw the annotations for + * the process to draw the annotations for * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon * @param printing - * if {@code true} we are printing instead of drawing to the screen + * if {@code true} we are printing instead of drawing to the screen */ public void drawAnnotations(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { // let decorators draw @@ -434,11 +436,11 @@ public void drawAnnotations(final ExecutionUnit process, final Graphics2D g2, fi * the annotations render phase. * * @param process - * the process to draw the operator backgrounds for + * the process to draw the operator backgrounds for * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon * @param printing - * if {@code true} we are printing instead of drawing to the screen + * if {@code true} we are printing instead of drawing to the screen */ public void drawOperatorBackgrounds(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { Graphics2D gBG = (Graphics2D) g2.create(); @@ -477,11 +479,11 @@ public void drawOperatorBackgrounds(final ExecutionUnit process, final Graphics2 * then calls all registered {@link ProcessDrawDecorator}s for the connections render phase. * * @param process - * the process to draw the connections for + * the process to draw the connections for * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon * @param printing - * if {@code true} we are printing instead of drawing to the screen + * if {@code true} we are printing instead of drawing to the screen */ public void drawConnections(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { Graphics2D gCo = (Graphics2D) g2.create(); @@ -503,11 +505,11 @@ public void drawConnections(final ExecutionUnit process, final Graphics2D g2, fi * the annotations render phase. * * @param process - * the process to draw the operator annotations for + * the process to draw the operator annotations for * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon * @param printing - * if {@code true} we are printing instead of drawing to the screen + * if {@code true} we are printing instead of drawing to the screen */ public void drawOperatorAnnotations(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { // let decorators draw @@ -520,11 +522,11 @@ public void drawOperatorAnnotations(final ExecutionUnit process, final Graphics2 * {@link ProcessDrawDecorator}s for the operator render phase. * * @param process - * the process to draw the operators for + * the process to draw the operators for * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon * @param printing - * if {@code true} we are printing instead of drawing to the screen + * if {@code true} we are printing instead of drawing to the screen */ public void drawOperators(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { Graphics2D gOp = (Graphics2D) g2.create(); @@ -557,11 +559,11 @@ public void drawOperators(final ExecutionUnit process, final Graphics2D g2, fina * the operator additions render phase. * * @param process - * the process to draw the operator additions for + * the process to draw the operator additions for * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon * @param printing - * if {@code true} we are printing instead of drawing to the screen + * if {@code true} we are printing instead of drawing to the screen */ public void drawOperatorAdditions(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { // let decorators draw @@ -574,11 +576,11 @@ public void drawOperatorAdditions(final ExecutionUnit process, final Graphics2D * for the overlay render phase. * * @param process - * the process to draw the overlay for + * the process to draw the overlay for * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon * @param printing - * if {@code true} we are printing instead of drawing to the screen + * if {@code true} we are printing instead of drawing to the screen */ public void drawOverlay(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { // let decorators draw @@ -590,11 +592,11 @@ public void drawOverlay(final ExecutionUnit process, final Graphics2D g2, final * calls all registered {@link ProcessDrawDecorator}s for the foreground render phase. * * @param process - * the process to draw the foreground for + * the process to draw the foreground for * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon * @param printing - * if {@code true} we are printing instead of drawing to the screen + * if {@code true} we are printing instead of drawing to the screen */ public void drawForeground(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { Graphics2D gBG = (Graphics2D) g2.create(); @@ -606,14 +608,14 @@ public void drawForeground(final ExecutionUnit process, final Graphics2D g2, fin * Draws the given {@link Operator} if inside the graphics clip bounds. * * @param op - * the operator to draw. Note that it must have a position attached, see - * {@link GUIProcessXMLFilter} + * the operator to draw. Note that it must have a position attached, see + * {@link GUIProcessXMLFilter} * @param drawPorts - * if {@true} will also draw operator ports, otherwise will not draw ports + * if {@true} will also draw operator ports, otherwise will not draw ports * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon * @param printing - * if {@code true} we are printing instead of drawing to the screen + * if {@code true} we are printing instead of drawing to the screen * */ public void drawOperator(final Operator op, final boolean drawPorts, final Graphics2D g2, final boolean printing) { @@ -641,11 +643,11 @@ public void drawOperator(final Operator op, final boolean drawPorts, final Graph * Adds the given draw decorator for the specified render phase. * * @param decorator - * the decorator instance to add + * the decorator instance to add * @param phase - * the phase during which the decorator should be called to draw. If multiple - * decorators want to draw during the same phase, they are called in the order they - * were registered + * the phase during which the decorator should be called to draw. If multiple + * decorators want to draw during the same phase, they are called in the order they + * were registered */ public void addDecorator(final ProcessDrawDecorator decorator, final RenderPhase phase) { if (decorator == null) { @@ -663,9 +665,9 @@ public void addDecorator(final ProcessDrawDecorator decorator, final RenderPhase * removed, does nothing. * * @param decorator - * the decorator instance to remove + * the decorator instance to remove * @param phase - * the phase from which the decorator should be removed + * the phase from which the decorator should be removed */ public void removeDecorator(final ProcessDrawDecorator decorator, final RenderPhase phase) { if (decorator == null) { @@ -683,7 +685,7 @@ public void removeDecorator(final ProcessDrawDecorator decorator, final RenderPh * was drawn. * * @param decorator - * the decorator instance to add + * the decorator instance to add */ public void addDecorator(final OperatorDrawDecorator decorator) { if (decorator == null) { @@ -698,7 +700,7 @@ public void addDecorator(final OperatorDrawDecorator decorator) { * nothing. * * @param decorator - * the decorator instance to remove + * the decorator instance to remove */ public void removeDecorator(final OperatorDrawDecorator decorator) { if (decorator == null) { @@ -712,9 +714,9 @@ public void removeDecorator(final OperatorDrawDecorator decorator) { * Draws the given {@link Operator}. * * @param operator - * the operator to draw + * the operator to draw * @param g2 - * the graphics context + * the graphics context */ private void renderOperator(final Operator operator, final Graphics2D g2) { Rectangle2D frame = model.getOperatorRect(operator); @@ -744,22 +746,27 @@ private void renderOperator(final Operator operator, final Graphics2D g2) { baseColor = Color.LIGHT_GRAY; } + if (operator instanceof OperatorChain) { + Rectangle shadowRect = new Rectangle(bodyShape.getBounds()); + shadowRect.setLocation(shadowRect.x - PROCESS_BOX_SHADOW_OFFSET_X, shadowRect.y + PROCESS_BOX_SHADOW_OFFSET_Y); + Shape shadowShape = new RoundRectangle2D.Double(shadowRect.getMinX(), shadowRect.getMinY(), + shadowRect.getWidth(), shadowRect.getHeight(), OPERATOR_CORNER, OPERATOR_CORNER); + + g2.setPaint(baseColor); + g2.fill(shadowShape); + + g2.setPaint(LINE_COLOR); + g2.setStroke(LINE_STROKE); + drawOperatorShape(g2, isSelected, isHovered, shadowShape); + } + g2.setPaint(baseColor); g2.fill(bodyShape); g2.setPaint(LINE_COLOR); g2.setStroke(LINE_STROKE); - if (isSelected) { - g2.setPaint(OPERATOR_BORDER_COLOR_SELECTED); - g2.setStroke(OPERATOR_STROKE_SELECTED); - } else if (isHovered) { - g2.setPaint(OPERATOR_BORDER_COLOR_HIGHLIGHT); - g2.setStroke(OPERATOR_STROKE_HIGHLIGHT); - } else { - g2.setPaint(OPERATOR_BORDER_COLOR); - g2.setStroke(OPERATOR_STROKE_NORMAL); - } - g2.draw(bodyShape); + + drawOperatorShape(g2, isSelected, isHovered, bodyShape); // Label: Name g2.setFont(OPERATOR_FONT); @@ -861,12 +868,32 @@ private void renderOperator(final Operator operator, final Graphics2D g2) { // placeholder for workflow annotations icon iconX += IMAGE_BREAKPOINTS.getIconWidth() + 1; + } - if (operator instanceof OperatorChain) { - ProcessDrawUtils.getIcon(operator, IMAGE_BRANCH).paintIcon(null, g2, iconX, - (int) (frame.getY() + frame.getHeight() - IMAGE_BRANCH.getIconHeight() - 1)); + /** + * Draw the given shape with color settings for an Operator + * + * @param graphics2D + * The graphics to use for drawing + * @param isSelected + * if true set colors to show selection + * @param isHovered + * if true set colors to show hovering + * @param bodyShape + * a shape to be drawn + */ + private void drawOperatorShape(Graphics2D graphics2D, boolean isSelected, boolean isHovered, Shape bodyShape) { + if (isSelected) { + graphics2D.setPaint(OPERATOR_BORDER_COLOR_SELECTED); + graphics2D.setStroke(OPERATOR_STROKE_SELECTED); + } else if (isHovered) { + graphics2D.setPaint(OPERATOR_BORDER_COLOR_HIGHLIGHT); + graphics2D.setStroke(OPERATOR_STROKE_HIGHLIGHT); + } else { + graphics2D.setPaint(OPERATOR_BORDER_COLOR); + graphics2D.setStroke(OPERATOR_STROKE_NORMAL); } - iconX += IMAGE_BRANCH.getIconWidth() + 1; + graphics2D.draw(bodyShape); } /** @@ -1049,9 +1076,9 @@ private void renderPorts(final Ports ports, final Graphics2D g, * Draws the connections for the given ports. * * @param ports - * the output ports for which to draw connections + * the output ports for which to draw connections * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon */ @SuppressWarnings("deprecation") private void renderConnections(final OutputPorts ports, final Graphics2D g2) { @@ -1118,9 +1145,9 @@ private void renderConnections(final OutputPorts ports, final Graphics2D g2) { * Draws the operator background (white round rectangle). * * @param operator - * the operator to draw the background for + * the operator to draw the background for * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon */ private void renderOperatorBackground(final Operator operator, final Graphics2D g2) { Rectangle2D frame = model.getOperatorRect(operator); @@ -1131,8 +1158,15 @@ private void renderOperatorBackground(final Operator operator, final Graphics2D return; } - RoundRectangle2D background = new RoundRectangle2D.Double(frame.getX() - 7, frame.getY() - 3, frame.getWidth() + 14, - frame.getHeight() + 11, OPERATOR_BG_CORNER, OPERATOR_BG_CORNER); + int expandLeft = 0; + int expandHeight = 0; + if (operator instanceof OperatorChain) { + expandLeft = PROCESS_BOX_SHADOW_OFFSET_X; + expandHeight = PROCESS_BOX_SHADOW_OFFSET_Y; + } + RoundRectangle2D background = new RoundRectangle2D.Double(frame.getX() - 7 - expandLeft, frame.getY() - 3, + frame.getWidth() + 14 + expandLeft, frame.getHeight() + 11 + expandHeight, + OPERATOR_BG_CORNER, OPERATOR_BG_CORNER); g2.setColor(Color.WHITE); g2.fill(background); @@ -1159,11 +1193,11 @@ private void renderOperatorBackground(final Operator operator, final Graphics2D * Draws the connections background (round pipe) for the given ports. * * @param inputPorts - * the input ports for which to draw connection backgrounds + * the input ports for which to draw connection backgrounds * @param ports - * the output ports for which to draw connection backgrounds + * the output ports for which to draw connection backgrounds * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon */ @SuppressWarnings("deprecation") private void renderConnectionsBackground(final InputPorts inputPorts, final OutputPorts ports, final Graphics2D g2) { @@ -1237,9 +1271,9 @@ private void renderPortsBackground(final Ports ports, final Grap * Draws the background for the given process. * * @param process - * the process for which to render the background + * the process for which to render the background * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon */ private void renderBackground(final ExecutionUnit process, final Graphics2D g2, boolean printing) { double width = model.getProcessWidth(process); @@ -1388,9 +1422,9 @@ private void renderBackground(final ExecutionUnit process, final Graphics2D g2, * Renders the drag border if needed. * * @param process - * the process for which to render the background + * the process for which to render the background * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon */ private void renderForeground(final ExecutionUnit process, final Graphics2D g2, boolean printing) { if (drawHighlight && !printing && (model.isDragStarted() || model.isDropTargetSet() && model.isImportDragged()) @@ -1412,9 +1446,9 @@ private void renderForeground(final ExecutionUnit process, final Graphics2D g2, * Draws the drag border. * * @param process - * the process for which to render the background + * the process for which to render the background * @param g2 - * the graphics context to draw upon + * the graphics context to draw upon */ private void drawDragBorder(final ExecutionUnit process, final Graphics2D g2) { double width = model.getProcessWidth(process); @@ -1430,17 +1464,17 @@ private void drawDragBorder(final ExecutionUnit process, final Graphics2D g2) { * Lets the decorators draw for the specified {@link RenderPhase}. * * @param process - * the process which should be decorated + * the process which should be decorated * @param g2 - * the graphics context. Each decorator gets a new context which is disposed - * afterwards + * the graphics context. Each decorator gets a new context which is disposed + * afterwards * @param phase - * the render phase which determines the decorators that are called + * the render phase which determines the decorators that are called * @param printing - * if {@code true} we are printing instead of drawing to the screen + * if {@code true} we are printing instead of drawing to the screen */ private void drawPhaseDecorators(final ExecutionUnit process, final Graphics2D g2, final RenderPhase phase, - final boolean printing) { + final boolean printing) { double width = model.getProcessWidth(process) * (1 / model.getZoomFactor()); double height = model.getProcessHeight(process) * (1 / model.getZoomFactor()); int borderWidth = 2; @@ -1471,12 +1505,12 @@ private void drawPhaseDecorators(final ExecutionUnit process, final Graphics2D g * Lets the decorators draw for the specified {@link RenderPhase}. * * @param operator - * the operator which should be decorated + * the operator which should be decorated * @param g2 - * the graphics context. Each decorator gets a new context which is disposed - * afterwards + * the graphics context. Each decorator gets a new context which is disposed + * afterwards * @param printing - * if {@code true} we are printing instead of drawing to the screen + * if {@code true} we are printing instead of drawing to the screen */ private void drawOperatorDecorators(final Operator operator, final Graphics2D g2, final boolean printing) { double width = model.getProcessWidth(operator.getExecutionUnit()) * (1 / model.getZoomFactor()); @@ -1511,16 +1545,16 @@ private void drawOperatorDecorators(final Operator operator, final Graphics2D g2 * Draws text centered in the process. * * @param process - * the process in question + * the process in question * @param g2 - * the graphics context + * the graphics context * @param font * @param text * @param color * @param yOffset */ private void drawCenteredText(ExecutionUnit process, Graphics2D g2, Font font, String text, Color color, - double yOffset) { + double yOffset) { double width = model.getProcessWidth(process); double height = model.getProcessHeight(process); diff --git a/src/main/java/com/rapidminer/gui/flow/processrendering/model/ProcessRendererModel.java b/src/main/java/com/rapidminer/gui/flow/processrendering/model/ProcessRendererModel.java index 824682921..febb84a84 100644 --- a/src/main/java/com/rapidminer/gui/flow/processrendering/model/ProcessRendererModel.java +++ b/src/main/java/com/rapidminer/gui/flow/processrendering/model/ProcessRendererModel.java @@ -121,6 +121,9 @@ public final class ProcessRendererModel { /** the size of each operator/process port */ public static final int PORT_SIZE = 14; + /** The default maximum number of available undo steps */ + private static final int DEFAULT_UNDO_LIST_SIZE = 20; + /** event listener for this model */ private final EventListenerList eventListener; @@ -210,7 +213,9 @@ public final class ProcessRendererModel { */ private boolean importDragged; - /** the current mouse position relative to the process the mouse is over */ + /** + * the current mouse position relative to the process the mouse is over + */ private Point mousePositionRelativeToProcess; // initialize the filter responsible for reading/writing operator coordinates from/to XML @@ -224,9 +229,9 @@ public final class ProcessRendererModel { public ProcessRendererModel() { this.eventListener = new EventListenerList(); - this.processes = Collections.unmodifiableList(Collections. emptyList()); - this.selectedOperators = Collections.unmodifiableList(Collections. emptyList()); - this.draggedOperators = Collections.unmodifiableList(Collections. emptyList()); + this.processes = Collections.emptyList(); + this.selectedOperators = Collections.emptyList(); + this.draggedOperators = Collections.emptyList(); this.processSizes = new WeakHashMap<>(); this.portNumbers = new WeakHashMap<>(); this.snapToGrid = Boolean @@ -295,10 +300,8 @@ public void processUpdated(Process process) { @Override public void processChanged(Process process) { - if (process != null) { - if (!RapidMiner.getExecutionMode().isHeadless()) { - process.getRootOperator().setUserData(RapidMinerGUI.IS_GUI_PROCESS, new FlagUserData()); - } + if (process != null && !RapidMiner.getExecutionMode().isHeadless()) { + process.getRootOperator().setUserData(RapidMinerGUI.IS_GUI_PROCESS, new FlagUserData()); } } }); @@ -319,12 +322,11 @@ public Process getProcess() { * reset. Will also inform listeners that the process was loaded if so indicated. * * @param process - * the process to be set + * the process to be set * @param isNew - * indicates if the process should be handled as a new process + * indicates if the process should be handled as a new process * @param open - * whether the process was newly opened e.g. from a file - * + * whether the process was newly opened e.g. from a file * @since 7.5 */ public void setProcess(Process process, boolean isNew, boolean open) { @@ -338,9 +340,7 @@ public void setProcess(Process process, boolean isNew, boolean open) { displayedChain = process.getRootOperator(); fireDisplayedChainChanged(); - List newList = new ArrayList<>(1); - newList.add(displayedChain); - this.selectedOperators = Collections.unmodifiableList(newList); + this.selectedOperators = Collections.singletonList(displayedChain); fireOperatorSelectionChanged(getSelectedOperators()); if (isNew) { @@ -377,7 +377,6 @@ public boolean hasChanged() { * * @return an exception if a problem occurred * @see #setToStep(int) - * * @since 7.5 */ public Exception undo() { @@ -461,10 +460,10 @@ public List getProcesses() { * Returns the process at the specified index. * * @param index - * the index of the process to return. If index is invalid, throws + * the index of the process to return. If index is invalid, throws * @return the currently displayed process at the specified index * @throws IndexOutOfBoundsException - * if index < 0 or index >= length + * if index < 0 or index >= length */ public ExecutionUnit getProcess(int index) throws ArrayIndexOutOfBoundsException { return getProcesses().get(index); @@ -474,9 +473,8 @@ public ExecutionUnit getProcess(int index) throws ArrayIndexOutOfBoundsException * Returns the index of the given process. * * @param process - * the process for which the index should be retrieved - * @return the index of the process starting with {@code 0} or {@code -1} if the process is not - * part of {@link #getProcesses()} + * the process for which the index should be retrieved + * @return the index of the process starting with {@code 0} or {@code -1} if the process is not part of {@link #getProcesses()} */ public int getProcessIndex(ExecutionUnit process) { return getProcesses().indexOf(process); @@ -486,7 +484,7 @@ public int getProcessIndex(ExecutionUnit process) { * Sets the currently displayed processes. * * @param processes - * the new processes to display + * the new processes to display */ public void setProcesses(List processes) { if (processes == null) { @@ -509,7 +507,7 @@ public OperatorChain getDisplayedChain() { * to trigger the event. * * @param displayedChain - * the new operator chain to display + * the new operator chain to display */ public void setDisplayedChain(OperatorChain displayedChain) { if (displayedChain == null) { @@ -525,7 +523,7 @@ public void setDisplayedChain(OperatorChain displayedChain) { * the change. Will do nothing if the operator chain is already displayed. Convenience method. * * @param displayedChain - * the new chain to display + * the new chain to display * @since 7.5 */ public void setDisplayedChainAndFire(OperatorChain displayedChain) { @@ -550,14 +548,14 @@ public List getSelectedOperators() { * Clears the operator selection. */ public void clearOperatorSelection() { - this.selectedOperators = Collections.unmodifiableList(Collections. emptyList()); + this.selectedOperators = Collections.emptyList(); } /** * Adds the given operator to the currently selected operators. * * @param selectedOperator - * this operator is added to the list of currently selected operators + * this operator is added to the list of currently selected operators */ public void addOperatorToSelection(Operator selectedOperator) { List newList = new ArrayList<>(getSelectedOperators().size() + 1); @@ -571,7 +569,7 @@ public void addOperatorToSelection(Operator selectedOperator) { * not selected, does nothing. * * @param selectedOperator - * this operator is removed from the list of currently selected operators + * this operator is removed from the list of currently selected operators */ public void removeOperatorFromSelection(Operator selectedOperator) { List newList = new ArrayList<>(getSelectedOperators()); @@ -583,7 +581,7 @@ public void removeOperatorFromSelection(Operator selectedOperator) { * Adds the given operators to the currently selected operators. * * @param selectedOperators - * these operators are added to the list of currently selected operators + * these operators are added to the list of currently selected operators */ public void addOperatorsToSelection(List selectedOperators) { List newList = new ArrayList<>(getSelectedOperators().size() + selectedOperators.size()); @@ -605,7 +603,7 @@ public List getDraggedOperators() { * Sets the given operators as the currently dragged operators. * * @param draggedOperators - * these operators are set as the currently dragged operators + * these operators are set as the currently dragged operators */ public void setDraggedOperators(Collection draggedOperators) { List newList = new ArrayList<>(draggedOperators.size()); @@ -617,7 +615,7 @@ public void setDraggedOperators(Collection draggedOperators) { * Clears the dragged operators. */ public void clearDraggedOperators() { - this.draggedOperators = Collections.unmodifiableList(Collections. emptyList()); + this.draggedOperators = Collections.emptyList(); } /** @@ -633,7 +631,7 @@ public boolean isSnapToGrid() { * Sets whether operators snap to a grid or not. * * @param snapToGrid - * whether operators should snap to a grid or not + * whether operators should snap to a grid or not */ public void setSnapToGrid(boolean snapToGrid) { this.snapToGrid = snapToGrid; @@ -652,7 +650,7 @@ public boolean isDragStarted() { * Sets whether a drag operation (operator or repository entry) is in progress. * * @param dragStarted - * {@code true} if dragging is in progress; {@code false} otherwise + * {@code true} if dragging is in progress; {@code false} otherwise */ public void setDragStarted(boolean dragStarted) { this.dragStarted = dragStarted; @@ -673,7 +671,7 @@ public boolean isDropTargetSet() { * not supported. * * @param dropTargetSet - * {@code true} if a valid drop target was set; {@code false} otherwise + * {@code true} if a valid drop target was set; {@code false} otherwise */ public void setDropTargetSet(boolean dropTargetSet) { this.dropTargetSet = dropTargetSet; @@ -692,8 +690,8 @@ public boolean isOperatorSourceHovered() { * Sets whether an an operator source (tree, WoC, ...) is hovered or not. * * @param operatorSourceHovered - * {@code true} if a an operator source (tree, WoC, ...) is hovered; {@code false} - * otherwise + * {@code true} if a an operator source (tree, WoC, ...) is hovered; {@code false} + * otherwise */ public void setOperatorSourceHovered(boolean operatorSourceHovered) { this.operatorSourceHovered = operatorSourceHovered; @@ -703,7 +701,7 @@ public void setOperatorSourceHovered(boolean operatorSourceHovered) { * Sets the current mouse position over the process renderer. Can be {@code null}. * * @param currentMousePosition - * the position or {@code null} if it is not over the renderer + * the position or {@code null} if it is not over the renderer */ public void setCurrentMousePosition(Point currentMousePosition) { this.currentMousePosition = currentMousePosition; @@ -733,7 +731,7 @@ public boolean isImportDragged() { * import is accepted. * * @param importDragged - * {@code true} if the import would be accepted; {@code false} otherwise + * {@code true} if the import would be accepted; {@code false} otherwise */ public void setImportDragged(boolean importDragged) { this.importDragged = importDragged; @@ -752,7 +750,7 @@ public OutputPort getSelectedConnectionSource() { * Sets the selected connection source port. * * @param selectedConnectionSource - * the connection source port or {@code null} + * the connection source port or {@code null} */ public void setSelectedConnectionSource(OutputPort selectedConnectionSource) { this.selectedConnectionSource = selectedConnectionSource; @@ -771,7 +769,7 @@ public Port getConnectingPortSource() { * Sets the connection source port of the connection currently being created. * * @param connectingPortSource - * the source port of the connection currently being created or {@code null} + * the source port of the connection currently being created or {@code null} */ public void setConnectingPortSource(Port connectingPortSource) { this.connectingPortSource = connectingPortSource; @@ -790,7 +788,7 @@ public int getHoveringProcessIndex() { * Sets the index of the process over which the mosue currently hovers. * * @param hoveringProcessIndex - * the hovered process index + * the hovered process index */ public void setHoveringProcessIndex(int hoveringProcessIndex) { this.hoveringProcessIndex = hoveringProcessIndex; @@ -809,7 +807,7 @@ public Port getHoveringPort() { * Sets the operator over which the mouse hovers. * * @param hoveringOperator - * the operator under the mouse or {@code null} + * the operator under the mouse or {@code null} */ public void setHoveringOperator(Operator hoveringOperator) { this.hoveringOperator = hoveringOperator; @@ -828,7 +826,7 @@ public Operator getHoveringOperator() { * Sets the {@link OutputPort} of the connection over which the mouse hovers. * * @param hoveringConnectionSource - * the output port of the connection under the mouse or {@code null} + * the output port of the connection under the mouse or {@code null} */ public void setHoveringConnectionSource(OutputPort hoveringConnectionSource) { this.hoveringConnectionSource = hoveringConnectionSource; @@ -847,7 +845,7 @@ public OutputPort getHoveringConnectionSource() { * Sets the port over which the mouse hovers. * * @param hoveringPort - * the port under the mouse or {@code null} + * the port under the mouse or {@code null} */ public void setHoveringPort(Port hoveringPort) { this.hoveringPort = hoveringPort; @@ -866,7 +864,7 @@ public Rectangle2D getSelectionRectangle() { * Sets the rectangle which represents the current selection box of the user. * * @param selectionRectangle - * the selection rectangle or {@code null} + * the selection rectangle or {@code null} */ public void setSelectionRectangle(Rectangle2D selectionRectangle) { this.selectionRectangle = selectionRectangle; @@ -876,7 +874,7 @@ public void setSelectionRectangle(Rectangle2D selectionRectangle) { * Returns the size of the given process. * * @param process - * the size of this process is returned + * the size of this process is returned * @return the size of the specified process or {@code null} */ public Dimension getProcessSize(ExecutionUnit process) { @@ -899,7 +897,7 @@ public Dimension getProcessSize(ExecutionUnit process) { * {@link #getProcessSize(ExecutionUnit)}. * * @param process - * the process for which the width should be returned + * the process for which the width should be returned * @return the width or -1 if no process size has been stored */ public double getProcessWidth(ExecutionUnit process) { @@ -928,7 +926,7 @@ public double getZoomFactor() { * nothing. * * @param zoomFactor - * factor in {@link #ZOOM_FACTORS} + * factor in {@link #ZOOM_FACTORS} */ public void setZoomFactor(double zoomFactor) { if (getZoomFactor() == zoomFactor) { @@ -946,7 +944,6 @@ public void setZoomFactor(double zoomFactor) { } /** - * * @return {@code true} if it is still possible to zoom in */ public boolean canZoomIn() { @@ -954,7 +951,6 @@ public boolean canZoomIn() { } /** - * * @return {@code true} if it is still possible to zoom out */ public boolean canZoomOut() { @@ -962,9 +958,7 @@ public boolean canZoomOut() { } /** - * - * @return {@code true} if it is possible to reset the zoom (aka the process is currently zoomed - * in/out) + * @return {@code true} if it is possible to reset the zoom (aka the process is currently zoomed in/out) */ public boolean canZoomReset() { return zoomIndex != ORIGINAL_ZOOM_INDEX; @@ -1001,9 +995,9 @@ public void resetZoom() { * {@code null} for the specified process, does nothing. * * @param process - * the process for which the height should be set + * the process for which the height should be set * @param width - * the new width + * the new width */ public void setProcessWidth(ExecutionUnit process, double width) { if (process == null) { @@ -1022,7 +1016,7 @@ public void setProcessWidth(ExecutionUnit process, double width) { * of {@link #getProcessSize(ExecutionUnit)}. * * @param process - * the process for which the height should be returned + * the process for which the height should be returned * @return the height or -1 if no process size has been stored */ public double getProcessHeight(ExecutionUnit process) { @@ -1041,9 +1035,9 @@ public double getProcessHeight(ExecutionUnit process) { * {@code null} for the specified process, does nothing. * * @param process - * the process for which the height should be set + * the process for which the height should be set * @param height - * the new height + * the new height */ public void setProcessHeight(ExecutionUnit process, double height) { if (process == null) { @@ -1061,9 +1055,9 @@ public void setProcessHeight(ExecutionUnit process, double height) { * Sets the size of the given process. * * @param process - * the size of this process is stored + * the size of this process is stored * @param size - * the size of the specified process + * the size of the specified process */ public void setProcessSize(ExecutionUnit process, Dimension size) { if (process == null) { @@ -1079,10 +1073,10 @@ public void setProcessSize(ExecutionUnit process, Dimension size) { * Returns a {@link Rectangle2D} representing the given {@link Operator}. * * @param op - * the operator in question + * the operator in question * @return the rectangle. Can return {@code null} but only if the operator has not been added to - * the {@link com.rapidminer.gui.flow.processrendering.view.ProcessRendererView - * ProcessRendererView}. + * the {@link com.rapidminer.gui.flow.processrendering.view.ProcessRendererView + * ProcessRendererView}. */ public Rectangle2D getOperatorRect(Operator op) { return ProcessLayoutXMLFilter.lookupOperatorRectangle(op); @@ -1092,7 +1086,7 @@ public Rectangle2D getOperatorRect(Operator op) { * Returns the {@link WorkflowAnnotations} container for the given {@link Operator}. * * @param op - * the operator in question + * the operator in question * @return the container. Can be {@code null} if no annotations exist for this operator */ public WorkflowAnnotations getOperatorAnnotations(Operator op) { @@ -1103,7 +1097,7 @@ public WorkflowAnnotations getOperatorAnnotations(Operator op) { * Removes the given {@link OperatorAnnotation}. * * @param anno - * the annotation to remove + * the annotation to remove */ public void removeOperatorAnnotation(OperatorAnnotation anno) { AnnotationProcessXMLFilter.removeOperatorAnnotation(anno); @@ -1113,7 +1107,7 @@ public void removeOperatorAnnotation(OperatorAnnotation anno) { * Adds the given {@link OperatorAnnotation}. * * @param anno - * the annotation to add + * the annotation to add */ public void addOperatorAnnotation(OperatorAnnotation anno) { AnnotationProcessXMLFilter.addOperatorAnnotation(anno); @@ -1123,7 +1117,7 @@ public void addOperatorAnnotation(OperatorAnnotation anno) { * Returns the {@link WorkflowAnnotations} container for the given {@link ExecutionUnit}. * * @param process - * the process in question + * the process in question * @return the container. Can be {@code null} if no annotations exist for this process */ public WorkflowAnnotations getProcessAnnotations(ExecutionUnit process) { @@ -1134,7 +1128,7 @@ public WorkflowAnnotations getProcessAnnotations(ExecutionUnit process) { * Removes the given {@link ProcessAnnotation}. * * @param anno - * the annotation to remove + * the annotation to remove */ public void removeProcessAnnotation(ProcessAnnotation anno) { AnnotationProcessXMLFilter.removeProcessAnnotation(anno); @@ -1144,7 +1138,7 @@ public void removeProcessAnnotation(ProcessAnnotation anno) { * Adds the given {@link ProcessAnnotation}. * * @param anno - * the annotation to add + * the annotation to add */ public void addProcessAnnotation(ProcessAnnotation anno) { AnnotationProcessXMLFilter.addProcessAnnotation(anno); @@ -1154,7 +1148,7 @@ public void addProcessAnnotation(ProcessAnnotation anno) { * Returns the {@link ProcessBackgroundImage} for the given {@link ExecutionUnit}. * * @param process - * the process in question + * the process in question * @return the background image. Can be {@code null} if none is set for this process */ public ProcessBackgroundImage getBackgroundImage(ExecutionUnit process) { @@ -1165,7 +1159,7 @@ public ProcessBackgroundImage getBackgroundImage(ExecutionUnit process) { * Removes the given {@link ProcessBackgroundImage}. * * @param process - * the process for which to remove the background image + * the process for which to remove the background image */ public void removeBackgroundImage(ExecutionUnit process) { BackgroundImageProcessXMLFilter.removeBackgroundImage(process); @@ -1175,7 +1169,7 @@ public void removeBackgroundImage(ExecutionUnit process) { * Sets the given {@link ProcessBackgroundImage}. * * @param image - * the image to add + * the image to add */ public void setBackgroundImage(ProcessBackgroundImage image) { BackgroundImageProcessXMLFilter.setBackgroundImage(image); @@ -1185,7 +1179,7 @@ public void setBackgroundImage(ProcessBackgroundImage image) { * Returns the number of ports for the given {@link Operator}. * * @param op - * the operator in question + * the operator in question * @return the number of ports or {@code null} if they have not yet been stored */ public Integer getNumberOfPorts(Operator op) { @@ -1196,9 +1190,9 @@ public Integer getNumberOfPorts(Operator op) { * Sets the number of ports for the given {@link Operator}. * * @param op - * the operator in question + * the operator in question * @param number - * the number of ports or {@code null} + * the number of ports or {@code null} */ public Integer setNumberOfPorts(Operator op, Integer number) { return portNumbers.put(op, number); @@ -1209,9 +1203,9 @@ public Integer setNumberOfPorts(Operator op, Integer number) { * to match the existing ports! * * @param op - * the operator for which the rectangle should be set + * the operator for which the rectangle should be set * @param rect - * the rectangle representing position and size of operator + * the rectangle representing position and size of operator */ public void setOperatorRect(Operator op, Rectangle2D rect) { if (op == null) { @@ -1234,7 +1228,7 @@ public void setOperatorRect(Operator op, Rectangle2D rect) { * Returns the spacing of the specified {@link Port}. * * @param port - * the port in question + * the port in question * @return the additional spacing before this port */ public int getPortSpacing(Port port) { @@ -1245,9 +1239,9 @@ public int getPortSpacing(Port port) { * Sets the spacing of the specified {@link Port}. * * @param port - * the port in question + * the port in question * @param spacing - * the additional spacing before the port + * the additional spacing before the port */ public void setPortSpacing(Port port, int spacing) { if (port == null) { @@ -1260,7 +1254,7 @@ public void setPortSpacing(Port port, int spacing) { * Resets the spacing of the specified {@link Port} to the default value. * * @param port - * the port in question + * the port in question */ public void resetPortSpacing(Port port) { if (port == null) { @@ -1273,7 +1267,7 @@ public void resetPortSpacing(Port port) { * Looks up the view position of the specified {@link OperatorChain}. * * @param chain - * The operator chain. + * The operator chain. * @return The position or null. * @since 7.5 */ @@ -1285,9 +1279,9 @@ public Point getOperatorChainPosition(OperatorChain chain) { * Sets the view position of the specified {@link OperatorChain}. * * @param chain - * The operator chain. + * The operator chain. * @param position - * The center position. + * The center position. * @since 7.5 */ public void setOperatorChainPosition(OperatorChain chain, Point position) { @@ -1301,7 +1295,7 @@ public void setOperatorChainPosition(OperatorChain chain, Point position) { * Resets the view position of the specified {@link OperatorChain}. * * @param chain - * The operator chain. + * The operator chain. * @since 7.5 */ public void resetOperatorChainPosition(OperatorChain chain) { @@ -1315,7 +1309,7 @@ public void resetOperatorChainPosition(OperatorChain chain) { * Looks up the zoom of the specified {@link OperatorChain}. * * @param chain - * The operator chain. + * The operator chain. * @return The position or null. * @since 7.5 */ @@ -1327,9 +1321,9 @@ public Double getOperatorChainZoom(OperatorChain chain) { * Sets the zoom of the specified {@link OperatorChain}. * * @param chain - * The operator chain. + * The operator chain. * @param zoom - * The zoom. + * The zoom. * @since 7.5 */ public void setOperatorChainZoom(OperatorChain chain, Double zoom) { @@ -1343,7 +1337,7 @@ public void setOperatorChainZoom(OperatorChain chain, Double zoom) { * Resets the zoom of the specified {@link OperatorChain}. * * @param chain - * The operator chain. + * The operator chain. * @since 7.5 */ public void resetOperatorChainZoom(OperatorChain chain) { @@ -1357,7 +1351,7 @@ public void resetOperatorChainZoom(OperatorChain chain) { * Looks up the scroll position of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain. + * The operator chain. * @return The scroll position or null * @since 7.5 */ @@ -1369,9 +1363,9 @@ public Point getScrollPosition(OperatorChain operatorChain) { * Sets the scroll position of the specified {@link OperatorChain}. * * @param operatorChain - * The operator. + * The operator. * @param scrollPos - * The scroll position. + * The scroll position. * @since 7.5 */ public void setScrollPosition(OperatorChain operatorChain, Point scrollPos) { @@ -1382,7 +1376,7 @@ public void setScrollPosition(OperatorChain operatorChain, Point scrollPos) { * Resets the scroll position of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain. + * The operator chain. * @since 7.5 */ public void resetScrollPosition(OperatorChain operatorChain) { @@ -1393,7 +1387,7 @@ public void resetScrollPosition(OperatorChain operatorChain) { * Looks up the scroll process index of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain. + * The operator chain. * @return The index or null * @since 7.5 */ @@ -1405,9 +1399,9 @@ public Double getScrollIndex(OperatorChain operatorChain) { * Sets the scroll process index of the specified {@link OperatorChain}. * * @param operatorChain - * The operator. + * The operator. * @param index - * The process index. + * The process index. * @since 7.5 */ public void setScrollIndex(OperatorChain operatorChain, Double index) { @@ -1418,7 +1412,7 @@ public void setScrollIndex(OperatorChain operatorChain, Double index) { * Resets the scroll process index of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain. + * The operator chain. * @since 7.5 */ public void resetScrollIndex(OperatorChain operatorChain) { @@ -1430,7 +1424,7 @@ public void resetScrollIndex(OperatorChain operatorChain) { * restored via undo/redo and should not be scrolled to. * * @param operator - * the operator + * the operator * @return if the flag was set * @since 7.5 */ @@ -1443,7 +1437,7 @@ public boolean getRestore(Operator operator) { * restored via undo/redo and should not be scrolled to. * * @param operator - * the operator + * the operator * @since 7.5 */ public void setRestore(Operator operator) { @@ -1455,7 +1449,7 @@ public void setRestore(Operator operator) { * should be scrolled to when selected. * * @param operator - * The operator + * The operator * @since 7.5 */ public void resetRestore(Operator operator) { @@ -1475,7 +1469,7 @@ public Point getMousePositionRelativeToProcess() { * Sets the {@link Point} the mouse is at relative to the process it currently is over. * * @param mousePositionRelativeToProcess - * the point or {@code null} + * the point or {@code null} */ public void setMousePositionRelativeToProcess(Point mousePositionRelativeToProcess) { this.mousePositionRelativeToProcess = mousePositionRelativeToProcess; @@ -1486,7 +1480,7 @@ public void setMousePositionRelativeToProcess(Point mousePositionRelativeToProce * model. * * @param listener - * the listener instance to add + * the listener instance to add */ public void registerEventListener(final ProcessRendererEventListener listener) { if (listener == null) { @@ -1499,7 +1493,7 @@ public void registerEventListener(final ProcessRendererEventListener listener) { * Removes the {@link ProcessRendererEventListener} from this model. * * @param listener - * the listener instance to remove + * the listener instance to remove */ public void removeEventListener(final ProcessRendererEventListener listener) { if (listener == null) { @@ -1605,9 +1599,9 @@ public void fireProcessSizeChanged() { * process position should be the (new) center position. * * @param center - * the (new) center point + * the (new) center point * @param index - * the (new) process index + * the (new) process index * @since 7.5 */ public void prepareProcessZoomWillChange(Point center, int index) { @@ -1633,7 +1627,7 @@ public void fireMiscChanged() { * Fire when an operator has been moved. * * @param operator - * the moved operator + * the moved operator */ public void fireOperatorMoved(Operator operator) { List list = new LinkedList<>(); @@ -1645,7 +1639,7 @@ public void fireOperatorMoved(Operator operator) { * Fire when operators have been moved. * * @param operators - * a collection of moved operators + * a collection of moved operators */ public void fireOperatorsMoved(Collection operators) { fireOperatorsChanged(OperatorEvent.OPERATORS_MOVED, operators); @@ -1655,7 +1649,7 @@ public void fireOperatorsMoved(Collection operators) { * Fire when the operator selection has changed. * * @param operators - * a collection of selected operators + * a collection of selected operators */ public void fireOperatorSelectionChanged(Collection operators) { fireOperatorsChanged(OperatorEvent.SELECTED_OPERATORS_CHANGED, operators); @@ -1665,7 +1659,7 @@ public void fireOperatorSelectionChanged(Collection operators) { * Fire when the number of ports for operators has changed. * * @param operators - * a collection of operators which had their ports changed + * a collection of operators which had their ports changed */ public void firePortsChanged(Collection operators) { fireOperatorsChanged(OperatorEvent.PORTS_CHANGED, operators); @@ -1675,7 +1669,7 @@ public void firePortsChanged(Collection operators) { * Fire when an annotation has been moved. * * @param anno - * the moved annotation + * the moved annotation */ public void fireAnnotationMoved(WorkflowAnnotation anno) { List list = new LinkedList<>(); @@ -1687,7 +1681,7 @@ public void fireAnnotationMoved(WorkflowAnnotation anno) { * Fire when annotations have been moved. * * @param annotations - * the moved annotations + * the moved annotations */ public void fireAnnotationsMoved(Collection annotations) { fireAnnotationsChanged(AnnotationEvent.ANNOTATIONS_MOVED, annotations); @@ -1697,7 +1691,7 @@ public void fireAnnotationsMoved(Collection annotations) { * Fire when an annotation has been selected. * * @param anno - * the selected annotation + * the selected annotation */ public void fireAnnotationSelected(WorkflowAnnotation anno) { List list = new LinkedList<>(); @@ -1710,7 +1704,7 @@ public void fireAnnotationSelected(WorkflowAnnotation anno) { * repaint. * * @param anno - * the changed annotation, can be {@code null} + * the changed annotation, can be {@code null} */ public void fireAnnotationMiscChanged(WorkflowAnnotation anno) { List list = new LinkedList<>(); @@ -1730,7 +1724,7 @@ private void addToUndoList(final boolean viewSwitch) { return; } String maxSizeProperty = ParameterService.getParameterValue(RapidMinerGUI.PROPERTY_RAPIDMINER_GUI_UNDOLIST_SIZE); - int maxSize = 20; + int maxSize = DEFAULT_UNDO_LIST_SIZE; try { if (maxSizeProperty != null) { maxSize = Integer.parseInt(maxSizeProperty); @@ -1785,7 +1779,8 @@ private Exception setToStep(int index) { if (!stateXML.equals(currentXML)) { process = undoManager.restoreProcess(index); process.setProcessLocation(procLoc); - hasChanged = true; + // check whether the current xml corresponds to the saved one + hasChanged = procLoc == null || !process.getRootOperator().getXML(false).equals(procLoc.getRawXML()); fireProcessChanged(); } @@ -1825,7 +1820,7 @@ private void resetUndo() { * Fires the given {@link ModelEvent}. * * @param type - * the event type + * the event type */ private void fireModelChanged(final ModelEvent type) { Object[] listeners = eventListener.getListenerList(); @@ -1842,9 +1837,9 @@ private void fireModelChanged(final ModelEvent type) { * Fires the given {@link OperatorEvent} with the affected {@link Operator}s. * * @param type - * the event type + * the event type * @param operators - * the affected operators + * the affected operators */ private void fireOperatorsChanged(final OperatorEvent type, Collection operators) { Object[] listeners = eventListener.getListenerList(); @@ -1861,9 +1856,9 @@ private void fireOperatorsChanged(final OperatorEvent type, Collection * Fires the given {@link AnnotationEvent} with the affected {@link WorkflowAnnotation}s. * * @param type - * the event type + * the event type * @param annotations - * the affected annotations + * the affected annotations */ private void fireAnnotationsChanged(final AnnotationEvent type, Collection annotations) { Object[] listeners = eventListener.getListenerList(); @@ -1945,7 +1940,7 @@ private void fireProcessStored() { * {@link NewProcessUndoManager}. This should be called if the model is no longer used and is * expected to be garbage collected. It can not be used reliably after this call. * - * @since 8.1 + * @since 8.2 */ public void dispose() { undoManager.reset(); diff --git a/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererController.java b/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererController.java index 106ebfe75..606c14f25 100644 --- a/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererController.java +++ b/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererController.java @@ -23,7 +23,6 @@ import java.awt.Point; import java.awt.Shape; import java.awt.Stroke; -import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ActionEvent; @@ -33,6 +32,7 @@ import java.awt.geom.Rectangle2D; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -40,7 +40,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; - +import java.util.function.BiFunction; +import java.util.stream.Stream; import javax.swing.JScrollPane; import javax.swing.JViewport; import javax.swing.SwingConstants; @@ -51,6 +52,7 @@ import com.mxgraph.util.mxRectangle; import com.mxgraph.view.mxGraph; import com.rapidminer.gui.RapidMinerGUI; +import com.rapidminer.gui.dnd.RepositoryLocationList; import com.rapidminer.gui.dnd.TransferableOperator; import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotation; import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotations; @@ -65,10 +67,10 @@ import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.OutputPort; import com.rapidminer.operator.ports.Port; +import com.rapidminer.operator.ports.PortOwner; import com.rapidminer.operator.ports.Ports; import com.rapidminer.operator.ports.metadata.CompatibilityLevel; import com.rapidminer.operator.ports.metadata.MetaData; -import com.rapidminer.repository.Entry; import com.rapidminer.repository.Folder; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; @@ -111,7 +113,7 @@ public ProcessRendererController(ProcessRendererView view, ProcessRendererModel * algorithm. * * @param process - * the operators of this process will be arranged + * the operators of this process will be arranged */ public void autoArrange(final ExecutionUnit process) { List list = new ArrayList<>(1); @@ -120,11 +122,10 @@ public void autoArrange(final ExecutionUnit process) { } /** - * Automatically arranges the operators in the specified process according to a layouting - * algorithm. + * Automatically arranges the {@link Operator Operators} in the specified {@link ExecutionUnit ExecutionUnits} according to a layouting algorithm. * - * @param process - * the operators of this process will be arranged + * @param processes + * the operators of these processes will be arranged */ public void autoArrange(final List processes) { int unitNumber = processes.size(); @@ -227,33 +228,15 @@ public synchronized void autoFit() { /** * Calculates the position of an operator. * - * @param op * @param index + * the operators index in the surrounding {@link ExecutionUnit} * @param setPosition - * If {@code true}, will actually change the position of the operator, otherwise will - * not move the operator + * If {@code true}, will actually change the position of the operator, otherwise will not move the operator * @return the position and size of the operator, never {@code null} + * @see ProcessDrawUtils#autoPosition(Operator, int, ProcessRendererModel) ProcessDrawUtils#autoPosition(op, index, model) */ public Rectangle2D autoPosition(Operator op, int index, boolean setPosition) { - int maxPerRow = (int) Math.max(1, - Math.floor(model.getProcessWidth(op.getExecutionUnit()) / (ProcessDrawer.GRID_AUTOARRANGE_WIDTH + 10))); - int col = index % maxPerRow; - int row = index / maxPerRow; - Rectangle2D old = model.getOperatorRect(op); - - double x = col * ProcessDrawer.GRID_AUTOARRANGE_WIDTH + ProcessDrawer.GRID_X_OFFSET; - double y = ProcessDrawer.GRID_AUTOARRANGE_HEIGHT * row + ProcessDrawer.GRID_Y_OFFSET; - double width = Math.floor(old != null ? old.getWidth() : ProcessDrawer.OPERATOR_WIDTH); - double height = Math.floor(old != null ? old.getHeight() : ProcessDrawer.OPERATOR_MIN_HEIGHT); - - Rectangle2D rect; - if (model.isSnapToGrid()) { - Point snappedPoint = ProcessDrawUtils.snap(new Point2D.Double(x, y)); - rect = new Rectangle2D.Double(snappedPoint.getX(), snappedPoint.getY(), width, height); - } else { - rect = new Rectangle2D.Double(x, y, width, height); - } - + Rectangle2D rect = ProcessDrawUtils.autoPosition(op, index, model); if (setPosition) { model.setOperatorRect(op, rect); model.fireOperatorMoved(op); @@ -265,7 +248,7 @@ public Rectangle2D autoPosition(Operator op, int index, boolean setPosition) { * Opens a rename textfield at the location of the specified operator. * * @param op - * the operator to be renamed + * the operator to be renamed */ public void rename(final Operator op) { view.rename(op); @@ -275,9 +258,9 @@ public void rename(final Operator op) { * Select the given operator. * * @param op - * the operator to select + * the operator to select * @param clear - * if {@code true}, an existing selection will be cleared + * if {@code true}, an existing selection will be cleared */ void selectOperator(final Operator op, final boolean clear) { selectOperator(op, clear, false); @@ -291,7 +274,7 @@ void processDisplayedChainChanged() { List processes; OperatorChain op = model.getDisplayedChain(); if (op == null) { - processes = Collections. emptyList(); + processes = Collections.emptyList(); } else { processes = new LinkedList<>(op.getSubprocesses()); } @@ -309,7 +292,7 @@ void processDisplayedChainChanged() { * Also triggers height recalculation of the operator itself. * * @param op - * the operator which had his ports changed + * the operator which had his ports changed */ void processPortsChanged(Operator op) { if (model.getOperatorRect(op) != null) { @@ -323,12 +306,11 @@ void processPortsChanged(Operator op) { * Select the given operator. * * @param op - * the operator to select + * the operator to select * @param clear - * if {@code true}, an existing selection will be cleared + * if {@code true}, an existing selection will be cleared * @param range - * if true, select interval from last already selected operator to now selected - * operator + * if true, select interval from last already selected operator to now selected operator */ void selectOperator(final Operator op, final boolean clear, final boolean range) { boolean changed = false; @@ -349,10 +331,8 @@ void selectOperator(final Operator op, final boolean clear, final boolean range) } } } - } else { - if (selectedOperators.contains(model.getDisplayedChain())) { - selectedOperators.remove(model.getDisplayedChain()); - } + } else if (selectedOperators.contains(model.getDisplayedChain())) { + selectedOperators.remove(model.getDisplayedChain()); } if (range) { int lastIndex = -1; @@ -401,11 +381,12 @@ void selectOperator(final Operator op, final boolean clear, final boolean range) * Starting from the current selection, selects the first operator in the given direction. * * @param e - * the key event which triggered the selection + * the key event which triggered the selection */ void selectInDirection(final KeyEvent e) { int keyCode = e.getKeyCode(); - if (model.getSelectedOperators().isEmpty()) { + if (model.getSelectedOperators().isEmpty() || + model.getSelectedOperators().size() == 1 && model.getSelectedOperators().get(0) == model.getDisplayedChain()) { for (ExecutionUnit unit : model.getProcesses()) { if (unit.getNumberOfOperators() > 0) { selectOperator(unit.getOperators().get(0), true); @@ -439,6 +420,7 @@ void selectInDirection(final KeyEvent e) { case KeyEvent.VK_DOWN: ok = otherPos.getMaxY() > pos.getMaxY(); break; + default: } if (ok) { double dx = otherPos.getCenterX() - pos.getCenterX(); @@ -460,7 +442,7 @@ void selectInDirection(final KeyEvent e) { * Returns whether an operator has is connected to either output or input ports. * * @param op - * the operator in question + * the operator in question * @return {@code true} if the operator has a connection; {@code false} otherwise */ boolean hasConnections(final Operator op) { @@ -481,45 +463,48 @@ boolean hasConnections(final Operator op) { * Returns whether the given {@link Transferable} can be accepted as a drop or not. * * @param t - * the transferable + * the transferable * @return {@code true} if the process renderer can handle the drop; {@code false} otherwise */ boolean canImportTransferable(final Transferable t) { - for (DataFlavor flavor : t.getTransferDataFlavors()) { - - // check if folder is being dragged. Folders cannot be dropped on the process panel - if (flavor == TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR) { - RepositoryLocation location; - try { - - // get repository location - location = (RepositoryLocation) t.getTransferData(flavor); - - // locate entry - Entry locateEntry = location.locateEntry(); - - // if entry is folder, return false - if (locateEntry instanceof Folder) { - return false; - } - - } catch (UnsupportedFlavorException e) { - } catch (IOException e) { - } catch (RepositoryException e) { + // check if folder is being dragged. Folders cannot be dropped on the process panel + Stream repositoryLocations = null; + if (t.isDataFlavorSupported(TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_LIST_FLAVOR)) { + try { + Object transferData = t.getTransferData(TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_LIST_FLAVOR); + if (transferData instanceof RepositoryLocationList) { + repositoryLocations = ((RepositoryLocationList) transferData).getAll().stream(); + } else if (transferData instanceof RepositoryLocation[]) { + repositoryLocations = Arrays.stream((RepositoryLocation[]) transferData); } + } catch (UnsupportedFlavorException | IOException ignored) { + // ignore + } + } else if (t.isDataFlavorSupported(TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR)) { + try { + repositoryLocations = Stream.of( + (RepositoryLocation) t.getTransferData(TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR)); + } catch (UnsupportedFlavorException | IOException ignored) { + // ignore } } - - return true; + // if not representing any repo locations or at least one none-folder present, return true + return repositoryLocations == null || !repositoryLocations.map(l -> { + try { + return l.locateEntry(); + } catch (RepositoryException e) { + return null; + } + }).allMatch(e -> e instanceof Folder); } /** * Connects the operators specified by the output and input port and enables them. * * @param out - * the output port + * the output port * @param in - * the input port + * the input port */ void connect(final OutputPort out, final InputPort in) { Operator inOp = in.getPorts().getOwner().getOperator(); @@ -537,9 +522,9 @@ void connect(final OutputPort out, final InputPort in) { * Ensures that the process is at least width wide. * * @param executionUnit - * the process to ensure the minimum width + * the process to ensure the minimum width * @param width - * the mininum width + * the mininum width */ void ensureWidth(final ExecutionUnit executionUnit, final int width) { Dimension old = new Dimension((int) model.getProcessWidth(executionUnit), @@ -555,9 +540,9 @@ void ensureWidth(final ExecutionUnit executionUnit, final int width) { * Ensures that the process is at least height heigh. * * @param executionUnit - * the process to ensure the minimum height - * @param width - * the mininum height + * the process to ensure the minimum height + * @param height + * the minimum height */ void ensureHeight(final ExecutionUnit executionUnit, final int height) { Dimension old = new Dimension((int) model.getProcessWidth(executionUnit), @@ -573,9 +558,9 @@ void ensureHeight(final ExecutionUnit executionUnit, final int height) { * Increases the process size if necessary for the given operator. * * @param process - * the process for which the size should be checked - * @param location - * the location which must fit inside the process + * the process for which the size should be checked + * @param rect + * the location which must fit inside the process * @return {@code true} if process was resized; {@code false} otherwise */ boolean ensureProcessSizeFits(final ExecutionUnit process, final Rectangle2D rect) { @@ -593,35 +578,32 @@ boolean ensureProcessSizeFits(final ExecutionUnit process, final Rectangle2D rec double processHeight = processSize.getHeight() * (1 / model.getZoomFactor()); double width = processWidth; double height = processHeight; - if (processSize != null) { - if (processWidth < rect.getMaxX() + ProcessDrawer.GRID_X_OFFSET) { - double diff = rect.getMaxX() + ProcessDrawer.GRID_X_OFFSET - processWidth; - if (diff > ProcessDrawer.GRID_X_OFFSET) { - width += diff; - } else { - width += ProcessDrawer.GRID_X_OFFSET; - } - needsResize = true; - } - if (processHeight < rect.getMaxY() + ProcessDrawer.GRID_Y_OFFSET) { - double diff = rect.getMaxY() + ProcessDrawer.GRID_Y_OFFSET - processHeight; - if (diff > ProcessDrawer.GRID_Y_OFFSET) { - height += diff; - } else { - height += ProcessDrawer.GRID_Y_OFFSET; - } - needsResize = true; + if (processWidth < rect.getMaxX() + ProcessDrawer.GRID_X_OFFSET) { + double diff = rect.getMaxX() + ProcessDrawer.GRID_X_OFFSET - processWidth; + if (diff > ProcessDrawer.GRID_X_OFFSET) { + width += diff; + } else { + width += ProcessDrawer.GRID_X_OFFSET; } - if (needsResize) { - model.setProcessWidth(process, width); - model.setProcessHeight(process, height); - balance(); - model.fireProcessSizeChanged(); - return true; + needsResize = true; + } + if (processHeight < rect.getMaxY() + ProcessDrawer.GRID_Y_OFFSET) { + double diff = rect.getMaxY() + ProcessDrawer.GRID_Y_OFFSET - processHeight; + if (diff > ProcessDrawer.GRID_Y_OFFSET) { + height += diff; + } else { + height += ProcessDrawer.GRID_Y_OFFSET; } + needsResize = true; } - - return false; + if (!needsResize) { + return false; + } + model.setProcessWidth(process, width); + model.setProcessHeight(process, height); + balance(); + model.fireProcessSizeChanged(); + return true; } /** @@ -629,13 +611,13 @@ boolean ensureProcessSizeFits(final ExecutionUnit process, final Rectangle2D rec * remembers the hovering port and potentially resets the hovering operator. * * @param ports - * the ports to be checked + * the ports to be checked * @param x - * the x coordinate + * the x coordinate * @param y - * the y coordinate + * the y coordinate * @return {@code true} if a port of the given list lies under the coordinates; {@code false} - * otherwise + * otherwise */ boolean checkPortUnder(final Ports ports, final int x, final int y) { for (Port port : ports.getAllPorts()) { @@ -648,10 +630,34 @@ boolean checkPortUnder(final Ports ports, final int x, final int if (dx * dx + dy * dy < 3 * ProcessDrawer.PORT_SIZE * ProcessDrawer.PORT_SIZE / 2) { if (model.getHoveringPort() != port) { model.setHoveringPort(port); - if (model.getHoveringPort().getPorts().getOwner().getOperator() == model.getDisplayedChain()) { - showStatus(I18N.getGUILabel("processRenderer.displayChain.port.hover")); + Port connectingPort = model.getConnectingPortSource(); + Port hoveringPort = model.getHoveringPort(); + PortOwner hoveringOwner = hoveringPort.getPorts().getOwner(); + if (connectingPort == null) { + if (hoveringOwner.getOperator() == model.getDisplayedChain()) { + showStatus(I18N.getGUILabel("processRenderer.displayChain.port.hover")); + } else { + showStatus(I18N.getGUILabel("processRenderer.operator.port.hover")); + } } else { - showStatus(I18N.getGUILabel("processRenderer.operator.port.hover")); + PortOwner connectingOwner = connectingPort.getPorts().getOwner(); + // different type of ports, aka input/output or output/input + if (connectingPort instanceof InputPort && hoveringPort instanceof OutputPort || + connectingPort instanceof OutputPort && hoveringPort instanceof InputPort) { + // only if ports are in the same subprocess, and either we connect (sub)process input and output ports or ports of different operators + if ((connectingOwner.getOperator() != hoveringOwner.getOperator() || model.getDisplayedChain() == connectingOwner.getOperator()) + && connectingOwner.getConnectionContext() == hoveringOwner.getConnectionContext()) { + showStatus(I18N.getGUILabel("processRenderer.operator.port.hover_connect")); + } else if (hoveringOwner.getOperator() == connectingOwner.getOperator() && + model.getDisplayedChain() != connectingOwner.getOperator()) { + // if input/output ports of same operator should be connected which is NOT currently displayed chain + showStatus(I18N.getGUILabel("processRenderer.operator.port.hover_switch_source")); + } + } else if (connectingPort != hoveringPort && (connectingPort instanceof InputPort && hoveringPort instanceof InputPort || + connectingPort instanceof OutputPort && hoveringPort instanceof OutputPort)) { + // if port is different but both are either input/output, display "switch source of connection" + showStatus(I18N.getGUILabel("processRenderer.operator.port.hover_switch_source")); + } } view.setHoveringOperator(null); model.fireMiscChanged(); @@ -666,7 +672,7 @@ boolean checkPortUnder(final Ports ports, final int x, final int * Returns the index of the specified process in the current list of processes. * * @param executionUnit - * the process we want to get the index of + * the process we want to get the index of * @return the index of the process or -1 if it is not currently displayed */ int getIndex(final ExecutionUnit executionUnit) { @@ -716,7 +722,6 @@ int getIndex(final ExecutionUnit executionUnit) { /** * Sets the initial sizes of all processes if the model does not already contain them. - * */ void setInitialSizes() { List units = model.getProcesses(); @@ -734,9 +739,9 @@ void setInitialSizes() { * connector shape. * * @param p - * the point in question + * the point in question * @param unit - * the process for which to check + * the process for which to check * @return an output port for which the point lies inside the connector or {@code null} */ OutputPort getPortForConnectorNear(final Point p, final ExecutionUnit unit) { @@ -764,9 +769,9 @@ OutputPort getPortForConnectorNear(final Point p, final ExecutionUnit unit) { * Returns the closest operator to the left of the given point and process. * * @param p - * looks for an operator to the left of this location + * looks for an operator to the left of this location * @param unit - * the process for the location + * the process for the location * @return the closest operator or {@code null} */ Operator getClosestLeftNeighbour(final Point2D p, final ExecutionUnit unit) { @@ -792,7 +797,7 @@ Operator getClosestLeftNeighbour(final Point2D p, final ExecutionUnit unit) { * Insert the specified operator into the currently hovered connection. * * @param operator - * the operator to be inserted + * the operator to be inserted */ @SuppressWarnings("deprecation") void insertIntoHoveringConnection(final Operator operator) { @@ -863,9 +868,9 @@ void insertIntoHoveringConnection(final Operator operator) { * Set spacing and reduce spacing for successor if possible. * * @param port - * the port to be moved + * the port to be moved * @param delta - * by how much the port spacing should be changed + * by how much the port spacing should be changed * @return how much the port was moved */ double shiftPortSpacing(final Port port, final double delta) { @@ -937,79 +942,44 @@ void insertIntoHoveringConnection(final Operator operator) { * data from the model, if that fails determines a position it automatically. * * @param op - * the operator for which a rectangle should be created + * the operator for which a rectangle should be created * @return the rectangle representing the operator, never {@code null} + * @see ProcessDrawUtils#createOperatorPosition(Operator, ProcessRendererModel, BiFunction) */ Rectangle2D createOperatorPosition(Operator op) { - Rectangle2D rect = model.getOperatorRect(op); - if (rect != null) { - return rect; - } - - // if connected (e.g. because inserted by quick fix), place in the middle - if (op.getInputPorts().getNumberOfPorts() > 0 && op.getOutputPorts().getNumberOfPorts() > 0 - && op.getInputPorts().getPortByIndex(0).isConnected() - && op.getOutputPorts().getPortByIndex(0).isConnected()) { - - // to avoid that this method is called again from getPortLocation() we check whether - // all children know where they are. - boolean dependenciesOk = true; - Operator sourceOp = op.getInputPorts().getPortByIndex(0).getSource().getPorts().getOwner().getOperator(); - Operator destOp = op.getOutputPorts().getPortByIndex(0).getDestination().getPorts().getOwner().getOperator(); - dependenciesOk &= sourceOp == model.getDisplayedChain() || model.getOperatorRect(sourceOp) != null; - dependenciesOk &= destOp == model.getDisplayedChain() || model.getOperatorRect(destOp) != null; - - if (dependenciesOk) { - Point2D sourcePos = ProcessDrawUtils.createPortLocation(op.getInputPorts().getPortByIndex(0).getSource(), - model); - Point2D destPos = ProcessDrawUtils.createPortLocation(op.getOutputPorts().getPortByIndex(0).getDestination(), - model); - double x = Math.floor((sourcePos.getX() + destPos.getX()) / 2 - ProcessDrawer.OPERATOR_WIDTH / 2); - double y = Math.floor((sourcePos.getY() + destPos.getY()) / 2 - ProcessDrawer.PORT_OFFSET); - rect = new Rectangle2D.Double(x, y, ProcessDrawer.OPERATOR_WIDTH, ProcessDrawer.OPERATOR_MIN_HEIGHT); - } - } - if (rect == null) { - // otherwise, or, if positions were not known in previous approach, position - // according to index - int index = 0; - ExecutionUnit unit = op.getExecutionUnit(); - if (unit != null) { - index = unit.getOperators().indexOf(op); - } - rect = autoPosition(op, index, false); - } + return ProcessDrawUtils.createOperatorPosition(op, model, ProcessRendererController::isDependenciesOk); + } - return rect; + /** + * A helper method used when {@link #createOperatorPosition(Operator) creating operator positions} to avoid that + * the method is called again from {@link ProcessDrawUtils#createPortLocation(Port, ProcessRendererModel) ProcessDrawUtils.createPortLocation}. + */ + private static boolean isDependenciesOk(Operator op, ProcessRendererModel model) { + boolean dependenciesOk = true; + // we check whether all children know where they are + Operator sourceOp = op.getInputPorts().getPortByIndex(0).getSource().getPorts().getOwner().getOperator(); + Operator destOp = op.getOutputPorts().getPortByIndex(0).getDestination().getPorts().getOwner().getOperator(); + dependenciesOk &= sourceOp == model.getDisplayedChain() || model.getOperatorRect(sourceOp) != null; + dependenciesOk &= destOp == model.getDisplayedChain() || model.getOperatorRect(destOp) != null; + return dependenciesOk; } /** * Ensures that each operator in the given {@link ExecutionUnit} has a location. * * @param unit - * the process in question + * the process in question * @return the list of operators that did not have a location and now have one */ List ensureOperatorsHaveLocation(ExecutionUnit unit) { - List movedOperators = new LinkedList<>(); - for (Operator op : unit.getOperators()) { - - // check if all operators have positions, if not, set them now - Rectangle2D rect = model.getOperatorRect(op); - if (rect == null) { - rect = createOperatorPosition(op); - model.setOperatorRect(op, rect); - movedOperators.add(op); - } - } - return movedOperators; + return ProcessDrawUtils.ensureOperatorsHaveLocation(unit, model); } /** * Shows the given message in the main GUI status bar. * * @param msg - * the message + * the message */ void showStatus(final String msg) { RapidMinerGUI.getMainFrame().getStatusBar().setSpecialText(msg); @@ -1026,7 +996,7 @@ void clearStatus() { * Creates the initial size for a process. * * @param unit - * the process for which the initial size should be created + * the process for which the initial size should be created * @return the size, never {@code null} */ private Dimension createInitialSize(final ExecutionUnit unit) { @@ -1042,14 +1012,14 @@ private Dimension createInitialSize(final ExecutionUnit unit) { /** * Moves the operators of the specified process to their new positions with an animation. * - * @param process - * the process of the operators to be moved + * @param processes + * the processes of the operators to be moved * @param newPositions - * the new position for each operator + * the new position for each operator * @param steps - * the number of movement steps to reach their new position + * the number of movement steps to reach their new position * @param time - * the time in ms for the animation + * the time in ms for the animation */ private void moveOperators(final List processes, final List> newPositions, final int steps, final int time) { @@ -1100,7 +1070,8 @@ public void actionPerformed(ActionEvent e) { // calculate new display position for operator double dx = dxs.get(i).get(op); double dy = dys.get(i).get(op); - double x, y; + double x; + double y; // during animation, we don't really care about exact positioning if (count < steps - 1) { x = currentRect.getX() + dx; @@ -1186,9 +1157,9 @@ private void balanceHeights() { * changed. * * @param executionUnit - * the process for which to set the height - * @param width - * the new height + * the process for which to set the height + * @param height + * the new height */ private void setHeight(final ExecutionUnit executionUnit, final double height) { if (model.getProcessHeight(executionUnit) != height) { @@ -1201,9 +1172,9 @@ private void setHeight(final ExecutionUnit executionUnit, final double height) { * Automatically adapts the size of the given process to fit the available space. * * @param process - * the size of this process will be adapted + * the size of this process will be adapted * @param balance - * if {@code true}, will balance the size of all processes + * if {@code true}, will balance the size of all processes */ private void autoFit(ExecutionUnit process, boolean balance) { double w = 0; diff --git a/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererMouseHandler.java b/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererMouseHandler.java index 84cccea16..3d091ccf2 100644 --- a/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererMouseHandler.java +++ b/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererMouseHandler.java @@ -28,23 +28,31 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; - +import java.util.Map.Entry; import javax.swing.JOptionPane; import javax.swing.JViewport; import javax.swing.SwingUtilities; +import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawUtils; import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawer; import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel; +import com.rapidminer.gui.properties.celleditors.value.PropertyValueCellEditor; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.operator.ExecutionUnit; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorChain; +import com.rapidminer.operator.ports.IncompatibleMDClassException; import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.OutputPort; import com.rapidminer.operator.ports.Port; import com.rapidminer.operator.ports.PortException; +import com.rapidminer.operator.ports.PortOwner; +import com.rapidminer.operator.ports.metadata.CompatibilityLevel; +import com.rapidminer.operator.ports.metadata.MetaData; +import com.rapidminer.parameter.ParameterType; import com.rapidminer.tools.I18N; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; /** @@ -109,30 +117,28 @@ public void mouseMoved(final MouseEvent e) { */ public void mouseDragged(final MouseEvent e) { // Pan viewport - if ((e.getModifiers() & InputEvent.BUTTON2_MASK) != 0) { - if (view.getParent() instanceof JViewport) { - JViewport jv = (JViewport) view.getParent(); - Point p = jv.getViewPosition(); - int newX = p.x - (e.getX() - mousePositionAtDragStart.x); - int newY = p.y - (e.getY() - mousePositionAtDragStart.y); - int maxX = view.getWidth() - jv.getWidth(); - int maxY = view.getHeight() - jv.getHeight(); - if (newX < 0) { - newX = 0; - } - if (newX > maxX) { - newX = maxX; - } - if (newY < 0) { - newY = 0; - } - if (newY > maxY) { - newY = maxY; - } - jv.setViewPosition(new Point(newX, newY)); - e.consume(); - return; + if ((e.getModifiers() & InputEvent.BUTTON2_MASK) != 0 && view.getParent() instanceof JViewport) { + JViewport jv = (JViewport) view.getParent(); + Point p = jv.getViewPosition(); + int newX = p.x - (e.getX() - mousePositionAtDragStart.x); + int newY = p.y - (e.getY() - mousePositionAtDragStart.y); + int maxX = view.getWidth() - jv.getWidth(); + int maxY = view.getHeight() - jv.getHeight(); + if (newX < 0) { + newX = 0; } + if (newX > maxX) { + newX = maxX; + } + if (newY < 0) { + newY = 0; + } + if (newY > maxY) { + newY = maxY; + } + jv.setViewPosition(new Point(newX, newY)); + e.consume(); + return; } // drag ports @@ -143,7 +149,6 @@ public void mouseDragged(final MouseEvent e) { if (model.getConnectingPortSource().getPorts().getOwner().getOperator() == model.getDisplayedChain() && e.isShiftDown()) { cancelConnectionDragging(); - connectionDraggingCanceled = true; } e.consume(); } @@ -153,14 +158,12 @@ public void mouseDragged(final MouseEvent e) { ExecutionUnit draggingInSubprocess = draggedOperatorsOrigins.keySet().iterator().next().getExecutionUnit(); Operator hoveringOperator = model.getHoveringOperator(); if (hoveringOperator != null) { - if (draggedOperatorsOrigins.size() == 1) { - if (ProcessDrawUtils.hasOperatorFreePorts(hoveringOperator)) { - int pid = controller.getIndex(draggingInSubprocess); - Point processSpace = view.toProcessSpace(e.getPoint(), pid); - if (processSpace != null) { - model.setHoveringConnectionSource( - controller.getPortForConnectorNear(processSpace, draggingInSubprocess)); - } + if (draggedOperatorsOrigins.size() == 1 && ProcessDrawUtils.hasOperatorFreePorts(hoveringOperator)) { + int pid = controller.getIndex(draggingInSubprocess); + Point processSpace = view.toProcessSpace(e.getPoint(), pid); + if (processSpace != null) { + model.setHoveringConnectionSource( + controller.getPortForConnectorNear(processSpace, draggingInSubprocess)); } } @@ -201,17 +204,13 @@ public void mouseDragged(final MouseEvent e) { double maxY = 0; // shift - for (Operator op : draggedOperatorsOrigins.keySet()) { - Rectangle2D origin = draggedOperatorsOrigins.get(op); - if (origin.getMaxX() + difX >= unitWidth) { - if (origin.getMaxX() + difX > maxX) { - maxX = origin.getMaxX() + difX; - } + for (Entry opAndRectangle : draggedOperatorsOrigins.entrySet()) { + Rectangle2D origin = opAndRectangle.getValue(); + if (origin.getMaxX() + difX >= unitWidth && origin.getMaxX() + difX > maxX) { + maxX = origin.getMaxX() + difX; } - if (origin.getMaxY() + difY >= unitHeight) { - if (origin.getMaxY() + difY > maxY) { - maxY = origin.getMaxY() + difY; - } + if (origin.getMaxY() + difY >= unitHeight && origin.getMaxY() + difY > maxY) { + maxY = origin.getMaxY() + difY; } if (origin.getMinY() + difY < 0) { @@ -222,7 +221,7 @@ public void mouseDragged(final MouseEvent e) { } Rectangle2D opPos = new Rectangle2D.Double(Math.floor(origin.getX() + difX), Math.floor(origin.getY() + difY), origin.getWidth(), origin.getHeight()); - model.setOperatorRect(op, opPos); + model.setOperatorRect(opAndRectangle.getKey(), opPos); } model.fireOperatorsMoved(draggedOperatorsOrigins.keySet()); e.consume(); @@ -265,13 +264,11 @@ public void mousePressed(final MouseEvent e) { pressedMouseButton = e.getButton(); connectionDraggingCanceled = false; - if (SwingUtilities.isLeftMouseButton(e)) { - if (model.getHoveringOperator() == null && model.getHoveringPort() == null - && model.getSelectedConnectionSource() != model.getHoveringConnectionSource()) { - model.setSelectedConnectionSource(model.getHoveringConnectionSource()); - model.fireMiscChanged(); - e.consume(); - } + if (SwingUtilities.isLeftMouseButton(e) && model.getHoveringOperator() == null && model.getHoveringPort() == null + && model.getSelectedConnectionSource() != model.getHoveringConnectionSource()) { + model.setSelectedConnectionSource(model.getHoveringConnectionSource()); + model.fireMiscChanged(); + e.consume(); } if (e.getButton() == MouseEvent.BUTTON2) { @@ -282,15 +279,7 @@ public void mousePressed(final MouseEvent e) { // disconnect when clicking with alt + left mouse on connection if (model.getHoveringConnectionSource() != null && model.getHoveringOperator() == null && SwingUtilities.isLeftMouseButton(e) && e.isAltDown()) { - OutputPort port = model.getHoveringConnectionSource(); - if (port.isConnected()) { - port.disconnect(); - model.setHoveringConnectionSource(null); - if (model.getSelectedConnectionSource() != null && model.getSelectedConnectionSource().equals(port)) { - model.setSelectedConnectionSource(null); - } - model.fireMiscChanged(); - } + ProcessRendererView.disconnectHoveredConnection(model); e.consume(); } @@ -301,7 +290,6 @@ public void mousePressed(final MouseEvent e) { // cancel if right mouse button is pressed if (SwingUtilities.isRightMouseButton(e)) { cancelConnectionDragging(); - connectionDraggingCanceled = true; e.consume(); return; } @@ -309,7 +297,6 @@ public void mousePressed(final MouseEvent e) { // cancel if any button is pressed but not over hovering port if (model.getHoveringPort() == null) { cancelConnectionDragging(); - connectionDraggingCanceled = true; e.consume(); return; } @@ -322,63 +309,39 @@ public void mousePressed(final MouseEvent e) { // Left mouse button pressed on port with alt pressed -> remove connection if (e.isAltDown()) { - if (hoveringPort instanceof OutputPort) { - if (((OutputPort) hoveringPort).isConnected()) { + if (hoveringPort.isConnected()) { + if (hoveringPort instanceof OutputPort) { ((OutputPort) hoveringPort).disconnect(); - } - } else if (hoveringPort instanceof InputPort) { - if (((InputPort) hoveringPort).isConnected()) { + } else if (hoveringPort instanceof InputPort) { ((InputPort) hoveringPort).getSource().disconnect(); } } view.repaint(); } else { // Left mouse button pressed on port -> start connecting ports - if (hoveringPort instanceof OutputPort) { - if (connectingPortSource != null && connectingPortSource instanceof InputPort) { - connectConnectingPortSourceWithHoveringPort((InputPort) connectingPortSource, - (OutputPort) hoveringPort, hoveringPort); - } else { - if (!e.isShiftDown()) { - model.setConnectingPortSource(hoveringPort); - } - } - } else if (hoveringPort instanceof InputPort) { - if (connectingPortSource != null && connectingPortSource instanceof OutputPort) { - connectConnectingPortSourceWithHoveringPort((InputPort) hoveringPort, - (OutputPort) connectingPortSource, hoveringPort); - } else { - if (!e.isShiftDown()) { - model.setConnectingPortSource(hoveringPort); - } + try { + verifyAndConnectConnectingPortSourceWithHoveringPort(); + } catch (IncompatiblePortsException ex) { + if (!e.isShiftDown()) { + model.setConnectingPortSource(hoveringPort); } } } e.consume(); - } else if (model.getHoveringOperator() == null) { + } else if (model.getHoveringOperator() == null && !e.isShiftDown() && !(SwingTools.isControlOrMetaDown(e) && e.getButton() == 1) && + (model.getSelectedOperators().isEmpty() || !model.getSelectedOperators().get(0).equals(model.getDisplayedChain()))) { // deselect unless shift is pressed - if (!e.isShiftDown() && !(SwingTools.isControlOrMetaDown(e) && e.getButton() == 1)) { - if (model.getSelectedOperators().isEmpty() || model.getSelectedOperators().size() >= 1 - && !model.getSelectedOperators().get(0).equals(model.getDisplayedChain())) { - controller.selectOperator(model.getDisplayedChain(), true); - } - } + controller.selectOperator(model.getDisplayedChain(), true); } if (hoveringPort != null) { controller.selectOperator(hoveringPort.getPorts().getOwner().getOperator(), true); pressHasSelected = true; e.consume(); - } else { - if (model.getHoveringOperator() == null) { - if (!e.isShiftDown() && !(SwingTools.isControlOrMetaDown(e) && e.getButton() == 1)) { - if (model.getSelectedOperators().isEmpty() || model.getSelectedOperators().size() >= 1 - && !model.getSelectedOperators().get(0).equals(model.getDisplayedChain())) { - controller.selectOperator(model.getDisplayedChain(), true); - pressHasSelected = true; - } - } - } + } else if (model.getHoveringOperator() == null && !e.isShiftDown() && !(SwingTools.isControlOrMetaDown(e) && e.getButton() == 1) && + (model.getSelectedOperators().isEmpty() || !model.getSelectedOperators().get(0).equals(model.getDisplayedChain()))) { + controller.selectOperator(model.getDisplayedChain(), true); + pressHasSelected = true; } if (model.getHoveringOperator() != null) { @@ -416,22 +379,22 @@ public void mousePressed(final MouseEvent e) { // WINDOWS: mouseReleased // LINUX: mousePressed // DO NOT HANDLE BACKGROUND CLICKS HERE, they are handled in a later RenderPhase - Operator clickedOperator = null; + showOperatorPopup(e, !model.getDisplayedChain().equals(getClickedOperator()) || model.getHoveringConnectionSource() != null); + } + + private Operator getClickedOperator() { if (model.getHoveringPort() != null) { - clickedOperator = model.getHoveringPort().getPorts().getOwner().getOperator(); - } else if (model.getHoveringOperator() != null) { - clickedOperator = model.getHoveringOperator(); - } else { - clickedOperator = model.getDisplayedChain(); + return model.getHoveringPort().getPorts().getOwner().getOperator(); } - if (e.isPopupTrigger() - && (!model.getDisplayedChain().equals(clickedOperator) || model.getHoveringConnectionSource() != null)) { - if (!connectionDraggingCanceled) { - if (view.showPopupMenu(e)) { - e.consume(); - return; - } - } + if (model.getHoveringOperator() != null) { + return model.getHoveringOperator(); + } + return model.getDisplayedChain(); + } + + private void showOperatorPopup(MouseEvent e, boolean condition) { + if (e.isPopupTrigger() && condition && !connectionDraggingCanceled && view.showPopupMenu(e)) { + e.consume(); } } @@ -448,22 +411,7 @@ public void mousePressedBackground(MouseEvent e) { // WINDOWS: mouseReleased // LINUX: mousePressed // ONLY HANDLE BACKGROUND CLICKS HERE - Operator clickedOperator = null; - if (model.getHoveringPort() != null) { - clickedOperator = model.getHoveringPort().getPorts().getOwner().getOperator(); - } else if (model.getHoveringOperator() != null) { - clickedOperator = model.getHoveringOperator(); - } else { - clickedOperator = model.getDisplayedChain(); - } - if (e.isPopupTrigger() && model.getDisplayedChain().equals(clickedOperator)) { - if (!connectionDraggingCanceled) { - if (view.showPopupMenu(e)) { - e.consume(); - return; - } - } - } + showOperatorPopup(e, model.getDisplayedChain().equals(getClickedOperator())); } /** @@ -484,26 +432,25 @@ public void mouseReleased(final MouseEvent e) { // cancel if right mouse button is released if (SwingUtilities.isRightMouseButton(e)) { cancelConnectionDragging(); - connectionDraggingCanceled = true; } Port hoveringPort = model.getHoveringPort(); // cancel if any button is released but not over hovering port if (hoveringPort == null) { cancelConnectionDragging(); - connectionDraggingCanceled = true; } // connect when released over hovering port if (SwingUtilities.isLeftMouseButton(e) && hoveringPort != null && !e.isAltDown()) { - if (hoveringPort instanceof InputPort && connectingPortSource instanceof OutputPort) { - connectConnectingPortSourceWithHoveringPort((InputPort) hoveringPort, (OutputPort) connectingPortSource, - hoveringPort); - } else if (hoveringPort instanceof OutputPort && connectingPortSource instanceof InputPort) { - connectConnectingPortSourceWithHoveringPort((InputPort) connectingPortSource, (OutputPort) hoveringPort, - hoveringPort); + try { + verifyAndConnectConnectingPortSourceWithHoveringPort(); + } catch (IncompatiblePortsException ex) { + if (!e.isShiftDown() && !hoveringPort.equals(connectingPortSource)) { + cancelConnectionDragging(); + } } } + e.consume(); } @@ -543,10 +490,13 @@ public void mouseReleased(final MouseEvent e) { } } model.setSelectionRectangle(null); + model.fireMiscChanged(); e.consume(); } else { if (hasDragged && draggedOperatorsOrigins != null && draggedOperatorsOrigins.size() == 1) { - controller.insertIntoHoveringConnection(model.getHoveringOperator()); + if (ProcessDrawUtils.canOperatorBeInsertedIntoConnection(model, model.getHoveringOperator())) { + controller.insertIntoHoveringConnection(model.getHoveringOperator()); + } e.consume(); } else if (!hasDragged && model.getHoveringOperator() != null && !e.isPopupTrigger() && SwingUtilities.isLeftMouseButton(e) @@ -577,25 +527,10 @@ public void mouseReleased(final MouseEvent e) { // WINDOWS: mouseReleased // LINUX: mousePressed // DO NOT HANDLE BACKGROUND CLICKS HERE, they are handled in a later RenderPhase - Operator clickedOperator = null; - if (model.getHoveringPort() != null) { - clickedOperator = model.getHoveringPort().getPorts().getOwner().getOperator(); - } else if (model.getHoveringOperator() != null) { - clickedOperator = model.getHoveringOperator(); - } else { - clickedOperator = model.getDisplayedChain(); - } - if (e.isPopupTrigger() - && (!model.getDisplayedChain().equals(clickedOperator) || model.getHoveringConnectionSource() != null)) { - if (!connectionDraggingCanceled) { - if (view.showPopupMenu(e)) { - e.consume(); - return; - } - } + showOperatorPopup(e, !model.getDisplayedChain().equals(getClickedOperator()) || model.getHoveringConnectionSource() != null); + if (!e.isConsumed()) { + view.repaint(); } - - view.repaint(); } /** @@ -611,22 +546,7 @@ public void mouseReleasedBackground(final MouseEvent e) { // WINDOWS: mouseReleased // LINUX: mousePressed // ONLY HANDLE BACKGROUND CLICKS HERE - Operator clickedOperator = null; - if (model.getHoveringPort() != null) { - clickedOperator = model.getHoveringPort().getPorts().getOwner().getOperator(); - } else if (model.getHoveringOperator() != null) { - clickedOperator = model.getHoveringOperator(); - } else { - clickedOperator = model.getDisplayedChain(); - } - if (e.isPopupTrigger() && model.getDisplayedChain().equals(clickedOperator)) { - if (!connectionDraggingCanceled) { - if (view.showPopupMenu(e)) { - e.consume(); - return; - } - } - } + showOperatorPopup(e, model.getDisplayedChain().equals(getClickedOperator())); } /** @@ -636,20 +556,61 @@ public void mouseReleasedBackground(final MouseEvent e) { */ public void mouseClicked(final MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e)) { - if (e.getClickCount() == 2) { - if (model.getHoveringOperator() != null) { - if (model.getHoveringOperator() instanceof OperatorChain) { - model.setDisplayedChainAndFire((OperatorChain) model.getHoveringOperator()); - } + if (e.getClickCount() != 2) { + return; + } + if (model.getHoveringOperator() != null) { + if (model.getHoveringOperator() instanceof OperatorChain && e.getModifiersEx() != InputEvent.ALT_DOWN_MASK) { + // dive into operator chain, unless user has pressed ALT key. ALT + double-click = activate primary parameter + ActionStatisticsCollector.getInstance().logOperatorDoubleClick(model.getHoveringOperator(), ActionStatisticsCollector.OPERATOR_ACTION_OPEN); + model.setDisplayedChainAndFire((OperatorChain) model.getHoveringOperator()); e.consume(); + return; + } + // look for a primary parameter, and activate it if found + ParameterType primaryParameter = model.getHoveringOperator().getPrimaryParameter(); + ActionStatisticsCollector.getInstance().logOperatorDoubleClick(model.getHoveringOperator(), ActionStatisticsCollector.OPERATOR_ACTION_PRIMARY_PARAMETER); + if (primaryParameter != null) { + PropertyValueCellEditor editor = RapidMinerGUI.getMainFrame().getPropertyPanel().getEditorForKey(primaryParameter.getKey()); + if (editor != null) { + editor.activate(); + e.consume(); + return; + } } } - } else if (SwingUtilities.isRightMouseButton(e)) { - if (model.getConnectingPortSource() != null) { - cancelConnectionDragging(); - connectionDraggingCanceled = true; - e.consume(); + if (model.getHoveringPort() == null) { + return; + } + Port hoveringPort = model.getHoveringPort(); + PortOwner hoveringPortOwner = hoveringPort.getPorts().getOwner(); + // should only work for yet unconnected outer ports + if (hoveringPortOwner.getPortHandler().equals(hoveringPortOwner.getOperator()) || hoveringPort.isConnected()) { + return; } + ExecutionUnit surroundingUnit = hoveringPortOwner.getOperator().getExecutionUnit(); + if (hoveringPort instanceof OutputPort) { + OutputPort hoveringOutputPort = (OutputPort) hoveringPort; + for (InputPort in : surroundingUnit.getInnerSinks().getAllPorts()) { + if (attemptConnection(hoveringOutputPort, in)) { + e.consume(); + return; + } + } + } else { + InputPort hoveringInputPort = (InputPort) hoveringPort; + for (OutputPort out : surroundingUnit.getInnerSources().getAllPorts()) { + if (attemptConnection(out, hoveringInputPort)) { + e.consume(); + return; + } + } + } + return; + } + if (SwingUtilities.isRightMouseButton(e) && model.getConnectingPortSource() != null) { + cancelConnectionDragging(); + e.consume(); } } @@ -667,7 +628,7 @@ public void mouseEntered(final MouseEvent e) {} */ public void mouseExited(final MouseEvent e) { controller.clearStatus(); - }; + } /** * Updates the currently hovered element @@ -716,10 +677,14 @@ private void updateHoveringState(final MouseEvent e) { if (model.getHoveringOperator() != op) { model.setHoveringPort(null); view.setHoveringOperator(op); - if (model.getHoveringOperator() instanceof OperatorChain) { - controller.showStatus(I18N.getGUILabel("processRenderer.displayChain.hover")); + if (model.getConnectingPortSource() == null) { + if (model.getHoveringOperator() instanceof OperatorChain) { + controller.showStatus(I18N.getGUILabel("processRenderer.displayChain.hover")); + } else { + controller.showStatus(I18N.getGUILabel("processRenderer.operator.hover")); + } } else { - controller.showStatus(I18N.getGUILabel("processRenderer.operator.hover")); + controller.showStatus(I18N.getGUILabel("processRenderer.connection.hover_cancel")); } } e.consume(); @@ -735,13 +700,43 @@ private void updateHoveringState(final MouseEvent e) { view.updateCursor(); model.fireMiscChanged(); } - if (model.getHoveringConnectionSource() != null) { + if (model.getHoveringConnectionSource() != null && model.getConnectingPortSource() == null) { controller.showStatus(I18N.getGUILabel("processRenderer.connection.hover")); + } else if (model.getConnectingPortSource() != null) { + controller.showStatus(I18N.getGUILabel("processRenderer.connection.hover_cancel")); } else { controller.clearStatus(); } } + /** + * Verifies the ports and connect with the connection source port if possible + * + * @throws IncompatiblePortsException in case the connection is not possible + */ + private void verifyAndConnectConnectingPortSourceWithHoveringPort() throws IncompatiblePortsException { + final Port connectingPortSource = model.getConnectingPortSource(); + final Port hoveringPort = model.getHoveringPort(); + final InputPort input; + final OutputPort output; + if (hoveringPort instanceof InputPort && connectingPortSource instanceof OutputPort) { + input = (InputPort) hoveringPort; + output = (OutputPort) connectingPortSource; + } else if (hoveringPort instanceof OutputPort && connectingPortSource instanceof InputPort) { + input = (InputPort) connectingPortSource; + output = (OutputPort) hoveringPort; + } else { + throw new IncompatiblePortsException(); + } + + Operator destOp = input.getPorts().getOwner().getOperator(); + Operator sourceOp = output.getPorts().getOwner().getOperator(); + // outer ports of an operator should not connect to each other + if (!destOp.equals(model.getDisplayedChain()) && destOp.equals(sourceOp)) { + throw new IncompatiblePortsException(); + } + connectConnectingPortSourceWithHoveringPort(input, output, hoveringPort); + } /** * Connects the clicked port with the connection source port. * @@ -816,7 +811,6 @@ private void connectConnectingPortSourceWithHoveringPort(final InputPort input, view.repaint(); } finally { cancelConnectionDragging(); - connectionDraggingCanceled = true; } } @@ -835,5 +829,46 @@ private boolean isDisplayChainPortDragged() { private void cancelConnectionDragging() { model.setConnectingPortSource(null); model.fireMiscChanged(); + connectionDraggingCanceled = true; } + + /** + * Connects the given ports if they are both not connected yet and the {@link MetaData} matches. If the ports were + * connected, this will also call {@link #cancelConnectionDragging()}. + * Will return {@code true} iff the ports were connected. + * + * @param out + * the output port + * @param in + * the input port + * @return {@code true} iff the ports were connected. + * @since 8.2 + */ + private boolean attemptConnection(OutputPort out, InputPort in) { + MetaData outMetaData; + try { + outMetaData = out.getMetaData(MetaData.class); + } catch (IncompatibleMDClassException e){ + //Should not happen + return false; + } + if (!in.isConnected() && !out.isConnected() && outMetaData != null && in.isInputCompatible(outMetaData, CompatibilityLevel.VERSION_5)) { + out.connectTo(in); + cancelConnectionDragging(); + return true; + } + return false; + } + + /** + * Used to identify incompatible port combinations + * + * @author Jonas Wilms-Pfau + * @see #verifyAndConnectConnectingPortSourceWithHoveringPort + * @since 8.2.0 + */ + private static class IncompatiblePortsException extends Exception { + // marker exception + } + } diff --git a/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererView.java b/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererView.java index a39829fb1..5acaab093 100644 --- a/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererView.java +++ b/src/main/java/com/rapidminer/gui/flow/processrendering/view/ProcessRendererView.java @@ -26,13 +26,11 @@ import java.awt.Point; import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; -import java.awt.event.HierarchyEvent; -import java.awt.event.HierarchyListener; +import java.awt.event.InputEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; @@ -40,13 +38,12 @@ import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Collection; -import java.util.HashMap; +import java.util.EnumMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; - import javax.swing.Action; import javax.swing.JComponent; import javax.swing.JMenu; @@ -94,6 +91,7 @@ import com.rapidminer.gui.flow.processrendering.view.actions.RenameAction; import com.rapidminer.gui.flow.processrendering.view.actions.SelectAllAction; import com.rapidminer.gui.flow.processrendering.view.components.ProcessRendererTooltipProvider; +import com.rapidminer.gui.properties.celleditors.value.PropertyValueCellEditor; import com.rapidminer.gui.tools.PrintingTools; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.ResourceMenu; @@ -112,12 +110,15 @@ import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.OutputPort; import com.rapidminer.operator.ports.Port; +import com.rapidminer.operator.ports.Ports; import com.rapidminer.operator.ports.quickfix.QuickFix; +import com.rapidminer.parameter.ParameterType; import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.SystemInfoUtilities; import com.rapidminer.tools.SystemInfoUtilities.OperatingSystem; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; /** @@ -473,7 +474,7 @@ public void mouseExited(final MouseEvent e) { return; } } - }; + } }; /** the key handler for the entire process renderer */ @@ -562,8 +563,20 @@ public void keyPressed(final KeyEvent e) { // operator phase event, no more decorator processing afterwards if (!model.getSelectedOperators().isEmpty()) { Operator selected = model.getSelectedOperators().get(0); - if (selected instanceof OperatorChain) { + if (selected instanceof OperatorChain && e.getModifiersEx() != InputEvent.ALT_DOWN_MASK) { + // dive into operator chain, unless user has pressed ALT key. ALT + ENTER = activate primary parameter + ActionStatisticsCollector.getInstance().logOperatorDoubleClick(selected, ActionStatisticsCollector.OPERATOR_ACTION_OPEN); model.setDisplayedChainAndFire((OperatorChain) selected); + } else { + // look for a primary parameter, and activate it if found + ParameterType primaryParameter = selected.getPrimaryParameter(); + ActionStatisticsCollector.getInstance().logOperatorDoubleClick(selected, ActionStatisticsCollector.OPERATOR_ACTION_PRIMARY_PARAMETER); + if (primaryParameter != null) { + PropertyValueCellEditor editor = RapidMinerGUI.getMainFrame().getPropertyPanel().getEditorForKey(primaryParameter.getKey()); + if (editor != null) { + editor.activate(); + } + } } } e.consume(); @@ -707,9 +720,9 @@ public ProcessRendererView(final ProcessRendererModel model, final ProcessPanel new OperatorAnimationProcessListener(model); // prepare event decorators for each phase - decorators = new HashMap<>(); + decorators = new EnumMap<>(RenderPhase.class); for (RenderPhase phase : RenderPhase.eventOrder()) { - decorators.put(phase, new CopyOnWriteArrayList()); + decorators.put(phase, new CopyOnWriteArrayList<>()); } overviewPanel = new OverviewPanel(this); @@ -741,13 +754,9 @@ public void modelChanged(ProcessRendererModelEvent e) { controller.autoFit(); //$FALL-THROUGH$ case PROCESS_SIZE_CHANGED: - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - updateComponentSize(); - repaint(); - } + SwingUtilities.invokeLater(() -> { + updateComponentSize(); + repaint(); }); break; case MISC_CHANGED: @@ -845,13 +854,7 @@ public void componentResized(final ComponentEvent e) { controller.autoFit(); } }); - processPanel.addHierarchyListener(new HierarchyListener() { - - @Override - public void hierarchyChanged(HierarchyEvent e) { - controller.autoFit(); - } - }); + processPanel.addHierarchyListener(e -> controller.autoFit()); // register transfer handler and drop target setTransferHandler(new ProcessRendererTransferHandler(this, model, controller)); @@ -878,9 +881,9 @@ public void handleLicenseEvent(final LicenseEvent event) { }); // add some actions to the action map of this component - ((ResourceAction) mainFrame.getActions().TOGGLE_BREAKPOINT[BreakpointListener.BREAKPOINT_AFTER]).addToActionMap(this, - WHEN_FOCUSED); - ((ResourceAction) mainFrame.getActions().TOGGLE_ACTIVATION_ITEM).addToActionMap(this, WHEN_FOCUSED); + mainFrame.getActions().TOGGLE_BREAKPOINT[BreakpointListener.BREAKPOINT_AFTER]. + addToActionMap(this, WHEN_FOCUSED); + mainFrame.getActions().TOGGLE_ACTIVATION_ITEM.addToActionMap(this, WHEN_FOCUSED); selectAllAction.addToActionMap(this, WHEN_FOCUSED); OperatorTransferHandler.addToActionMap(this); deleteSelectedAction.addToActionMap(this, "delete", WHEN_FOCUSED); @@ -1031,8 +1034,8 @@ private int getXOffset(final int processIndex) { public void processUpdated() { Operator hoveredOp = model.getHoveringOperator(); boolean hoveredOperatorFound = hoveredOp == null ? true : false; - List movedOperators = new LinkedList(); - List portChangedOperators = new LinkedList(); + List movedOperators = new LinkedList<>(); + List portChangedOperators = new LinkedList<>(); // make sure location of every opterator is set and potentially reset hovered op for (ExecutionUnit unit : model.getProcesses()) { @@ -1352,24 +1355,22 @@ public void rename(final Operator op) { add(renameField); renameField.requestFocusInWindow(); // accepting changes on enter and focus lost - renameField.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(final ActionEvent e) { - if (renameField != null) { - String name = renameField.getText().trim(); - if (name.length() > 0) { - op.rename(name); - } - remove(renameField); - renameField = null; - // this makes sure that pressing F2 afterwards works - // otherwise nothing might be focused - ProcessRendererView.this.requestFocusInWindow(); - repaint(); - } + Runnable renamer = () -> { + if (renameField == null) { + return; } - }); + String name = renameField.getText().trim(); + if (name.length() > 0) { + op.rename(name); + } + remove(renameField); + renameField = null; + // this makes sure that pressing F2 afterwards works + // otherwise nothing might be focused + ProcessRendererView.this.requestFocusInWindow(); + repaint(); + }; + renameField.addActionListener(e -> renamer.run()); renameField.addFocusListener(new FocusAdapter() { @Override @@ -1378,19 +1379,7 @@ public void focusLost(final FocusEvent e) { if (e.isTemporary()) { return; } - - if (renameField != null) { - String name = renameField.getText().trim(); - if (name.length() > 0) { - op.rename(name); - } - remove(renameField); - renameField = null; - // this makes sure that pressing F2 afterwards works - // otherwise nothing might be focused - ProcessRendererView.this.requestFocusInWindow(); - repaint(); - } + renamer.run(); } }); // ignore changes on escape @@ -1452,6 +1441,7 @@ boolean showPopupMenu(final MouseEvent e) { // port or not port, that is the question final Port hoveringPort = model.getHoveringPort(); + OperatorChain displayedChain = model.getDisplayedChain(); if (hoveringPort != null) { // add port actions final IOObject data = hoveringPort.getAnyDataOrNull(); @@ -1493,55 +1483,33 @@ public void loggedActionPerformed(final ActionEvent e) { @Override public void loggedActionPerformed(final ActionEvent e) { - if (hoveringPort != null) { - if (hoveringPort.isConnected()) { - if (hoveringPort instanceof OutputPort) { - ((OutputPort) hoveringPort).disconnect(); - } else { - ((InputPort) hoveringPort).getSource().disconnect(); - } + if (hoveringPort.isConnected()) { + if (hoveringPort instanceof OutputPort) { + ((OutputPort) hoveringPort).disconnect(); + } else { + ((InputPort) hoveringPort).getSource().disconnect(); } } } }); } - if (model.getDisplayedChain() instanceof ProcessRootOperator) { - if (hoveringPort.getPorts() == model.getDisplayedChain().getSubprocess(0).getInnerSources() - || hoveringPort.getPorts() == model.getDisplayedChain().getSubprocess(0).getInnerSinks()) { - menu.add(new ConnectPortToRepositoryAction(hoveringPort)); - } + Ports ports = hoveringPort.getPorts(); + ExecutionUnit subprocess = displayedChain.getSubprocess(0); + if (displayedChain instanceof ProcessRootOperator && + (ports == subprocess.getInnerSources() || ports == subprocess.getInnerSinks())) { + menu.add(new ConnectPortToRepositoryAction(hoveringPort)); } firePortMenuWillOpen(menu, hoveringPort); } else if (model.getHoveringOperator() == null && model.getHoveringConnectionSource() != null) { // right-clicked a connection spline - final Port port = model.getHoveringConnectionSource(); menu.add(new ResourceAction(true, "delete_connection") { private static final long serialVersionUID = 1L; @Override public void loggedActionPerformed(final ActionEvent e) { - if (port != null) { - if (port.isConnected()) { - OutputPort disconnectedPort; - if (port instanceof OutputPort) { - ((OutputPort) port).disconnect(); - disconnectedPort = (OutputPort) port; - } else { - ((InputPort) port).getSource().disconnect(); - disconnectedPort = ((InputPort) port).getSource(); - } - if (port.equals(disconnectedPort)) { - model.setHoveringConnectionSource(null); - } - if (model.getSelectedConnectionSource() != null - && model.getSelectedConnectionSource().equals(disconnectedPort)) { - model.setSelectedConnectionSource(null); - } - model.fireMiscChanged(); - } - } + disconnectHoveredConnection(model); } }); } else { @@ -1622,11 +1590,11 @@ public void loggedActionPerformed(final ActionEvent ae) { menu.addSeparator(); String name = "Process"; - if (model.getDisplayedChain().getProcess().getProcessLocation() != null) { - name = model.getDisplayedChain().getProcess().getProcessLocation().getShortName(); + if (displayedChain.getProcess().getProcessLocation() != null) { + name = displayedChain.getProcess().getProcessLocation().getShortName(); } menu.add(PrintingTools.makeExportPrintMenu(this, name)); - fireOperatorMenuWillOpen(menu, model.getDisplayedChain()); + fireOperatorMenuWillOpen(menu, displayedChain); } else { boolean first = true; for (OutputPort port : hoveredOp.getOutputPorts().getAllPorts()) { @@ -1663,6 +1631,25 @@ public void loggedActionPerformed(final ActionEvent e) { } + /** + * Disconnects the hovered connection. + * + * @param model the {@link ProcessRendererModel} + * @since 8.2 + */ + static void disconnectHoveredConnection(ProcessRendererModel model) { + OutputPort port = model.getHoveringConnectionSource(); + if (port == null || !port.isConnected()) { + return; + } + port.disconnect(); + model.setHoveringConnectionSource(null); + if (port.equals(model.getSelectedConnectionSource())) { + model.setSelectedConnectionSource(null); + } + model.fireMiscChanged(); + } + /** * Updates the currently displayed cursor depending on hover state. */ diff --git a/src/main/java/com/rapidminer/gui/flow/processrendering/view/components/OperatorWarningHandler.java b/src/main/java/com/rapidminer/gui/flow/processrendering/view/components/OperatorWarningHandler.java index 2a6cd46b3..32acef1f6 100644 --- a/src/main/java/com/rapidminer/gui/flow/processrendering/view/components/OperatorWarningHandler.java +++ b/src/main/java/com/rapidminer/gui/flow/processrendering/view/components/OperatorWarningHandler.java @@ -27,9 +27,11 @@ import com.rapidminer.gui.tools.ProcessGUITools; import com.rapidminer.gui.tools.bubble.BubbleWindow; import com.rapidminer.operator.ExecutionUnit; +import com.rapidminer.operator.InvalidRepositoryEntryError; import com.rapidminer.operator.Operator; import com.rapidminer.operator.ProcessSetupError; import com.rapidminer.operator.ports.Port; +import com.rapidminer.operator.ports.metadata.ProcessNotInRepositoryMetaDataError; import com.rapidminer.parameter.ParameterType; import com.rapidminer.tools.ProcessTools; import com.rapidminer.tools.container.Pair; @@ -88,12 +90,11 @@ public void processMouseEvent(ExecutionUnit process, MouseEventType type, MouseE if (operator != null && !operator.getErrorList().isEmpty()) { // calculate the bounding box of the warning icon as it is drawn by {@link - // ProcessDrawer#renderOperator()} Rectangle2D frame = model.getOperatorRect(operator); int iconX = (int) (frame.getX() + 3 + WARNING_ICON_SIZE); int iconY = (int) (frame.getY() + frame.getHeight() - WARNING_ICON_SIZE - 2); - int size = (int) Math.ceil(WARNING_ICON_SIZE); + int size = WARNING_ICON_SIZE; Rectangle2D boundingBox = new Rectangle2D.Float(iconX, iconY, size, size); @@ -133,14 +134,8 @@ public void killWarningBubble() { public void showOperatorWarning(Operator operator) { Pair missingParamPair = ProcessTools.getOperatorWithoutMandatoryParameter(operator); if (missingParamPair != null) { - if (operatorWarningBubble != null) { - // if the required bubble is already shown kill it for toggle effect - if (!operatorWarningBubble.isKilled() && missingParamPair.equals(lastMissingParamPair)) { - operatorWarningBubble.killBubble(true); - return; - } - // kill wrong bubble and go on showing new bubble - operatorWarningBubble.killBubble(true); + if (shouldRefreshExistingBubble(missingParamPair.equals(lastMissingParamPair))) { + return; } operatorWarningBubble = ProcessGUITools.displayPrecheckMissingMandatoryParameterWarning( @@ -154,21 +149,15 @@ public void showOperatorWarning(Operator operator) { } lastMissingParamPair = null; - Port missingInputPort = ProcessTools.getMissingPortConnection(operator); + Pair missingInputPort = ProcessTools.getMissingPortConnection(operator); if (missingInputPort != null) { - if (operatorWarningBubble != null) { - // if the required bubble is already shown kill it for toggle effect - if (!operatorWarningBubble.isKilled() && missingInputPort.equals(lastMissingInputPort)) { - operatorWarningBubble.killBubble(true); - return; - } - // kill wrong bubble and go on showing new bubble - operatorWarningBubble.killBubble(true); + if (shouldRefreshExistingBubble(missingInputPort.equals(lastMissingInputPort))) { + return; } operatorWarningBubble = ProcessGUITools.displayPrecheckInputPortDisconnectedWarning(missingInputPort, false); // set last values - lastMissingInputPort = missingInputPort; + lastMissingInputPort = missingInputPort.getFirst(); lastProcessSetupError = null; return; } @@ -187,11 +176,37 @@ public void showOperatorWarning(Operator operator) { // kill wrong bubble and go on showing new bubble operatorWarningBubble.killBubble(true); } - operatorWarningBubble = ProcessGUITools.displayProcessSetupError(processSetupError); + + if (processSetupError instanceof InvalidRepositoryEntryError) { + operatorWarningBubble = ProcessGUITools.displayPrecheckBrokenMandatoryParameterWarning(operator, ((InvalidRepositoryEntryError) processSetupError).getParameterKey()); + } else if (processSetupError instanceof ProcessNotInRepositoryMetaDataError) { + operatorWarningBubble = ProcessGUITools.displayPrecheckProcessNotSavedWarning(operator, processSetupError); + } else { + operatorWarningBubble = ProcessGUITools.displayProcessSetupError(processSetupError); + } lastProcessSetupError = processSetupError; return; } } lastProcessSetupError = null; } + + /** + * Checks whether a bubble is currently existing and shown. Returns {@code true} iff the current bubble + * was not killed and was showing, so that it only needs to be toggled and not created. + * + * @param isAlreadyShown if the current bubble is sufficient + * @return whether the bubble should eb refreshed or created anew + * @since 8.2 + */ + private boolean shouldRefreshExistingBubble(boolean isAlreadyShown) { + if (operatorWarningBubble == null) { + return false; + } + boolean isAlreadyKilled = operatorWarningBubble.isKilled(); + // if the required bubble is already shown kill it for toggle effect + // else kill wrong bubble and go on showing new bubble + operatorWarningBubble.killBubble(true); + return !isAlreadyKilled && isAlreadyShown; + } } diff --git a/src/main/java/com/rapidminer/gui/graphs/ClusterModelObjectViewer.java b/src/main/java/com/rapidminer/gui/graphs/ClusterModelObjectViewer.java index 419e98cbd..300e945d3 100644 --- a/src/main/java/com/rapidminer/gui/graphs/ClusterModelObjectViewer.java +++ b/src/main/java/com/rapidminer/gui/graphs/ClusterModelObjectViewer.java @@ -21,12 +21,12 @@ import javax.swing.DefaultListModel; import javax.swing.JComponent; import javax.swing.JList; -import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import com.rapidminer.ObjectVisualizer; import com.rapidminer.gui.tools.ExtendedJScrollPane; +import com.rapidminer.gui.tools.MenuShortcutJList; import com.rapidminer.operator.clustering.HierarchicalClusterNode; import com.rapidminer.tools.ObjectVisualizerService; @@ -40,7 +40,7 @@ public class ClusterModelObjectViewer implements GraphObjectViewer, ListSelectio private DefaultListModel model = new DefaultListModel<>(); - private JList listComponent = new JList<>(this.model); + private JList listComponent = new MenuShortcutJList<>(this.model, false); private Object clusterModel; @@ -50,7 +50,6 @@ public ClusterModelObjectViewer(Object clusterModel) { @Override public JComponent getViewerComponent() { - listComponent.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); listComponent.addListSelectionListener(this); listComponent.setVisibleRowCount(-1); return new ExtendedJScrollPane(listComponent); diff --git a/src/main/java/com/rapidminer/gui/license/LicenseTools.java b/src/main/java/com/rapidminer/gui/license/LicenseTools.java index 951f2c947..6b600a199 100644 --- a/src/main/java/com/rapidminer/gui/license/LicenseTools.java +++ b/src/main/java/com/rapidminer/gui/license/LicenseTools.java @@ -36,6 +36,7 @@ import com.rapidminer.license.ConstraintNotRestrictedException; import com.rapidminer.license.License; import com.rapidminer.license.LicenseConstants; +import com.rapidminer.parameter.ParameterTypeDateFormat; import com.rapidminer.tools.FileSystemService; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; @@ -81,7 +82,7 @@ public class LicenseTools { /** * Date formatter to generate ISO8601 compliant string representations in UTC time. */ - public static final DateTimeFormatter ISO_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd") + public static final DateTimeFormatter ISO_DATE_FORMATTER = DateTimeFormatter.ofPattern(ParameterTypeDateFormat.DATE_FORMAT_YYYY_MM_DD) .withLocale(Locale.UK).withZone(ZoneOffset.UTC); /** diff --git a/src/main/java/com/rapidminer/gui/look/Colors.java b/src/main/java/com/rapidminer/gui/look/Colors.java index 21f40e07e..58910935e 100644 --- a/src/main/java/com/rapidminer/gui/look/Colors.java +++ b/src/main/java/com/rapidminer/gui/look/Colors.java @@ -32,7 +32,7 @@ public class Colors { private static final ColorUIResource INPUT_BACKGROUND = new ColorUIResource(255, 255, 255); private static final ColorUIResource INPUT_BACKGROUND_DARK = new ColorUIResource(240, 240, 240); private static final ColorUIResource INPUT_BACKGROUND_DISABLED = new ColorUIResource(254, 254, 254); - private static final ColorUIResource SELECTION_BOX_FOREGROUND = new ColorUIResource(255, 102, 0); + private static final ColorUIResource SELECTION_BOX_FOREGROUND = new ColorUIResource(255, 130, 40); private static final ColorUIResource SELECTION_BOX_FOREGROUND_DISABLED = new ColorUIResource(155, 155, 155); private static final ColorUIResource INPUT_BORDER = new ColorUIResource(187, 187, 187); @@ -89,6 +89,14 @@ public class Colors { public static final ColorUIResource BUTTON_BACKGROUND_HIGHLIGHTED_PRESSED_GRADIENT_END = new ColorUIResource(237, 110, 69); public static final ColorUIResource BUTTON_BACKGROUND_HIGHLIGHTED_DISABLED_GRADIENT_START = BUTTON_BACKGROUND_DISABLED_GRADIENT_START; public static final ColorUIResource BUTTON_BACKGROUND_HIGHLIGHTED_DISABLED_GRADIENT_END = BUTTON_BACKGROUND_DISABLED_GRADIENT_END; + public static final ColorUIResource BUTTON_BACKGROUND_HIGHLIGHTED_DARK_GRADIENT_START = new ColorUIResource(152, 169, 184); + public static final ColorUIResource BUTTON_BACKGROUND_HIGHLIGHTED_DARK_GRADIENT_END = new ColorUIResource(120, 134, 147); + public static final ColorUIResource BUTTON_BACKGROUND_HIGHLIGHTED_DARK_ROLLOVER_GRADIENT_START = new ColorUIResource(137, 154, 169); + public static final ColorUIResource BUTTON_BACKGROUND_HIGHLIGHTED_DARK_ROLLOVER_GRADIENT_END = new ColorUIResource(122, 139, 154); + public static final ColorUIResource BUTTON_BACKGROUND_HIGHLIGHTED_DARK_PRESSED_GRADIENT_START = new ColorUIResource(115, 132, 147); + public static final ColorUIResource BUTTON_BACKGROUND_HIGHLIGHTED_DARK_PRESSED_GRADIENT_END = new ColorUIResource(142, 159, 184); + public static final ColorUIResource BUTTON_BACKGROUND_HIGHLIGHTED_DARK_DISABLED_GRADIENT_START = BUTTON_BACKGROUND_DISABLED_GRADIENT_START; + public static final ColorUIResource BUTTON_BACKGROUND_HIGHLIGHTED_DARK_DISABLED_GRADIENT_END = BUTTON_BACKGROUND_DISABLED_GRADIENT_END; public static final ColorUIResource COMBOBOX_BACKGROUND = INPUT_BACKGROUND; public static final ColorUIResource COMBOBOX_BACKGROUND_DARK = INPUT_BACKGROUND_DARK; @@ -146,8 +154,14 @@ public class Colors { public static final ColorUIResource TAB_BACKGROUND = new ColorUIResource(213, 213, 213); public static final ColorUIResource TAB_BACKGROUND_HIGHLIGHT = new ColorUIResource(233, 233, 234); public static final ColorUIResource TAB_BACKGROUND_SELECTED = TAB_BACKGROUND_HIGHLIGHT; + public static final ColorUIResource TAB_BACKGROUND_START = new ColorUIResource(243, 243, 243); + public static final ColorUIResource TAB_BACKGROUND_START_SELECTED = new ColorUIResource(255, 255, 255); public static final ColorUIResource TAB_CONTENT_BORDER = TAB_BORDER; + public static final ColorUIResource START_DIALOG_BACKGROUND = BUTTON_BACKGROUND_GRADIENT_END; + public static final ColorUIResource START_DIALOG_ORANGE_FONT = new ColorUIResource(231, 84, 36); + + public static final ColorUIResource CARD_PANEL_BACKGROUND = PANEL_BACKGROUND; public static final ColorUIResource CARD_PANEL_BACKGROUND_HIGHLIGHT = new ColorUIResource(220, 220, 220); public static final ColorUIResource CARD_PANEL_BACKGROUND_SELECTED = new ColorUIResource(208, 209, 209); @@ -187,28 +201,31 @@ public class Colors { public static final ColorUIResource PROGRESSBAR_INDETERMINATE_FOREGROUND_2 = new ColorUIResource(255, 181, 58); public static final ColorUIResource PROGRESSBAR_BORDER = new ColorUIResource(221, 221, 221); + public static final ColorUIResource MULTI_STEP_PROGRESSBAR_NEUTRAL = new ColorUIResource(60, 60, 60); + public static final ColorUIResource MULTI_STEP_PROGRESSBAR_NEUTRAL_LIGHT = new ColorUIResource(200, 200, 200); + public static final ColorUIResource WARNING_COLOR = new ColorUIResource(255, 230, 152); public static final ColorUIResource WHITE = new ColorUIResource(255, 255, 255); public static final ColorUIResource BLACK = new ColorUIResource(0, 0, 0); - private ColorUIResource[] tabbedPaneColors = new ColorUIResource[] { new ColorUIResource(200, 205, 210), + private ColorUIResource[] tabbedPaneColors = new ColorUIResource[]{new ColorUIResource(200, 205, 210), new ColorUIResource(215, 220, 225), new ColorUIResource(170, 170, 190), new ColorUIResource(200, 200, 220), new ColorUIResource(190, 200, 220), new ColorUIResource(250, 250, 250), new ColorUIResource(255, 255, 255), new ColorUIResource(210, 210, 230), new ColorUIResource(180, 190, 210), new ColorUIResource(200, 200, 220), new ColorUIResource(210, 210, 230), new ColorUIResource(220, 220, 240), new ColorUIResource(230, 230, 250), new ColorUIResource(235, 235, 255), new ColorUIResource(240, 240, 255), new ColorUIResource(245, 245, 255), new ColorUIResource(250, 250, 255), new ColorUIResource(255, 255, 255), new ColorUIResource(255, 255, 255), - new ColorUIResource(210, 210, 230), new ColorUIResource(240, 240, 255), new ColorUIResource(245, 145, 0), }; + new ColorUIResource(210, 210, 230), new ColorUIResource(240, 240, 255), new ColorUIResource(245, 145, 0),}; - private ColorUIResource[] fileChooserColors = new ColorUIResource[] { new ColorUIResource(255, 200, 200), - new ColorUIResource(230, 170, 170) }; + private ColorUIResource[] fileChooserColors = new ColorUIResource[]{new ColorUIResource(255, 200, 200), + new ColorUIResource(230, 170, 170)}; private ColorUIResource desktopBackgroundColor = new ColorUIResource(180, 195, 220); public void addCustomEntriesToTable(UIDefaults table) { - Object[] values = new Object[] { "ToolTip.background", TOOLTIP_BACKGROUND, "ToolTip.foreground", TOOLTIP_FOREGROUND, - "ToolTip.borderColor", TOOLTIP_BORDER }; + Object[] values = new Object[]{"ToolTip.background", TOOLTIP_BACKGROUND, "ToolTip.foreground", TOOLTIP_FOREGROUND, + "ToolTip.borderColor", TOOLTIP_BORDER}; table.putDefaults(values); } diff --git a/src/main/java/com/rapidminer/gui/look/RapidLookAndFeel.java b/src/main/java/com/rapidminer/gui/look/RapidLookAndFeel.java index 0fc99400a..ee874c387 100644 --- a/src/main/java/com/rapidminer/gui/look/RapidLookAndFeel.java +++ b/src/main/java/com/rapidminer/gui/look/RapidLookAndFeel.java @@ -58,6 +58,17 @@ public class RapidLookAndFeel extends BasicLookAndFeel { /** the radius of RoundedRect corners */ public static final int CORNER_DEFAULT_RADIUS = 5; + /** the radius for tabs on the welcome dialog (since 8.2) */ + public static final int CORNER_START_TAB_RADIUS = 8; + /** the space between tabs for the welcome dialog (since 8.2) */ + public static final int START_TAB_GAP = 10; + /** the indentation fro the first tab of the welcome dialog (since 8.2) */ + public static final int START_TAB_INDENT = 25; + /** top gap for the welcome dialog (since 8.2) */ + public static final int START_DIALOG_TOP_GAP = 17; + /** set this client property to true for the tabbed pane in the welcome dialog (since 8.2) */ + public static final String START_DIALOG_INDICATOR_PROPERTY = "com.rapidminer.StartDialog"; + /** the radius of RoundedRect corners for tabs */ public static final int CORNER_TAB_RADIUS = 5; diff --git a/src/main/java/com/rapidminer/gui/look/RapidLookTools.java b/src/main/java/com/rapidminer/gui/look/RapidLookTools.java index d10f2092d..06b60047a 100644 --- a/src/main/java/com/rapidminer/gui/look/RapidLookTools.java +++ b/src/main/java/com/rapidminer/gui/look/RapidLookTools.java @@ -26,6 +26,7 @@ import java.awt.Paint; import java.awt.RenderingHints; import java.awt.Shape; +import java.awt.geom.Ellipse2D; import java.awt.geom.RoundRectangle2D; import javax.swing.AbstractButton; import javax.swing.ButtonModel; @@ -84,6 +85,13 @@ public final class RapidLookTools { */ public static final String PROPERTY_BUTTON_HIGHLIGHT = "button_highlight"; + /** + * If buttons should be highlighted in dark gray or not. Set to {@code true} or {@code false}. Default is {@code false}. + * + * @since 8.1.2 + */ + public static final String PROPERTY_BUTTON_HIGHLIGHT_DARK = "button_highlight_dark"; + /** * If buttons should have a darker border. Set to {@code true} or {@code false}. Default is {@code false}. * @@ -91,6 +99,13 @@ public final class RapidLookTools { */ public static final String PROPERTY_BUTTON_DARK_BORDER = "button_dark_border"; + /** + * If buttons should be circular. Set to {@code true} or {@code false}. Default is {@code false}. + * + * @since 8.1.2 + */ + public static final String PROPERTY_BUTTON_CIRCLE = "button_circular"; + /** * If input fields should have a darker border. Set to {@code true} or {@code false}. Default is {@code false}. * @@ -135,16 +150,15 @@ public static void drawMenuItemBackground(Graphics g, JMenuItem menuItem) { g.setColor(oldColor); } - public static boolean drawMenuItemFading(Component c, Graphics g) { + public static void drawMenuItemFading(Component c, Graphics g) { int w = c.getWidth(); int h = c.getHeight(); if (h < 0 || w < 0) { - return true; + return; } g.setColor(Colors.MENU_ITEM_BACKGROUND); - g.fillRect(0, 0, c.getWidth(), c.getHeight()); - return true; + g.fillRect(0, 0, w, h); } public static boolean isToolbarButton(JComponent b) { @@ -201,9 +215,14 @@ public static Colors getColors() { public static Shape createShapeForButton(AbstractButton b) { int w = b.getWidth(); int h = b.getHeight(); + boolean circular = Boolean.parseBoolean(String.valueOf(b.getClientProperty(RapidLookTools.PROPERTY_BUTTON_CIRCLE))); - return new RoundRectangle2D.Double(1, 1, w - 2, h - 2, RapidLookAndFeel.CORNER_DEFAULT_RADIUS, - RapidLookAndFeel.CORNER_DEFAULT_RADIUS); + if (circular) { + return createCircle(w, h); + } else { + return new RoundRectangle2D.Double(1, 1, w - 2, h - 2, RapidLookAndFeel.CORNER_DEFAULT_RADIUS, + RapidLookAndFeel.CORNER_DEFAULT_RADIUS); + } } /** @@ -216,9 +235,14 @@ public static Shape createShapeForButton(AbstractButton b) { public static Shape createBorderShapeForButton(AbstractButton b) { int w = b.getWidth(); int h = b.getHeight(); + boolean circular = Boolean.parseBoolean(String.valueOf(b.getClientProperty(RapidLookTools.PROPERTY_BUTTON_CIRCLE))); - return new RoundRectangle2D.Double(0, 0, w - 1, h - 1, RapidLookAndFeel.CORNER_DEFAULT_RADIUS, - RapidLookAndFeel.CORNER_DEFAULT_RADIUS); + if (circular) { + return createCircle(w, h); + } else { + return new RoundRectangle2D.Double(0, 0, w - 1, h - 1, RapidLookAndFeel.CORNER_DEFAULT_RADIUS, + RapidLookAndFeel.CORNER_DEFAULT_RADIUS); + } } /** @@ -274,6 +298,7 @@ public static void drawButton(AbstractButton b, Graphics g, Shape shape) { ColorUIResource colorGradientStart; ColorUIResource colorGradientEnd; boolean highlighted = Boolean.parseBoolean(String.valueOf(b.getClientProperty(PROPERTY_BUTTON_HIGHLIGHT))); + boolean highlightedDark = Boolean.parseBoolean(String.valueOf(b.getClientProperty(PROPERTY_BUTTON_HIGHLIGHT_DARK))); if (highlighted) { if (b.isEnabled()) { if (b.getModel().isPressed() || b.getModel().isSelected()) { @@ -290,6 +315,22 @@ public static void drawButton(AbstractButton b, Graphics g, Shape shape) { colorGradientStart = Colors.BUTTON_BACKGROUND_HIGHLIGHTED_DISABLED_GRADIENT_START; colorGradientEnd = Colors.BUTTON_BACKGROUND_HIGHLIGHTED_DISABLED_GRADIENT_END; } + } else if (highlightedDark) { + if (b.isEnabled()) { + if (b.getModel().isPressed() || b.getModel().isSelected()) { + colorGradientStart = Colors.BUTTON_BACKGROUND_HIGHLIGHTED_DARK_PRESSED_GRADIENT_START; + colorGradientEnd = Colors.BUTTON_BACKGROUND_HIGHLIGHTED_DARK_PRESSED_GRADIENT_END; + } else if (b.getModel().isRollover()) { + colorGradientStart = Colors.BUTTON_BACKGROUND_HIGHLIGHTED_DARK_ROLLOVER_GRADIENT_START; + colorGradientEnd = Colors.BUTTON_BACKGROUND_HIGHLIGHTED_DARK_ROLLOVER_GRADIENT_END; + } else { + colorGradientStart = Colors.BUTTON_BACKGROUND_HIGHLIGHTED_DARK_GRADIENT_START; + colorGradientEnd = Colors.BUTTON_BACKGROUND_HIGHLIGHTED_DARK_GRADIENT_END; + } + } else { + colorGradientStart = Colors.BUTTON_BACKGROUND_HIGHLIGHTED_DARK_DISABLED_GRADIENT_START; + colorGradientEnd = Colors.BUTTON_BACKGROUND_HIGHLIGHTED_DARK_DISABLED_GRADIENT_END; + } } else { if (b.isEnabled()) { if (b.getModel().isPressed() || b.getModel().isSelected()) { @@ -314,4 +355,22 @@ public static void drawButton(AbstractButton b, Graphics g, Shape shape) { g2.fill(shape); } + /** + * Creates a circle of a rectangle with the given width and height. Must have at least 16px of width and height, otherwise results will be broken. + * + * @param width + * the width + * @param height + * the height + * @return the ellipsis, never {@code null} + */ + private static Ellipse2D.Double createCircle(int width, int height) { + int centerX = (1 + width) / 2 + 1; + int centerY = (1 + height - 1) / 2; + int cornerX = centerX + width / 2 - 6; + int cornerY = centerY + width / 2 - 6; + Ellipse2D.Double circle = new Ellipse2D.Double(); + circle.setFrameFromCenter(centerX, centerY, cornerX, cornerY); + return circle; + } } diff --git a/src/main/java/com/rapidminer/gui/look/icons/CheckBoxIcon.java b/src/main/java/com/rapidminer/gui/look/icons/CheckBoxIcon.java index d0d3c2102..cb1d12f28 100644 --- a/src/main/java/com/rapidminer/gui/look/icons/CheckBoxIcon.java +++ b/src/main/java/com/rapidminer/gui/look/icons/CheckBoxIcon.java @@ -25,10 +25,9 @@ import java.awt.RenderingHints; import java.awt.Stroke; import java.io.Serializable; - +import javax.swing.AbstractButton; import javax.swing.ButtonModel; import javax.swing.Icon; -import javax.swing.JCheckBox; import javax.swing.plaf.UIResource; import com.rapidminer.gui.look.Colors; @@ -47,8 +46,15 @@ public class CheckBoxIcon implements Icon, UIResource, Serializable { @Override public void paintIcon(Component c, Graphics g, int x, int y) { - JCheckBox checkbox = (JCheckBox) c; - ButtonModel bm = checkbox.getModel(); + AbstractButton abstractButton = (AbstractButton) c; + paintInternally(c, g, x, y, abstractButton.getModel()); + } + + void paintInternally(Component c, Graphics g, int x, int y, ButtonModel bm) { + boolean isSelected = bm.isSelected(); + boolean isEnabled = bm.isEnabled(); + boolean isPressed = bm.isPressed(); + int w = c.getWidth(); int h = c.getHeight(); if (h < 0 || w < 0) { @@ -57,10 +63,6 @@ public void paintIcon(Component c, Graphics g, int x, int y) { g.translate(x, y); - boolean isSelected = bm.isSelected(); - boolean isEnabled = bm.isEnabled(); - boolean isPressed = bm.isPressed(); - Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); diff --git a/src/main/java/com/rapidminer/gui/look/icons/CheckBoxMenuItemIcon.java b/src/main/java/com/rapidminer/gui/look/icons/CheckBoxMenuItemIcon.java index 6887efa33..e075769ca 100644 --- a/src/main/java/com/rapidminer/gui/look/icons/CheckBoxMenuItemIcon.java +++ b/src/main/java/com/rapidminer/gui/look/icons/CheckBoxMenuItemIcon.java @@ -25,10 +25,9 @@ import java.awt.RenderingHints; import java.awt.Stroke; import java.io.Serializable; - +import javax.swing.AbstractButton; import javax.swing.ButtonModel; import javax.swing.Icon; -import javax.swing.JMenuItem; import javax.swing.plaf.UIResource; import com.rapidminer.gui.look.Colors; @@ -47,7 +46,7 @@ public class CheckBoxMenuItemIcon implements Icon, UIResource, Serializable { @Override public void paintIcon(Component c, Graphics g, int x, int y) { - JMenuItem b = (JMenuItem) c; + AbstractButton b = (AbstractButton) c; ButtonModel model = b.getModel(); g.translate(x, y); diff --git a/src/main/java/com/rapidminer/gui/look/icons/CombinedIcon.java b/src/main/java/com/rapidminer/gui/look/icons/CombinedIcon.java new file mode 100644 index 000000000..581e8fb53 --- /dev/null +++ b/src/main/java/com/rapidminer/gui/look/icons/CombinedIcon.java @@ -0,0 +1,218 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.gui.look.icons; + +import java.awt.Component; +import java.awt.Graphics; +import java.util.Objects; +import javax.swing.Icon; + + +/** + * Helper Icon class in case you need to display two {@link Icon Icons} but there is only one {@link Icon} supported by + * the {@link Component} + * + * @author Andreas Timm + * @since 8.2 + */ +public class CombinedIcon implements Icon { + + /** + * The orientation to lay out the combined icons + */ + public enum Orientation { + HORIZONTAL { + @Override + int getWidth(Icon firstIcon, Icon secondIcon, int gap) { + return firstIcon.getIconWidth() + secondIcon.getIconWidth() + gap; + } + + @Override + int getHeight(Icon firstIcon, Icon secondIcon, int gap) { + return Math.max(firstIcon.getIconHeight(), secondIcon.getIconHeight()); + } + + @Override + int getSecondIconX(int offset, Icon firstIcon, int gap) { + return offset + firstIcon.getIconWidth() + gap; + } + + @Override + int getSecondIconY(int offset, Icon firstIcon, int gap) { + return offset; + } + }, VERTICAL { + @Override + int getWidth(Icon firstIcon, Icon secondIcon, int gap) { + return Math.max(firstIcon.getIconWidth(), secondIcon.getIconWidth()); + } + + @Override + int getHeight(Icon firstIcon, Icon secondIcon, int gap) { + return firstIcon.getIconHeight() + secondIcon.getIconHeight() + gap; + } + + @Override + int getSecondIconX(int offset, Icon firstIcon, int gap) { + return offset; + } + + @Override + int getSecondIconY(int offset, Icon firstIcon, int gap) { + return offset + firstIcon.getIconHeight() + gap; + } + }; + + /** + * Calculates the overall width for the given icons depending on the orientation + * + * @param firstIcon + * the icon that will be displayed to the left or on top of the secondIcon + * @param secondIcon + * the other icon to be displayed next to the firstIcon + * @param gap + * the space between the icons + * @return amount of pixels required to display this combination of icons + */ + abstract int getWidth(Icon firstIcon, Icon secondIcon, int gap); + + /** + * Calculates the overall height for the given icons depending on the orientation + * + * @param firstIcon + * the icon that will be displayed to the left or on top of the secondIcon + * @param secondIcon + * the other icon to be displayed next to the firstIcon + * @param gap + * the space between the icons + * @return amount of pixels required to display this combination of icons + */ + abstract int getHeight(Icon firstIcon, Icon secondIcon, int gap); + + /** + * Calculates the x position for the second icon depending on the orientation + * + * @param offset + * the offset to add when calculating the position + * @param firstIcon + * the icon that will be displayed to the left or on top of the secondIcon + * @param gap + * the space between the icons + * @return the x position for the second icon + */ + abstract int getSecondIconX(int offset, Icon firstIcon, int gap); + + /** + * Calculates the y position for the second icon depending on the orientation + * + * @param offset + * the offset to add when calculating the position + * @param firstIcon + * the icon that will be displayed to the left or on top of the secondIcon + * @param gap + * the space between the icons + * @return the y position for the second icon + */ + abstract int getSecondIconY(int offset, Icon firstIcon, int gap); + } + + /** + * Default orientation is horizontal + */ + private Orientation orientation = Orientation.HORIZONTAL; + + /** + * Define a gap to separate the {@link Icon Icons} + */ + private int gap = 0; + + /** + * The {@link Icon Icons} that will be combined in the named order + */ + private Icon firstIcon; + private Icon secondIcon; + + /** + * Construct this instance to combine the provided icons. Based on the orientation the first icon will be placed to + * the left or above the second icon with a gap. Default orientation is horizontal with a gap of zero. + * + * @param firstIcon + * icon to be displayed first + * @param secondIcon + * icon to be displayed second + */ + public CombinedIcon(Icon firstIcon, Icon secondIcon) { + this.firstIcon = Objects.requireNonNull(firstIcon, "First icon may not be null!"); + this.secondIcon = Objects.requireNonNull(secondIcon, "Second icon may not be null!"); + } + + /** + * The {@link Orientation}, either horizontal or vertical + * + * @return current {@link Orientation} + */ + public Orientation getOrientation() { + return orientation; + } + + /** + * Only supporting defined {@link Orientation Orientations} + * + * @param orientation + * one of HORIZONTAL or VERTICAL + */ + public void setOrientation(Orientation orientation) { + this.orientation = orientation; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + firstIcon.paintIcon(c, g, x, y); + secondIcon.paintIcon(c, g, orientation.getSecondIconX(x, firstIcon, gap), orientation.getSecondIconY(y, firstIcon, gap)); + } + + @Override + public int getIconWidth() { + return orientation.getWidth(firstIcon, secondIcon, gap); + } + + @Override + public int getIconHeight() { + return orientation.getHeight(firstIcon, secondIcon, gap); + } + + /** + * Get the current setting for the gap between displayed {@link Icon Icons} + * + * @return amount of pixel to separate the {@link Icon Icons} + */ + public int getGap() { + return gap; + } + + /** + * Change the gap between the {@link Icon Icons} + * + * @param gap + * amount of pixel to separate the {@link Icon Icons} + */ + public void setGap(int gap) { + this.gap = gap; + } +} diff --git a/src/main/java/com/rapidminer/gui/look/icons/EmptyIcon.java b/src/main/java/com/rapidminer/gui/look/icons/EmptyIcon.java new file mode 100644 index 000000000..a4e7053fb --- /dev/null +++ b/src/main/java/com/rapidminer/gui/look/icons/EmptyIcon.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.gui.look.icons; + +import java.awt.Component; +import java.awt.Graphics; +import javax.swing.Icon; + + +/** + * Create a variable sized empty icon. Use prepared instances from {@link IconFactory}. + * + * @author Andreas Timm + * @since 8.2 + */ +public class EmptyIcon implements Icon { + + /** + * size of the icon + */ + private int width; + private int height; + + /** + * Create an empty icon with this size + * + * @param width + * of the icon + * @param height + * of the icon + */ + public EmptyIcon(int width, int height) { + this.width = width; + this.height = height; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + // intentionally left empty + } + + /** + * Get the icons height + * + * @return height of the icon in pixels + */ + public int getIconHeight() { + return height; + } + + /** + * Get the icons width + * + * @return width of the icon in pixels + */ + public int getIconWidth() { + return width; + } +} diff --git a/src/main/java/com/rapidminer/gui/look/icons/IconFactory.java b/src/main/java/com/rapidminer/gui/look/icons/IconFactory.java index 9e7658696..d71892406 100644 --- a/src/main/java/com/rapidminer/gui/look/icons/IconFactory.java +++ b/src/main/java/com/rapidminer/gui/look/icons/IconFactory.java @@ -1,25 +1,24 @@ /** * Copyright (C) 2001-2018 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero 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 * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.gui.look.icons; import java.awt.Dimension; - import javax.swing.Icon; @@ -33,13 +32,15 @@ public class IconFactory { public static final Dimension MENU_ICON_SIZE = new Dimension(10, 10); - private final static Icon RADIO_BUTTON_ICON = new RadioButtonIcon(); + private static final Icon RADIO_BUTTON_ICON = new RadioButtonIcon(); - private final static Icon CHECK_BOX_ICON = new CheckBoxIcon(); + private static final Icon CHECK_BOX_ICON = new CheckBoxIcon(); - private final static Icon CHECK_BOX_MENU_ITEM_ICON = new CheckBoxMenuItemIcon(); + private static final Icon CHECK_BOX_MENU_ITEM_ICON = new CheckBoxMenuItemIcon(); - private final static Icon RADIO_BUTTON_MENU_ITEM_ICON = new RadioButtonMenuItemIcon(); + private static final Icon RADIO_BUTTON_MENU_ITEM_ICON = new RadioButtonMenuItemIcon(); + + private static final Icon EMPTY_ICON_16x16 = new EmptyIcon(16, 16); public static Icon getRadioButtonIcon() { return RADIO_BUTTON_ICON; @@ -57,4 +58,7 @@ public static Icon getRadioButtonMenuItemIcon() { return RADIO_BUTTON_MENU_ITEM_ICON; } + public static Icon getEmptyIcon16x16() { + return EMPTY_ICON_16x16; + } } diff --git a/src/main/java/com/rapidminer/gui/look/ui/ComboBoxUI.java b/src/main/java/com/rapidminer/gui/look/ui/ComboBoxUI.java index 77d5ab1e7..4d114f2af 100644 --- a/src/main/java/com/rapidminer/gui/look/ui/ComboBoxUI.java +++ b/src/main/java/com/rapidminer/gui/look/ui/ComboBoxUI.java @@ -27,14 +27,12 @@ import java.awt.Polygon; import java.awt.Rectangle; import java.awt.RenderingHints; -import java.awt.Toolkit; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; - import javax.swing.BoxLayout; import javax.swing.ComboBoxEditor; import javax.swing.JButton; @@ -57,7 +55,7 @@ import com.rapidminer.gui.look.RapidLookListCellRenderer; import com.rapidminer.gui.look.RapidLookTools; import com.rapidminer.gui.look.borders.Borders; -import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.gui.tools.MenuShortcutJList; /** @@ -97,20 +95,7 @@ protected void configurePopup() { @SuppressWarnings({ "unchecked", "rawtypes" }) @Override protected JList createList() { - return new JList(this.comboBox.getModel()) { - - private static final long serialVersionUID = -2467344849011408539L; - - @Override - public void processMouseEvent(MouseEvent e) { - if (SwingTools.isControlOrMetaDown(e)) { - e = new MouseEvent((Component) e.getSource(), e.getID(), e.getWhen(), e.getModifiers() - ^ Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), e.getX(), e.getY(), - e.getClickCount(), e.isPopupTrigger()); - } - super.processMouseEvent(e); - } - }; + return new MenuShortcutJList(this.comboBox.getModel(), false); } @Override diff --git a/src/main/java/com/rapidminer/gui/look/ui/MultiStepProgressBar.java b/src/main/java/com/rapidminer/gui/look/ui/MultiStepProgressBar.java new file mode 100644 index 000000000..27a6c3b49 --- /dev/null +++ b/src/main/java/com/rapidminer/gui/look/ui/MultiStepProgressBar.java @@ -0,0 +1,639 @@ +/** + * RapidMiner Auto Model and Model Simulator Extension + * + * Copyright (C) 2018 RapidMiner GmbH + */ +package com.rapidminer.gui.look.ui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RadialGradientPaint; +import java.awt.geom.Arc2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.logging.Level; +import javax.swing.JPanel; + +import org.apache.commons.lang.ArrayUtils; + +import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawer; +import com.rapidminer.gui.look.Colors; +import com.rapidminer.tools.LogService; + + +/** + * A component which can be used for multistep progress bars for indicating where the user + * currently is in a multistep progress. + * + * @author Ingo Mierswa + * @since 8.2 + */ +public class MultiStepProgressBar extends JPanel { + + private static final long serialVersionUID = -3058771473146212336L; + + private static final Color BACKGROUND_COLOR = Colors.MULTI_STEP_PROGRESSBAR_NEUTRAL_LIGHT; + + // 16 pixel additional empty space on each side + private static final int PADDING = 16; + + // assume 8 pixel per letter should be sufficient + private static final int LETTER_SIZE = 8; + + private static final int HEIGHT = 56; + + private static final int LARGE_BUBBLE_DIAMETER = 28; + + private static final int BUBBLE_DIAMETER = 16; + + private static final int MAX_ANIMATION_STEPS = 24; + + private static final long ANIMATION_THREAD_SLEEP_MS = 18; + + private String[] stepNames; + + private Color color; + + private int currentStep; + + private int longestStep; + + private Dimension preferredSize; + + // fields needed for animation handling + private int targetStep; + private boolean currentlyInSubprocess = false; + private boolean indeterminateSubprocess = false; + private int animationStep; + private int currentSubstepToNextStep; + private int maxSubstepsToNextStep; + private boolean stopTransition; + private int indeterminateAnimationPhase; + private int bubbleAnimationPhase; + private boolean inTransition; + + /** + * Creates a new multi step progress bar for the given step names. Uses the specified color. + * + * @param stepNames + * the step names + * @param color + * the color of the bar + */ + public MultiStepProgressBar(String[] stepNames, Color color) { + this.stepNames = stepNames; + this.color = color; + this.currentStep = 0; + + longestStep = -1; + for (String step : stepNames) { + if (step.length() > longestStep) { + longestStep = step.length(); + } + } + + preferredSize = new Dimension(longestStep * stepNames.length * LETTER_SIZE + 2 * PADDING, HEIGHT); + + setFont(getFont().deriveFont(Font.BOLD, 13)); + } + + /** + * Starts a new subprocess which shows an animation towards the next bubble depending on the progress in a subprocess. + * Use {@link MultiStepProgressBar#startIndeterminateSubprocess} for indeterminate amount of steps. + * + * @param maxSubstepsToNextStep + * the maximal number of steps until the subprocess is ended + * @param targetStep + * the first step index + */ + public void startSubprocess(int maxSubstepsToNextStep, int targetStep) { + this.targetStep = targetStep; + this.maxSubstepsToNextStep = maxSubstepsToNextStep; + this.indeterminateSubprocess = false; + startSubprocessThread(); + } + + /** + * Sets the current step in an determinate subprocess. Moves the bar one step closer to the next major step. + * Calling this method might artificially take some small amounts of time to ensure that fast + * determined tasks with only a few substeps are feeling similar in length than the short transition + * phase. + * + * @param stepInSubprocess + * the step in the current subprocess + */ + public synchronized void setSubprocessStep(int stepInSubprocess) { + this.currentSubstepToNextStep = stepInSubprocess; + waitForNextIteration(); + } + + /** + * Returns the current step in the current subprocess or -1 if this is currently not in a subprocess. + * + * @return the current subprocess step + */ + public synchronized int getSubprocessStep() { + return this.currentSubstepToNextStep; + } + + /** + * Starts a new subprocess which shows an animation towards the next bubble depending on the progress in a subprocess. + */ + public void startIndeterminateSubprocess(int targetStep) { + this.targetStep = targetStep; + this.indeterminateSubprocess = true; + this.indeterminateAnimationPhase = 0; + startSubprocessThread(); + } + + /** + * Starts a new animation thread. + */ + private void startSubprocessThread() { + this.animationStep = -1; + this.bubbleAnimationPhase = 0; + this.currentlyInSubprocess = true; + new Thread(() -> { + // stop if subprocess was stopped but always finish the first animation of an indeterminate subprocess... + while (currentlyInSubprocess) { + // update the animation counter + animationStep++; + if (animationStep >= MAX_ANIMATION_STEPS) { + animationStep = 0; + } + + // repaint the bar + repaint(); + + waitForNextIteration(); + } + + // still in first animation phase of indeterminate animation? Finish it! + if (indeterminateSubprocess && indeterminateAnimationPhase == 0) { + for (int a = animationStep; a < MAX_ANIMATION_STEPS; a++) { + // update the animation counter + animationStep++; + + // repaint the bar + repaint(); + + waitForNextIteration(); + } + } + + indeterminateSubprocess = false; + indeterminateAnimationPhase = 0; + bubbleAnimationPhase = 0; + animationStep = -1; + + maxSubstepsToNextStep = -1; + currentSubstepToNextStep = -1; + + currentStep = targetStep; + repaint(); + }).start(); + } + + /** + * Waits before going on with execution. Handles the {@link InterruptedException}. + */ + private void waitForNextIteration() { + try { + Thread.sleep(ANIMATION_THREAD_SLEEP_MS); + } catch (InterruptedException e) { + currentlyInSubprocess = false; + Thread.currentThread().interrupt(); + LogService.getRoot().log(Level.WARNING, "Interrupt in progress bar animation thread.", e); + } + } + + /** + * Returns the step index for the given step name. If multiple steps have the same name, this method will return + * the index of the first occurrence. + * + * @param stepName + * the step name + * @return the corresponding step index + */ + public int getIndexForName(String stepName) { + return ArrayUtils.indexOf(stepNames, stepName); + } + + /** + * Sets the current step index. Ends a subprocess animation if there is one or shows a quick transition animation + * if there was no subprocess. + * + * @param index + * the index of the current step + */ + public void setCurrentStep(int index) { + setCurrentStep(index, true); + } + + /** + * Sets the current step index. Ends a subprocess animation if there is one or shows a quick transition animation + * if there was no subprocess. + * + * @param stepName + * the name of the current step + */ + public void setCurrentStep(String stepName) { + setCurrentStep(getIndexForName(stepName), true); + } + + /** + * Sets the current step index. Ends a subprocess animation if there is one or shows a quick transition animation + * if there was no subprocess. + * + * @param stepName + * the index of the current step + * @param animated + * indicates if an animation should be shown for the transition + */ + public void setCurrentStep(String stepName, boolean animated) { + setCurrentStep(getIndexForName(stepName), animated); + } + + /** + * Sets the current step index. Ends a subprocess animation if there is one or shows a quick transition animation + * if there was no subprocess. + * + * @param index + * the index of the current step + * @param animated + * indicates if an animation should be shown for the transition + */ + public void setCurrentStep(int index, boolean animated) { + if (!animated) { + currentStep = index; + repaint(); + return; + } + if (this.currentlyInSubprocess) { + // if a subprocess is running -> kill it and jump to result + this.currentlyInSubprocess = false; + } else if (index == currentStep + 1) { + // if no subprocess is running -> show a quick animation for the transition + // start new transition thread if new step is one bigger than the current one, + // just jump if user jumped over steps quickly + this.targetStep = index; + startTransitionAnimation(); + } else { + this.stopTransition = true; + this.animationStep = -1; + currentStep = index; + targetStep = index; + repaint(); + } + } + + /** + * Starts a quick animation which makes the transition to the next step. + */ + private void startTransitionAnimation() { + stopTransition = false; + bubbleAnimationPhase = 0; + new Thread(() -> { + inTransition = true; + for (animationStep = 0; animationStep < MAX_ANIMATION_STEPS; animationStep++) { + if (stopTransition) { + inTransition = false; + return; + } + + // repaint the bar + repaint(); + + waitForNextIteration(); + } + + if (!stopTransition) { + currentStep = targetStep; + animationStep = -1; + bubbleAnimationPhase = 0; + repaint(); + } + inTransition = false; + }).start(); + } + + /** + * Returns the preferred size which is based on the number of steps and the number of letters in the steps. + * + * @return the preferred size + */ + @Override + public Dimension getPreferredSize() { + return preferredSize; + } + + /** + * Paints the component. + * + * @param g + * the g to paint on + */ + @Override + public void paint(Graphics g) { + super.paint(g); + + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHints(ProcessDrawer.HI_QUALITY_HINTS); + + int widthPerStep = longestStep * LETTER_SIZE; + + int counter = 0; + int xPos = PADDING; + for (String step : stepNames) { + // =========== + // Step Name + // =========== + int textY = paintStepLabel(g2, counter, widthPerStep, xPos, step); + + // =========== + // Bars + // =========== + if (counter < stepNames.length - 1) { // not the last step + if (counter == currentStep) { + if (currentlyInSubprocess) { + if (indeterminateSubprocess) { + // indeterminate sub-process -> alternating gray and orange growing bars + paintIndeterminatedSubprocessBar(g2, widthPerStep, xPos, textY); + // update the indeterminate animation phase counter + if (animationStep >= MAX_ANIMATION_STEPS - 1) { + indeterminateAnimationPhase++; + } + } else { + // determinate subprocess -> use progress in current subprocess + paintDeterminateSubprocessBar(g2, widthPerStep, xPos, textY); + } + } else { + // not in subprocess but still animation going on? -> currently in transition animation + paintTransitionBar(g2, widthPerStep, xPos, textY); + } + } else { + // no subprocess for this bar? Done -> Orange; Not Done -> Gray + drawSimpleBar(g2, counter, widthPerStep, xPos, textY); + } + } + + // no animation running and it is the current bubble -> gradient border to indicate the current bubble + drawCurrentBubbleIndicator(g2, counter, widthPerStep, xPos, textY); + + // ============= + // Bubbles + // ============= + if ((counter == currentStep + 1) && ((currentlyInSubprocess && !indeterminateSubprocess) || (currentlyInSubprocess && indeterminateAnimationPhase > 0))) { + // currently in subprocess towards the next step? -> Animated Bubble + paintAnimatedBubble(g2, widthPerStep, xPos, textY); + // update bubble animation phase + if (animationStep >= MAX_ANIMATION_STEPS - 1) { + bubbleAnimationPhase++; + } + } else { + // no subprocess towards this bubble? -> Just draw the full bubble then + paintFullBubble(g2, counter, widthPerStep, xPos, textY); + } + + // prepare next step + xPos += widthPerStep; + counter++; + } + } + + /** + * Paints the step label and delivers the y-position of the label. + * + * @param g2 + * the graphics to paint on + * @param counter + * the current step counter + * @param widthPerStep + * the width per step (depending on the used step labels) + * @param xPos + * the current x-position + * @return the y-position of the label + */ + private int paintStepLabel(Graphics2D g2, int counter, int widthPerStep, int xPos, String step) { + FontMetrics metrics = g2.getFontMetrics(getFont()); + Rectangle2D stepBounds = metrics.getStringBounds(step, g2); + int textX = (int) Math.round(xPos + widthPerStep / 2.0d - stepBounds.getWidth() / 2.0d); + int textY = (int) Math.round(2 + stepBounds.getHeight()); + + if ((counter <= currentStep) || + ((counter == currentStep + 1) && + (currentlyInSubprocess || indeterminateSubprocess || inTransition))) { + // done or currently in subprocess towards this step? -> dark color + g2.setColor(Colors.MULTI_STEP_PROGRESSBAR_NEUTRAL); + } else { + // not done yet? -> lighter color + g2.setColor(Colors.MULTI_STEP_PROGRESSBAR_NEUTRAL_LIGHT); + } + g2.drawString(step, textX, textY); + return textY; + } + + /** + * Paint the transition bar for an indeterminate subprocess (no sub-steps but long computation in between). + * + * @param g2 + * the graphics to paint on + * @param widthPerStep + * the width per step (depending on the used step labels) + * @param xPos + * the current x-position + * @param textY + * the y-position of the texts + */ + private void paintIndeterminatedSubprocessBar(Graphics2D g2, int widthPerStep, int xPos, int textY) { + // alternating gray and orange growing bars (good style match to cases where the indeterminate time is short) + double percentage = animationStep / (double) MAX_ANIMATION_STEPS; + final boolean inEvenAnimationPhase = indeterminateAnimationPhase % 2 == 0; + g2.setColor(inEvenAnimationPhase ? color : BACKGROUND_COLOR); + + int barX = (int) Math.round(xPos + widthPerStep / 2.0d); + int barY = (int) Math.round(textY + 8 + LARGE_BUBBLE_DIAMETER / 2.0d - 3); + g2.fillRect(barX, barY, (int) Math.round(percentage * widthPerStep), 6); + + g2.setColor(inEvenAnimationPhase ? BACKGROUND_COLOR : color); + barX = (int) Math.round(xPos + widthPerStep / 2.0d) + (int) Math.round(percentage * widthPerStep); + barY = (int) Math.round(textY + 8 + LARGE_BUBBLE_DIAMETER / 2.0d - 3); + g2.fillRect(barX, barY, widthPerStep - (int) Math.round(percentage * widthPerStep), 6); + } + + /** + * Paint the transition bar for a determinate subprocess (multiple sub-steps). + * + * @param g2 + * the graphics to paint on + * @param widthPerStep + * the width per step (depending on the used step labels) + * @param xPos + * the current x-position + * @param textY + * the y-position of the texts + */ + private void paintDeterminateSubprocessBar(Graphics2D g2, int widthPerStep, int xPos, int textY) { + double percentage = currentSubstepToNextStep / (double) maxSubstepsToNextStep; + g2.setColor(color); + int barX = (int) Math.round(xPos + widthPerStep / 2.0d); + int barY = (int) Math.round(textY + 8 + LARGE_BUBBLE_DIAMETER / 2.0d - 3); + g2.fillRect(barX, barY, (int) Math.round(percentage * widthPerStep), 6); + + g2.setColor(BACKGROUND_COLOR); + barX = (int) Math.round(xPos + widthPerStep / 2.0d) + (int) Math.round(percentage * widthPerStep); + barY = (int) Math.round(textY + 8 + LARGE_BUBBLE_DIAMETER / 2.0d - 3); + g2.fillRect(barX, barY, widthPerStep - (int) Math.round(percentage * widthPerStep), 6); + } + + /** + * Paint the transition animation bar (quick transition shown between steps without substeps or long computations). + * + * @param g2 + * the graphics to paint on + * @param widthPerStep + * the width per step (depending on the used step labels) + * @param xPos + * the current x-position + * @param textY + * the y-position of the texts + */ + private void paintTransitionBar(Graphics2D g2, int widthPerStep, int xPos, int textY) { + if (animationStep >= 0) { + double percentage = animationStep / (double) MAX_ANIMATION_STEPS; + g2.setColor(color); + int barX = (int) Math.round(xPos + widthPerStep / 2.0d); + int barY = (int) Math.round(textY + 8 + LARGE_BUBBLE_DIAMETER / 2.0d - 3); + g2.fillRect(barX, barY, (int) Math.round(percentage * widthPerStep), 6); + + g2.setColor(BACKGROUND_COLOR); + barX = (int) Math.round(xPos + widthPerStep / 2.0d) + (int) Math.round(percentage * widthPerStep); + barY = (int) Math.round(textY + 8 + LARGE_BUBBLE_DIAMETER / 2.0d - 3); + g2.fillRect(barX, barY, widthPerStep - (int) Math.round(percentage * widthPerStep), 6); + } else { + // not (yet) in transition -> just draw the gray bar + g2.setColor(BACKGROUND_COLOR); + int barX = (int) Math.round(xPos + widthPerStep / 2.0d); + int barY = (int) Math.round(textY + 8 + LARGE_BUBBLE_DIAMETER / 2.0d - 3); + g2.fillRect(barX, barY, widthPerStep, 6); + } + } + + /** + * Draws the simple bar either in gray background (not there yet) or in highlight color if this step + * has been done already. + * + * @param g2 + * the graphics to paint on + * @param counter + * the current step counter + * @param widthPerStep + * the width per step (depending on the used step labels) + * @param xPos + * the current x-position + * @param textY + * the y-position of the texts + */ + private void drawSimpleBar(Graphics2D g2, int counter, int widthPerStep, int xPos, int textY) { + if (counter < currentStep) { // already done? highlight color + g2.setColor(color); + } else { // post the current step --> gray + g2.setColor(BACKGROUND_COLOR); + } + int barX = (int) Math.round(xPos + widthPerStep / 2.0d); + int barY = (int) Math.round(textY + 8 + LARGE_BUBBLE_DIAMETER / 2.0d - 3); + g2.fillRect(barX, barY, widthPerStep, 6); + } + + /** + * Paint the circular gradient indicator around the bubble of the current step. + * + * @param g2 + * the graphics to paint on + * @param counter + * the current step counter + * @param widthPerStep + * the width per step (depending on the used step labels) + * @param xPos + * the current x-position + * @param textY + * the y-position of the texts + */ + private void drawCurrentBubbleIndicator(Graphics2D g2, int counter, int widthPerStep, int xPos, int textY) { + if ((counter != currentStep) || (animationStep >= 0)) { + return; + } + int bubbleX = (int) Math.round(xPos + widthPerStep / 2.0d - LARGE_BUBBLE_DIAMETER / 2.0d); + int bubbleY = textY + 8; + + Point2D.Float point = new Point2D.Float(bubbleX + LARGE_BUBBLE_DIAMETER / 2.0f, bubbleY + LARGE_BUBBLE_DIAMETER / 2.0f); + float[] fractions = {0.0f, 1.0f}; + Color color1 = new Color(color.getRed(), color.getGreen(), color.getBlue(), 0); + Color color2 = new Color(color.getRed(), color.getGreen(), color.getBlue(), 90); + Color[] colors = {color1, color2}; + RadialGradientPaint paint = new RadialGradientPaint(point.x, point.y, LARGE_BUBBLE_DIAMETER / 2.0f, fractions, colors); + g2.setPaint(paint); + g2.fillOval(bubbleX, bubbleY, LARGE_BUBBLE_DIAMETER, LARGE_BUBBLE_DIAMETER); + } + + /** + * Paint the animated bubble indicating that the progress is currently made towards this bubble. + * + * @param g2 + * the graphics to paint on + * @param widthPerStep + * the width per step (depending on the used step labels) + * @param xPos + * the current x-position + * @param textY + * the y-position of the texts + */ + private void paintAnimatedBubble(Graphics2D g2, int widthPerStep, int xPos, int textY) { + // fill gray bubble with orange and then with gray again + final boolean inOddAnimationPhase = bubbleAnimationPhase % 2 == 1; + + g2.setColor(inOddAnimationPhase ? BACKGROUND_COLOR : color); + int bubbleX = (int) Math.round(xPos + widthPerStep / 2.0d - BUBBLE_DIAMETER / 2.0d); + int bubbleY = (int) Math.round(textY + 8 + (LARGE_BUBBLE_DIAMETER - BUBBLE_DIAMETER) / 2.0d); + g2.fillOval(bubbleX, bubbleY, BUBBLE_DIAMETER, BUBBLE_DIAMETER); + + // then orange partly filled bubble + double percentage = animationStep / (double) MAX_ANIMATION_STEPS; + double extent = 360d * percentage; + + g2.setColor(inOddAnimationPhase ? color : BACKGROUND_COLOR); + Arc2D arc = new Arc2D.Double(bubbleX, bubbleY, BUBBLE_DIAMETER, BUBBLE_DIAMETER, 90, -extent, Arc2D.PIE); + g2.fill(arc); + } + + /** + * Draws the full bubble which is done when there is no subprocess indicator to this bubble. + * The bubble is either drawn in the background color if not reached yet of in the highlight + * color if it has been reached already. + * + * @param g2 + * the graphics to paint on + * @param counter + * the current step counter + * @param widthPerStep + * the width per step (depending on the used step labels) + * @param xPos + * the current x-position + * @param textY + * the y-position of the texts + */ + private void paintFullBubble(Graphics2D g2, int counter, int widthPerStep, int xPos, int textY) { + if (counter <= currentStep) { + g2.setColor(color); + } else { + g2.setColor(BACKGROUND_COLOR); + } + int bubbleX = (int) Math.round(xPos + widthPerStep / 2.0d - BUBBLE_DIAMETER / 2.0d); + int bubbleY = (int) Math.round(textY + 8 + (LARGE_BUBBLE_DIAMETER - BUBBLE_DIAMETER) / 2.0d); + g2.fillOval(bubbleX, bubbleY, BUBBLE_DIAMETER, BUBBLE_DIAMETER); + } +} diff --git a/src/main/java/com/rapidminer/gui/look/ui/SliderUI.java b/src/main/java/com/rapidminer/gui/look/ui/SliderUI.java index f85fbd13a..831067d6a 100644 --- a/src/main/java/com/rapidminer/gui/look/ui/SliderUI.java +++ b/src/main/java/com/rapidminer/gui/look/ui/SliderUI.java @@ -1,21 +1,21 @@ /** * Copyright (C) 2001-2018 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero 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 * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.gui.look.ui; import java.awt.Dimension; @@ -27,7 +27,6 @@ import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.geom.Rectangle2D; - import javax.swing.JComponent; import javax.swing.JSlider; import javax.swing.SwingConstants; @@ -60,6 +59,16 @@ public void mouseReleased(MouseEvent e) { SliderUI.this.thumbIsPressed = false; getSlider().repaint(); } + + + private Rectangle getThumbBounds() { + return SliderUI.this.thumbRect; + } + + private JSlider getSlider() { + return SliderUI.this.slider; + } + } private boolean thumbIsPressed = false; @@ -74,11 +83,6 @@ public SliderUI(JSlider jSlider) { super(jSlider); } - @Override - public void installUI(JComponent c) { - super.installUI(c); - } - @Override public void paintThumb(Graphics g) { Graphics2D g2 = (Graphics2D) g; @@ -196,11 +200,8 @@ public void paintTrack(Graphics g) { RapidLookAndFeel.CORNER_DEFAULT_RADIUS / 2); // draw fill bar - int curVal = this.slider.getModel().getValue(); - double percentage = (double) (curVal - slider.getModel().getMinimum()) / (double)(slider.getModel().getMaximum() - slider.getModel().getMinimum()); - g2.setColor(Colors.SLIDER_TRACK_FOREGROUND); - g2.fill(new Rectangle2D.Double(4, trackTop + 2, (length - trackBuffer) * percentage, 3)); + g2.fill(new Rectangle2D.Double(4, trackTop + 2, this.thumbRect.x + 2, 3)); // draw border g2.setColor(Colors.SLIDER_TRACK_BORDER); @@ -221,12 +222,8 @@ public void paintTrack(Graphics g) { RapidLookAndFeel.CORNER_DEFAULT_RADIUS / 2); // draw fill bar - int curVal = this.slider.getModel().getValue(); - double percentage = (double) (curVal - slider.getModel().getMinimum()) / (double)(slider.getModel().getMaximum() - slider.getModel().getMinimum()); - g2.setColor(Colors.SLIDER_TRACK_FOREGROUND); - g2.fill(new Rectangle2D.Double(trackLeft + 2, (h - trackBuffer) * (1d - percentage), 3, (h - trackBuffer) - * percentage)); + g2.fill(new Rectangle2D.Double(trackLeft + 2, this.thumbRect.y - 2, 3, h - this.thumbRect.y - 4)); // draw border g2.setColor(Colors.SLIDER_TRACK_BORDER); @@ -236,7 +233,9 @@ public void paintTrack(Graphics g) { } @Override - public void paintFocus(Graphics g) {} + public void paintFocus(Graphics g) { + // intentionally left empty + } @Override protected void calculateThumbSize() { @@ -259,14 +258,6 @@ protected void calculateThumbLocation() { } } - private Rectangle getThumbBounds() { - return this.thumbRect; - } - - private JSlider getSlider() { - return this.slider; - } - protected MouseListener createThumbPressedListener() { return new ThumbListener(); } diff --git a/src/main/java/com/rapidminer/gui/look/ui/TabbedPaneUI.java b/src/main/java/com/rapidminer/gui/look/ui/TabbedPaneUI.java index 0f8c8ca3c..e309335e9 100644 --- a/src/main/java/com/rapidminer/gui/look/ui/TabbedPaneUI.java +++ b/src/main/java/com/rapidminer/gui/look/ui/TabbedPaneUI.java @@ -29,19 +29,19 @@ import java.awt.Polygon; import java.awt.Rectangle; import java.awt.RenderingHints; +import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; -import java.awt.event.MouseMotionListener; import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.awt.geom.QuadCurve2D; +import java.beans.PropertyChangeListener; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JTabbedPane; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; -import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.plaf.ColorUIResource; import javax.swing.plaf.ComponentUI; @@ -62,10 +62,7 @@ */ public class TabbedPaneUI extends BasicTabbedPaneUI { - private class TabbedPaneMouseListener implements MouseMotionListener, MouseListener { - - @Override - public void mouseClicked(MouseEvent e) {} + private class TabbedPaneMouseListener extends MouseAdapter { @Override public void mouseEntered(MouseEvent e) { @@ -87,9 +84,6 @@ public void mouseReleased(MouseEvent e) { updateMouseOver(e.getPoint()); } - @Override - public void mouseDragged(MouseEvent e) {} - @Override public void mouseMoved(MouseEvent e) { updateMouseOver(e.getPoint()); @@ -97,28 +91,33 @@ public void mouseMoved(MouseEvent e) { } private TabbedPaneMouseListener mouseListener = new TabbedPaneMouseListener(); - private ChangeListener tabSelListener = new ChangeListener() { - - @Override - public void stateChanged(ChangeEvent e) { - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - // the content border is not repainted for the SettingsDialog unless this is - // done - TabbedPaneUI.this.tabPane.repaint(); - } - }); - } - }; + // the content border is not repainted for the SettingsDialog unless this is done + private ChangeListener tabSelListener = e -> SwingUtilities.invokeLater(() -> TabbedPaneUI.this.tabPane.repaint()); + // update the state of this UI in case of client property change + private PropertyChangeListener startDialogListener = e -> isStartDialogTab = isStartDialogTab(); private int rolloveredTabIndex = -1; + private boolean isDockingFrameworkTab; + private boolean isStartDialogTab; + public static ComponentUI createUI(JComponent c) { return new TabbedPaneUI(); } + @Override + public void installUI(JComponent c) { + super.installUI(c); + isDockingFrameworkTab = isDockingFrameworkTab(); + isStartDialogTab = isStartDialogTab(); + } + + @Override + public void uninstallUI(JComponent c) { + super.uninstallUI(c); + isDockingFrameworkTab = isStartDialogTab = false; + } + @Override protected JButton createScrollButton(int direction) { if (direction != SOUTH && direction != NORTH && direction != EAST && direction != WEST) { @@ -133,6 +132,9 @@ protected void installListeners() { this.tabPane.addMouseListener(this.mouseListener); this.tabPane.addMouseMotionListener(this.mouseListener); this.tabPane.addChangeListener(this.tabSelListener); + if (!isDockingFrameworkTab) { + this.tabPane.addPropertyChangeListener(RapidLookAndFeel.START_DIALOG_INDICATOR_PROPERTY, startDialogListener); + } } @Override @@ -140,11 +142,10 @@ protected void uninstallListeners() { super.uninstallListeners(); this.tabPane.removeMouseListener(this.mouseListener); this.tabPane.removeMouseMotionListener(this.mouseListener); - } - - @Override - protected void installDefaults() { - super.installDefaults(); + this.tabPane.removeChangeListener(this.tabSelListener); + if (!isDockingFrameworkTab) { + this.tabPane.removePropertyChangeListener(RapidLookAndFeel.START_DIALOG_INDICATOR_PROPERTY, startDialogListener); + } } @Override @@ -162,7 +163,7 @@ protected MouseListener createMouseListener() { @Override protected int calculateTabWidth(int tabPlacement, int tabIndex, FontMetrics metrics) { - if (isDockingFrameworkTab()) { + if (isDockingFrameworkTab) { return super.calculateTabWidth(tabPlacement, tabIndex, metrics) - 5; } @@ -180,11 +181,23 @@ protected LayoutManager createLayoutManager() { @Override protected void calculateTabRects(int tabPlacement, int tabCount) { - final int spacer = -5; - final int indent = 0; + if (isStartDialogTab) { + calculateStartDialogTabs(tabPlacement, tabCount); + return; + } + calculateVLDockingTabRects(tabPlacement, tabCount); + } + /** + * Calculates the tab rectangles for VLDocking based tabs. + * + * @since 8.2 + */ + private void calculateVLDockingTabRects(int tabPlacement, int tabCount) { super.calculateTabRects(tabPlacement, tabCount); + final int spacer = -5; + final int indent = 0; for (int i = 1; i < rects.length; i++) { // hack to get the tabs closer together. Don't shift leftmost tab(s) if (rects[i].x > 0) { @@ -192,6 +205,20 @@ protected void calculateTabRects(int tabPlacement, int tabCount) { } } } + + /** + * Calculates the tab rectangles for the welcome dialog. + * + * @since 8.2 + */ + private void calculateStartDialogTabs(int tabPlacement, int tabCount) { + + super.calculateTabRects(tabPlacement, tabCount); + + for (int i = 0; i < rects.length; i++) { + rects[i].x += i * RapidLookAndFeel.START_TAB_GAP + RapidLookAndFeel.START_TAB_INDENT; + } + } }; } @@ -199,54 +226,28 @@ protected void calculateTabRects(int tabPlacement, int tabCount) { @Override protected Insets getTabInsets(int tabPlacement, int tabIndex) { - Insets t; - switch (tabPlacement) { - case SwingConstants.TOP: - if (isDockingFrameworkTab()) { - t = new Insets(6, 5, 6, -10); - } else { - t = new Insets(6, 8, 6, 8); - } - break; - case SwingConstants.LEFT: - t = new Insets(8, 8, 8, 8); - break; - case SwingConstants.RIGHT: - t = new Insets(8, 8, 8, 8); - break; - case SwingConstants.BOTTOM: - t = new Insets(8, 8, 8, 8); - break; - default: - t = new Insets(8, 8, 8, 8); - break; + Insets t = new Insets(8, 8, 8, 8); + if (tabPlacement == SwingConstants.TOP) { + t.top = t.bottom = 6; + if (isDockingFrameworkTab) { + t.left = 5; + t.right = -10; + } } return t; } @Override protected Insets getSelectedTabPadInsets(int tabPlacement) { - Insets t; - switch (tabPlacement) { - case SwingConstants.TOP: - if (isDockingFrameworkTab()) { - t = new Insets(0, 5, 0, 0); - } else { - t = new Insets(0, 5, 0, 5); - } - break; - case SwingConstants.LEFT: - t = new Insets(1, 5, 0, 5); - break; - case SwingConstants.RIGHT: - t = new Insets(1, 5, 0, 5); - break; - case SwingConstants.BOTTOM: - t = new Insets(1, 5, 0, 5); - break; - default: - t = new Insets(1, 5, 0, 5); - break; + Insets t = new Insets(1, 5, 0, 5); + if (tabPlacement == SwingConstants.TOP) { + t.top = 0; + if (isDockingFrameworkTab || isStartDialogTab) { + t.right = 0; + } + if (isStartDialogTab) { + t.left = 0; + } } return t; } @@ -304,7 +305,7 @@ protected void paintContentBorder(Graphics g, int tabPlacement, int selectedInde // left corner, bottom left corner, or in the middle. Reason for that is that the // top // left or bottom left tab has no upper/lower left corner at all - g2.setColor(Colors.TAB_BACKGROUND_SELECTED); + g2.setColor(isStartDialogTab ? Colors.TAB_BACKGROUND_START_SELECTED : Colors.TAB_BACKGROUND_SELECTED); if (selTabBounds.y < r) { g2.fillRect(x - 1, y + 1, 5, selTabBounds.height - 1); @@ -359,7 +360,7 @@ protected void paintContentBorder(Graphics g, int tabPlacement, int selectedInde // now we remove the top line of the rect depending on whether the tab is in the top // left corner, top right corner, or in the middle. Reason for that is that the top // left or top right tab has no upper left/right corner at all - g2.setColor(Colors.TAB_BACKGROUND_SELECTED); + g2.setColor(isStartDialogTab ? Colors.TAB_BACKGROUND_START_SELECTED : Colors.TAB_BACKGROUND_SELECTED); if (selTabBounds.x < r) { g2.fillRect(x + 1, y - 1, selTabBounds.width - 1, 5); @@ -452,20 +453,20 @@ private static void paintSelectedRight(Graphics g, int x, int y, int w, int h) { g.drawLine(w + x - 6, y + 6, x + w - 6, y + 6); } - private static void paintSelectedLeft(Graphics g, int x, int y, int w, int h) { + private void paintSelectedLeft(Graphics g, int x, int y, int w, int h) { Graphics2D g2 = (Graphics2D) g.create(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - drawLeftTab(x + 2, y, w - 2, h, g2, Colors.TAB_BACKGROUND_SELECTED); + drawLeftTab(x + 2, y, w - 2, h, g2, isStartDialogTab ? Colors.TAB_BACKGROUND_START_SELECTED : Colors.TAB_BACKGROUND_SELECTED); g2.dispose(); } - private static void paintSelectedTop(Graphics g, int x, int y, int w, int h) { + private void paintSelectedTop(Graphics g, int x, int y, int w, int h) { Graphics2D g2 = (Graphics2D) g.create(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - drawTopTab(x, y, w, h, g2, Colors.TAB_BACKGROUND_SELECTED); + drawTopTab(x, y, w, h, g2, isStartDialogTab ? Colors.TAB_BACKGROUND_START_SELECTED : Colors.TAB_BACKGROUND_SELECTED); g2.dispose(); } @@ -536,8 +537,8 @@ private static void paintSelectedBottom(Graphics g, int x, int y, int w, int h) * @param color * the background color of the tab */ - private static void drawTopTab(int x, int y, int w, int h, Graphics2D g2, ColorUIResource color) { - double rTop = RapidLookAndFeel.CORNER_TAB_RADIUS * 0.67; + private void drawTopTab(int x, int y, int w, int h, Graphics2D g2, ColorUIResource color) { + double rTop = isStartDialogTab ? RapidLookAndFeel.CORNER_START_TAB_RADIUS : RapidLookAndFeel.CORNER_TAB_RADIUS * 0.67; g2.setColor(color); g2.fill(createTopTabShape(x + 1, y + 1, w - 1, h, rTop, true)); @@ -638,9 +639,9 @@ private static Path2D createLeftTabShape(int x, int y, int w, int h, double rLef } private void paintTabBorderFree(Graphics g, int tabPlacement, int tabIndex, int xp, int yp, int mw, int h) { - int x = xp + 2; + int x = xp + (isStartDialogTab ? 0 : 2); int y = yp; - int w = mw - 4; + int w = mw - (isStartDialogTab ? 0 : 4); Graphics2D g2 = (Graphics2D) g.create(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); @@ -770,14 +771,14 @@ private void paintTabBorderFree(Graphics g, int tabPlacement, int tabIndex, int // highlight on hover drawLeftTab(x, y, w, h, g2, Colors.TAB_BACKGROUND_HIGHLIGHT); } else { - drawLeftTab(x, y, w, h, g2, Colors.TAB_BACKGROUND); + drawLeftTab(x, y, w, h, g2, isStartDialogTab ? Colors.TAB_BACKGROUND_START : Colors.TAB_BACKGROUND); } } else { // top if (tabIndex == rolloveredTabIndex) { // highlight on hover drawTopTab(x, y, w, h, g2, Colors.TAB_BACKGROUND_HIGHLIGHT); } else { - drawTopTab(x, y, w, h, g2, Colors.TAB_BACKGROUND); + drawTopTab(x, y, w, h, g2, isStartDialogTab ? Colors.TAB_BACKGROUND_START : Colors.TAB_BACKGROUND); } } g2.dispose(); @@ -785,10 +786,8 @@ private void paintTabBorderFree(Graphics g, int tabPlacement, int tabIndex, int @Override protected int getTabLabelShiftX(int tabPlacement, int tabIndex, boolean isSelected) { - if (tabPane.getTabLayoutPolicy() != JTabbedPane.SCROLL_TAB_LAYOUT) { - if (isSelected) { - return -5; - } + if (tabPane.getTabLayoutPolicy() != JTabbedPane.SCROLL_TAB_LAYOUT && isSelected && !isStartDialogTab) { + return -5; } return 0; } @@ -808,7 +807,7 @@ protected void updateMouseOver(Point p) { @Override public Insets getContentBorderInsets(int tabPlacement) { - return new Insets(1, 1, 2, 1); + return isStartDialogTab ? new Insets(1, 0, 0, 0) : new Insets(1, 1, 2, 1); } @Override @@ -842,4 +841,14 @@ private boolean isDockingFrameworkTab() { return false; } + + /** + * @return {@code true} iff the {@link #tabPane} has the client property set + * to indicate that it is from the welcome dialog + * @see RapidLookAndFeel#START_DIALOG_INDICATOR_PROPERTY + * @since 8.2 + */ + private boolean isStartDialogTab() { + return Boolean.parseBoolean(String.valueOf(tabPane.getClientProperty(RapidLookAndFeel.START_DIALOG_INDICATOR_PROPERTY))); + } } diff --git a/src/main/java/com/rapidminer/gui/operatormenu/NewOperatorMenu.java b/src/main/java/com/rapidminer/gui/operatormenu/NewOperatorMenu.java index 866e55249..5f10e5290 100644 --- a/src/main/java/com/rapidminer/gui/operatormenu/NewOperatorMenu.java +++ b/src/main/java/com/rapidminer/gui/operatormenu/NewOperatorMenu.java @@ -23,6 +23,7 @@ import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.tools.OperatorService; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; import java.util.Collections; @@ -46,6 +47,7 @@ public void performAction(OperatorDescription description) { try { Operator operator = OperatorService.createOperator(description); RapidMinerGUI.getMainFrame().getActions().insert(Collections.singletonList(operator)); + ActionStatisticsCollector.getInstance().log(ActionStatisticsCollector.TYPE_NEW_OPERATOR_MENU, "inserted", operator.getOperatorDescription().getKey()); } catch (Exception e) { SwingTools.showSimpleErrorMessage("cannot_instantiate", e, description.getName()); } diff --git a/src/main/java/com/rapidminer/gui/operatormenu/ReplaceOperatorMenu.java b/src/main/java/com/rapidminer/gui/operatormenu/ReplaceOperatorMenu.java index 716fd723c..8e479dd9e 100644 --- a/src/main/java/com/rapidminer/gui/operatormenu/ReplaceOperatorMenu.java +++ b/src/main/java/com/rapidminer/gui/operatormenu/ReplaceOperatorMenu.java @@ -18,7 +18,15 @@ */ package com.rapidminer.gui.operatormenu; +import java.awt.geom.Rectangle2D; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.RapidMinerGUI; +import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawUtils; +import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.operator.ExecutionUnit; import com.rapidminer.operator.Operator; @@ -28,17 +36,13 @@ import com.rapidminer.operator.ports.OutputPort; import com.rapidminer.tools.OperatorService; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** * An operator menu which can be used to replace the currently selected operator by one of the same * type. Simple operators can be by other simple operators or operator chains, operator chains can * only be replaced by other chains. This operator menu is available in the context menu of an * operator in tree view. - * + * * @author Ingo Mierswa, Simon Fischer, Tobias Malbrecht */ public class ReplaceOperatorMenu extends OperatorMenu { @@ -61,7 +65,8 @@ public void performAction(OperatorDescription description) { /** The currently selected operator will be replaced by the given operator. */ private void replace(Operator operator) { - List selection = RapidMinerGUI.getMainFrame().getSelectedOperators(); + MainFrame mainFrame = RapidMinerGUI.getMainFrame(); + List selection = mainFrame.getSelectedOperators(); if (selection.isEmpty()) { return; } @@ -72,8 +77,8 @@ private void replace(Operator operator) { } // remember source and sink connections so we can reconnect them later. - Map inputPortMap = new HashMap(); - Map outputPortMap = new HashMap(); + Map inputPortMap = new HashMap<>(); + Map outputPortMap = new HashMap<>(); for (OutputPort source : selectedOperator.getOutputPorts().getAllPorts()) { if (source.isConnected()) { inputPortMap.put(source.getName(), source.getDestination()); @@ -94,7 +99,7 @@ private void replace(Operator operator) { int failedReconnects = 0; // copy children if possible - if ((selectedOperator instanceof OperatorChain) && (operator instanceof OperatorChain)) { + if (selectedOperator instanceof OperatorChain && operator instanceof OperatorChain) { OperatorChain oldChain = (OperatorChain) selectedOperator; OperatorChain newChain = (OperatorChain) operator; int numCommonSubprocesses = Math.min(oldChain.getNumberOfSubprocesses(), newChain.getNumberOfSubprocesses()); @@ -131,11 +136,17 @@ private void replace(Operator operator) { } } - RapidMinerGUI.getMainFrame().selectOperator(operator.getParent()); + // copy operator rectangle from old operator to the new one to make the swap in place + ProcessRendererModel processModel = mainFrame.getProcessPanel().getProcessRenderer().getModel(); + Rectangle2D rect = processModel.getOperatorRect(selectedOperator); + rect = new Rectangle2D.Double(rect.getX(), rect.getY(), rect.getWidth(), + ProcessDrawUtils.calcHeighForOperator(operator)); + processModel.setOperatorRect(operator, rect); + mainFrame.selectAndShowOperator(operator, true); if (failedReconnects > 0) { SwingTools.showVerySimpleErrorMessage("op_replaced_failed_connections_restored", failedReconnects); } - + } } diff --git a/src/main/java/com/rapidminer/gui/operatortree/ProcessTreeModel.java b/src/main/java/com/rapidminer/gui/operatortree/ProcessTreeModel.java index 1c69e9361..79b09963a 100644 --- a/src/main/java/com/rapidminer/gui/operatortree/ProcessTreeModel.java +++ b/src/main/java/com/rapidminer/gui/operatortree/ProcessTreeModel.java @@ -249,7 +249,11 @@ private void fireTreeNodesChanged(Operator operator) { // update while // dying. for (TreeModelListener l : listenerList.getListeners(TreeModelListener.class)) { - l.treeNodesChanged(e); + try { + l.treeNodesChanged(e); + } catch (Exception ex) { + //ignore + } } } } @@ -257,7 +261,11 @@ private void fireTreeNodesChanged(Operator operator) { private void fireTreeNodesInserted(Operator operator) { TreeModelEvent e = makeChangeEvent(operator); for (TreeModelListener l : listenerList.getListeners(TreeModelListener.class)) { - l.treeNodesInserted(e); + try { + l.treeNodesInserted(e); + } catch (Exception ex) { + //ignore + } } } @@ -265,7 +273,11 @@ private void fireTreeNodesRemoved(Operator operator, int oldIndex) { TreePath path = getPathTo(operator).getParentPath(); TreeModelEvent e = new TreeModelEvent(this, path, new int[] { oldIndex }, new Object[] { operator }); for (TreeModelListener l : listenerList.getListeners(TreeModelListener.class)) { - l.treeNodesRemoved(e); + try { + l.treeNodesRemoved(e); + } catch (Exception ex) { + //ignore + } } } @@ -273,7 +285,11 @@ private void fireTreeStructureChanged(ExecutionUnit unit) { TreePath path = getPathTo(unit).getParentPath(); TreeModelEvent e = new TreeModelEvent(this, path); for (TreeModelListener l : listenerList.getListeners(TreeModelListener.class)) { - l.treeStructureChanged(e); + try { + l.treeStructureChanged(e); + } catch (Exception ex) { + //ignore + } } } diff --git a/src/main/java/com/rapidminer/gui/plotter/PlotterChooser.java b/src/main/java/com/rapidminer/gui/plotter/PlotterChooser.java index 2cafb505a..fbd5961b3 100644 --- a/src/main/java/com/rapidminer/gui/plotter/PlotterChooser.java +++ b/src/main/java/com/rapidminer/gui/plotter/PlotterChooser.java @@ -25,17 +25,13 @@ import java.awt.Graphics2D; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.GeneralPath; -import java.util.Iterator; import java.util.logging.Level; - import javax.swing.BorderFactory; import javax.swing.DefaultListModel; import javax.swing.Icon; @@ -50,6 +46,7 @@ import com.rapidminer.gui.new_plotter.gui.popup.PopupAction; import com.rapidminer.gui.tools.ListHoverHelper; +import com.rapidminer.gui.tools.MenuShortcutJList; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.tools.LogService; @@ -151,29 +148,19 @@ public Component getListCellRendererComponent(JList list, E value, } private Icon getIcon(String plotterName, boolean selected) { - // check to decide which icon size should be loaded - if (!isSmallIconsUsed()) { - if (selected) { - return SwingTools - .createImage("icons/chartPreview/" + ICON_SIZE + "/" + plotterName.replace(' ', '_') + ".png"); - } else { - return SwingTools.createImage( - "icons/chartPreview/" + ICON_SIZE + "/" + plotterName.replace(' ', '_') + "-grey.png"); - } - } else { - if (selected) { - return SwingTools.createImage( - "icons/chartPreview/" + SMALL_ICON_SIZE + "/" + plotterName.replace(' ', '_') + ".png"); - } else { - return SwingTools.createImage( - "icons/chartPreview/" + SMALL_ICON_SIZE + "/" + plotterName.replace(' ', '_') + "-grey.png"); - } + StringBuilder builder = new StringBuilder("icons/chartPreview/"); + builder.append(isSmallIconsUsed() ? SMALL_ICON_SIZE : ICON_SIZE).append('/'); + builder.append(plotterName.replace(' ', '_')); + if (!selected) { + builder.append("-grey"); } + builder.append(".png"); + return SwingTools.createImage(builder.toString()); } } - private JList plotterList = new JList<>(new DefaultListModel<>()); + private JList plotterList = new MenuShortcutJList<>(new DefaultListModel<>(), false); public PlotterChooser() { super(); @@ -190,15 +177,11 @@ public PlotterChooser() { final PopupAction popupAction = new PopupAction("choose_plotter", plotterList); setAction(popupAction); - this.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - if (smallIcons != isResolutionTooSmall()) { - smallIcons = !smallIcons; // toggle the type of icons - // if the resolution has changed, create a new panel to support this resolution - plotterList.setCellRenderer(new PlotterListCellRenderer<>()); - } + this.addActionListener(e -> { + if (smallIcons != isResolutionTooSmall()) { + smallIcons = !smallIcons; // toggle the type of icons + // if the resolution has changed, create a new panel to support this resolution + plotterList.setCellRenderer(new PlotterListCellRenderer<>()); } }); @@ -233,22 +216,16 @@ public void setSettings(PlotterConfigurationModel plotterSettings) { private void populateList(PlotterConfigurationModel plotterSettings) { ((DefaultListModel) plotterList.getModel()).clear(); - Iterator n = plotterSettings.getAvailablePlotters().keySet().iterator(); - while (n.hasNext()) { - String plotterName = n.next(); + plotterSettings.getAvailablePlotters().forEach((plotterName, plotterClass) -> { try { - Class plotterClass = plotterSettings.getAvailablePlotters().get(plotterName); if (plotterClass != null) { ((DefaultListModel) plotterList.getModel()).addElement(plotterName); } - } catch (IllegalArgumentException e) { - LogService.getRoot().log(Level.WARNING, - "com.rapidminer.gui.plotter.PlotterControlPanel.instatiating_plotter_error", plotterName); - } catch (SecurityException e) { + } catch (IllegalArgumentException | SecurityException e) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.plotter.PlotterControlPanel.instatiating_plotter_error", plotterName); } - } + }); } diff --git a/src/main/java/com/rapidminer/gui/plotter/PlotterControlPanel.java b/src/main/java/com/rapidminer/gui/plotter/PlotterControlPanel.java index 91dc7dd10..4526c982f 100644 --- a/src/main/java/com/rapidminer/gui/plotter/PlotterControlPanel.java +++ b/src/main/java/com/rapidminer/gui/plotter/PlotterControlPanel.java @@ -24,15 +24,10 @@ import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.ItemEvent; import java.awt.event.ItemListener; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; - import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JButton; @@ -44,10 +39,6 @@ import javax.swing.ListSelectionModel; import javax.swing.ScrollPaneConstants; import javax.swing.SwingConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; import com.rapidminer.datatable.DataTable; import com.rapidminer.gui.look.Colors; @@ -91,11 +82,10 @@ public class PlotterControlPanel extends JPanel implements PlotterChangedListene private List changeListenerElements = new LinkedList<>(); - private transient final ItemListener plotterComboListener = new ItemListener() { - - @Override - public void itemStateChanged(ItemEvent e) { - plotterSettings.setPlotter(plotterCombo.getSelectedItem().toString()); + private final transient ItemListener plotterComboListener = e -> { + Object selectedItem = plotterCombo.getSelectedItem(); + if (selectedItem != null) { + plotterSettings.setPlotter(selectedItem.toString()); } }; @@ -126,13 +116,7 @@ private void updateControls() { final JLabel coordinatesLabel = new JLabel(" "); PlotterMouseHandler mouseHandler = new PlotterMouseHandler(plotter, plotterSettings.getDataTable(), - new CoordinatesHandler() { - - @Override - public void updateCoordinates(String coordinateInfo) { - coordinatesLabel.setText(coordinateInfo); - } - }); + coordinatesLabel::setText); plotter.addMouseMotionListener(mouseHandler); plotter.addMouseListener(mouseHandler); @@ -174,7 +158,7 @@ public void updateCoordinates(String coordinateInfo) { label.setToolTipText(toolTip); this.add(label, c); final int finalAxisIndex = axisIndex; - final ListeningJComboBox axisCombo = new ListeningJComboBox(PlotterAdapter.PARAMETER_SUFFIX_AXIS + final ListeningJComboBox axisCombo = new ListeningJComboBox<>(PlotterAdapter.PARAMETER_SUFFIX_AXIS + PlotterAdapter.transformParameterName(plotter.getAxisName(finalAxisIndex)), 200); axisCombo.setToolTipText(toolTip); axisCombo.setPreferredSize( @@ -186,15 +170,12 @@ public void updateCoordinates(String coordinateInfo) { } changeListenerElements.add(axisCombo); - axisCombo.addItemListener(new ItemListener() { - - @Override - public void itemStateChanged(ItemEvent e) { - String value = PlotterAdapter.PARAMETER_SUFFIX_AXIS - + PlotterAdapter.transformParameterName(plotter.getAxisName(finalAxisIndex)); - String key = axisCombo.getSelectedItem().toString(); - - plotterSettings.setParameterAsString(value, key); + axisCombo.addItemListener(e -> { + String value = PlotterAdapter.PARAMETER_SUFFIX_AXIS + + PlotterAdapter.transformParameterName(plotter.getAxisName(finalAxisIndex)); + Object selectedItem = axisCombo.getSelectedItem(); + if (selectedItem != null) { + plotterSettings.setParameterAsString(value, selectedItem.toString()); } }); @@ -212,15 +193,9 @@ public void itemStateChanged(ItemEvent e) { + PlotterAdapter.PARAMETER_SUFFIX_LOG_SCALE, I18N.getMessage(I18N.getGUIBundle(), "gui.label.plotter_panel.log_scale.label"), false); changeListenerElements.add(logScaleBox); - logScaleBox.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - plotterSettings.setParameterAsBoolean(PlotterAdapter.PARAMETER_SUFFIX_AXIS - + PlotterAdapter.transformParameterName(plotter.getAxisName(finalAxisIndex)) - + PlotterAdapter.PARAMETER_SUFFIX_LOG_SCALE, logScaleBox.isSelected()); - } - }); + logScaleBox.addActionListener(e -> plotterSettings.setParameterAsBoolean(PlotterAdapter.PARAMETER_SUFFIX_AXIS + + PlotterAdapter.transformParameterName(plotter.getAxisName(finalAxisIndex)) + + PlotterAdapter.PARAMETER_SUFFIX_LOG_SCALE, logScaleBox.isSelected())); this.add(logScaleBox, c); this.add(createFiller(10), c); } @@ -257,22 +232,19 @@ public void actionPerformed(ActionEvent e) { plotList.setCellRenderer(new LineStyleCellRenderer<>(plotter)); - plotList.addListSelectionListener(new ListSelectionListener() { - - @Override - public void valueChanged(ListSelectionEvent e) { - if (!e.getValueIsAdjusting()) { - List list = new LinkedList<>(); - for (int i = 0; i < plotList.getModel().getSize(); i++) { - if (plotList.isSelectedIndex(i)) { - list.add(model.get(i).toString()); - } - } - String result = ParameterTypeEnumeration.transformEnumeration2String(list); - - plotterSettings.setParameterAsString(PlotterAdapter.PARAMETER_PLOT_COLUMNS, result); + plotList.addListSelectionListener(e -> { + if (e.getValueIsAdjusting()) { + return; + } + List list = new LinkedList<>(); + for (int i = 0; i < plotList.getModel().getSize(); i++) { + if (plotList.isSelectedIndex(i)) { + list.add(model.get(i)); } } + String result = ParameterTypeEnumeration.transformEnumeration2String(list); + + plotterSettings.setParameterAsString(PlotterAdapter.PARAMETER_PLOT_COLUMNS, result); }); plotList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); JScrollPane listScrollPane = new ExtendedJScrollPane(plotList); @@ -297,12 +269,10 @@ public void valueChanged(ListSelectionEvent e) { for (int j = 0; j < dataTable.getNumberOfColumns(); j++) { plotCombo.addItem(dataTable.getColumnName(j)); } - plotCombo.addItemListener(new ItemListener() { - - @Override - public void itemStateChanged(ItemEvent e) { - plotterSettings.setParameterAsString(PlotterAdapter.PARAMETER_PLOT_COLUMN, - plotCombo.getSelectedItem().toString()); + plotCombo.addItemListener(e -> { + Object selectedItem = plotCombo.getSelectedItem(); + if (selectedItem != null) { + plotterSettings.setParameterAsString(PlotterAdapter.PARAMETER_PLOT_COLUMN, selectedItem.toString()); } }); @@ -321,15 +291,9 @@ public void itemStateChanged(ItemEvent e) { PlotterAdapter.PARAMETER_PLOT_COLUMNS + PlotterAdapter.PARAMETER_SUFFIX_LOG_SCALE, I18N.getMessage(I18N.getGUIBundle(), "gui.label.plotter_panel.log_scale.label"), false); changeListenerElements.add(logScaleBox); - logScaleBox.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - plotterSettings.setParameterAsBoolean( - PlotterAdapter.PARAMETER_PLOT_COLUMNS + PlotterAdapter.PARAMETER_SUFFIX_LOG_SCALE, - logScaleBox.isSelected()); - } - }); + logScaleBox.addActionListener(e -> plotterSettings.setParameterAsBoolean( + PlotterAdapter.PARAMETER_PLOT_COLUMNS + PlotterAdapter.PARAMETER_SUFFIX_LOG_SCALE, + logScaleBox.isSelected())); this.add(logScaleBox, c); this.add(createFiller(10), c); } @@ -339,13 +303,7 @@ public void actionPerformed(ActionEvent e) { final ListeningJCheckBox sortingBox = new ListeningJCheckBox(PlotterAdapter.PARAMETER_SUFFIX_SORTING, I18N.getMessage(I18N.getGUIBundle(), "gui.label.plotter_panel.sorting.label"), false); changeListenerElements.add(sortingBox); - sortingBox.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - plotterSettings.setParameterAsBoolean(PlotterAdapter.PARAMETER_SUFFIX_SORTING, sortingBox.isSelected()); - } - }); + sortingBox.addActionListener(e -> plotterSettings.setParameterAsBoolean(PlotterAdapter.PARAMETER_SUFFIX_SORTING, sortingBox.isSelected())); this.add(sortingBox, c); this.add(createFiller(10), c); } @@ -355,14 +313,8 @@ public void actionPerformed(ActionEvent e) { final ListeningJCheckBox absoluteBox = new ListeningJCheckBox(PlotterAdapter.PARAMETER_SUFFIX_ABSOLUTE_VALUES, I18N.getMessage(I18N.getGUIBundle(), "gui.label.plotter_panel.abs_values.label"), false); changeListenerElements.add(absoluteBox); - absoluteBox.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - plotterSettings.setParameterAsBoolean(PlotterAdapter.PARAMETER_SUFFIX_ABSOLUTE_VALUES, - absoluteBox.isSelected()); - } - }); + absoluteBox.addActionListener(e -> plotterSettings.setParameterAsBoolean( + PlotterAdapter.PARAMETER_SUFFIX_ABSOLUTE_VALUES, absoluteBox.isSelected())); this.add(absoluteBox, c); this.add(createFiller(10), c); } @@ -380,13 +332,8 @@ public void actionPerformed(ActionEvent e) { zoomingSlider.setToolTipText(toolTip); this.add(zoomingSlider, c); this.add(createFiller(10), c); - zoomingSlider.addChangeListener(new ChangeListener() { - - @Override - public void stateChanged(ChangeEvent e) { - plotterSettings.setParameterAsInt(PlotterAdapter.PARAMETER_SUFFIX_ZOOM_FACTOR, zoomingSlider.getValue()); - } - }); + zoomingSlider.addChangeListener(e -> plotterSettings.setParameterAsInt( + PlotterAdapter.PARAMETER_SUFFIX_ZOOM_FACTOR, zoomingSlider.getValue())); } // jitter @@ -403,13 +350,8 @@ public void stateChanged(ChangeEvent e) { jitterSlider.setMajorTickSpacing(10); this.add(jitterSlider, c); this.add(createFiller(10), c); - jitterSlider.addChangeListener(new ChangeListener() { - - @Override - public void stateChanged(ChangeEvent e) { - plotterSettings.setParameterAsInt(PlotterAdapter.PARAMETER_JITTER_AMOUNT, jitterSlider.getValue()); - } - }); + jitterSlider.addChangeListener(e -> plotterSettings.setParameterAsInt( + PlotterAdapter.PARAMETER_JITTER_AMOUNT, jitterSlider.getValue())); } // option dialog @@ -420,13 +362,7 @@ public void stateChanged(ChangeEvent e) { optionsButton.setToolTipText(toolTip); this.add(optionsButton, c); this.add(createFiller(10), c); - optionsButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - plotter.showOptionsDialog(); - } - }); + optionsButton.addActionListener(e -> plotter.showOptionsDialog()); } // Add the plotter options components for user interaction, if provided @@ -468,22 +404,16 @@ public void actionPerformed(ActionEvent e) { public void updatePlotterCombo() { plotterCombo.removeItemListener(plotterComboListener); plotterCombo.removeAllItems(); - Iterator n = plotterSettings.getAvailablePlotters().keySet().iterator(); - while (n.hasNext()) { - String plotterName = n.next(); + plotterSettings.getAvailablePlotters().forEach((plotterName, plotterClass) -> { try { - Class plotterClass = plotterSettings.getAvailablePlotters().get(plotterName); if (plotterClass != null) { plotterCombo.addItem(plotterName); } - } catch (IllegalArgumentException e) { - LogService.getRoot().log(Level.WARNING, - "com.rapidminer.gui.plotter.PlotterControlPanel.instatiating_plotter_error", plotterName); - } catch (SecurityException e) { + } catch (IllegalArgumentException | SecurityException e) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.plotter.PlotterControlPanel.instatiating_plotter_error", plotterName); } - } + }); plotterCombo.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.action.plotter_panel.select_chart.tip")); plotterCombo.addItemListener(plotterComboListener); } @@ -508,7 +438,7 @@ public void plotterChanged(String plotterName) { * @return the filler label */ private static JLabel createFiller(final int height) { - JLabel label = new JLabel() { + return new JLabel() { private static final long serialVersionUID = 1L; @@ -517,7 +447,6 @@ public Dimension getPreferredSize() { return new Dimension(super.getPreferredSize().width, height); } }; - return label; } } diff --git a/src/main/java/com/rapidminer/gui/processeditor/NewOperatorGroupTree.java b/src/main/java/com/rapidminer/gui/processeditor/NewOperatorGroupTree.java index 27898b719..0aa0499c8 100644 --- a/src/main/java/com/rapidminer/gui/processeditor/NewOperatorGroupTree.java +++ b/src/main/java/com/rapidminer/gui/processeditor/NewOperatorGroupTree.java @@ -22,6 +22,7 @@ import java.awt.Component; import java.awt.Dimension; import java.awt.Point; +import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; @@ -36,6 +37,7 @@ import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.ImageIcon; +import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; @@ -54,6 +56,7 @@ import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.dnd.AbstractPatchedTransferHandler; import com.rapidminer.gui.dnd.OperatorTransferHandler; +import com.rapidminer.gui.dnd.TransferableOperator; import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel; import com.rapidminer.gui.look.Colors; import com.rapidminer.gui.operatortree.actions.InfoOperatorAction; @@ -78,6 +81,7 @@ import com.rapidminer.tools.I18N; import com.rapidminer.tools.ParameterService; import com.rapidminer.tools.usagestats.ActionStatisticsCollector; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector.UsageObject; /** @@ -87,6 +91,31 @@ */ public class NewOperatorGroupTree extends JPanel implements FilterListener, SelectionNavigationListener { + /** + * Simple POJO implementation of {@link UsageObject} to log stats + * using {@link #logSearchTerm(String, String, Operator)}. + * + * @since 8.1.2 + * @author Jan Czogalla + */ + private static final class OperatorTreeUsageObject implements UsageObject { + + private final String event; + private final String searchText; + private final Operator operator; + + private OperatorTreeUsageObject(String event, String searchText, Operator operator) { + this.event = event; + this.searchText = searchText; + this.operator = operator; + } + + @Override + public void logUsage() { + logSearchTerm(event, searchText, operator); + } + } + private static final long serialVersionUID = 133086849304885475L; private final FilterTextField filterField = new FilterTextField(12); @@ -226,15 +255,22 @@ public void valueChanged(final TreeSelectionEvent e) { private static final long serialVersionUID = 1L; @Override - protected List getDraggedOperators() { + public Transferable createTransferable(JComponent c) { Operator selectedOperator = NewOperatorGroupTree.this.getSelectedOperator(); if (selectedOperator == null) { - return Collections.emptyList(); - } else { - logSearchTerm("inserted", filterField.getText()); - return Collections.singletonList(selectedOperator); + return null; } + TransferableOperator transferable = new TransferableOperator(new Operator[]{selectedOperator}); + transferable.setUsageObject(new OperatorTreeUsageObject("inserted", filterField.getText(), selectedOperator)); + return transferable; } + + @Override + protected List getDraggedOperators() { + // noop, overriding createTransferable + return null; + } + }); operatorGroupTree.addMouseListener(new MouseAdapter() { @@ -485,7 +521,7 @@ private void insertSelected() { } MainFrame mainFrame = RapidMinerGUI.getMainFrame(); mainFrame.getActions().insert(Collections.singletonList(operator)); - logSearchTerm("inserted", filterField.getText()); + logSearchTerm("inserted", filterField.getText(), operator); } @Override @@ -532,14 +568,40 @@ public AbstractPatchedTransferHandler getOperatorTreeTransferhandler() { } /** - * Logs the searchText under the given event. Shortens the searchText if is longer than 50 - * characters. + * Logs the searchText under the given event. Shortens the searchText if is longer than 50 characters. + * Does not specify an operator key. */ - private void logSearchTerm(String event, String searchText) { + private static void logSearchTerm(String event, String searchText) { + logSearchTerm(event, searchText, (String) null); + } + + /** + * Logs the searchText under the given event. Shortens the searchText if is longer than 50 characters. + * Extracts the operator key from the given operator. + * + * @since 8.1.2 + */ + private static void logSearchTerm(String event, String searchText, Operator operator) { + logSearchTerm(event, searchText, operator == null ? null : operator.getOperatorDescription().getKey()); + } + + /** + * Logs the searchText under the given event. Shortens the searchText if is longer than 50 characters. + * Uses the specified operator key iff not {@code null}, otherwise uses the empty string. + * + * @since 8.1.2 + */ + private static void logSearchTerm(String event, String searchText, String operatorID) { if (searchText.length() > 50) { searchText = searchText.substring(0, 50) + "[...]"; } - ActionStatisticsCollector.INSTANCE.log(ActionStatisticsCollector.TYPE_OPERATOR_SEARCH, event, searchText); + if (operatorID == null) { + operatorID = ""; + } + StringBuilder arg = new StringBuilder(searchText); + arg.append(ActionStatisticsCollector.ARG_GLOBAL_SEARCH_SPACER); + arg.append(operatorID); + ActionStatisticsCollector.INSTANCE.log(ActionStatisticsCollector.TYPE_OPERATOR_SEARCH, event, arg.toString()); } public FilterTextField getFilterField() { diff --git a/src/main/java/com/rapidminer/gui/processeditor/XMLEditor.java b/src/main/java/com/rapidminer/gui/processeditor/XMLEditor.java index 981fee16e..74861bab5 100644 --- a/src/main/java/com/rapidminer/gui/processeditor/XMLEditor.java +++ b/src/main/java/com/rapidminer/gui/processeditor/XMLEditor.java @@ -37,6 +37,8 @@ import com.rapidminer.Process; import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.RapidMinerGUI; +import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawUtils; +import com.rapidminer.gui.flow.processrendering.view.ProcessRendererView; import com.rapidminer.gui.look.Colors; import com.rapidminer.gui.tools.ExtendedJToolBar; import com.rapidminer.gui.tools.ResourceAction; @@ -137,13 +139,28 @@ public void setSelection(List selection) { } } + /** + * Validates the process represented by the current xml. Will update the actual process if the xml representation + * differs. This diff will ignore white space changes, but will update the displayed xml with the parser + * conform version. + */ public synchronized void validateProcess() throws IOException, XMLException { - Process newExp = new Process(editor.getText().trim()); - if (!newExp.getRootOperator().getXML(true) - .equals(RapidMinerGUI.getMainFrame().getProcess().getRootOperator().getXML(true))) { - Process old = RapidMinerGUI.getMainFrame().getProcess(); - newExp.setProcessLocation(old.getProcessLocation()); - mainFrame.setProcess(newExp, false); + String editorContent = getXMLFromEditor(); + Process oldProcess = RapidMinerGUI.getMainFrame().getProcess(); + String oldXML = oldProcess.getRootOperator().getXML(true); + if (oldXML.trim().equals(editorContent)) { + return; + } + Process newProcess = new Process(editorContent); + ProcessRendererView processRenderer = mainFrame.getProcessPanel().getProcessRenderer(); + ProcessDrawUtils.ensureOperatorsHaveLocation(newProcess, processRenderer.getModel()); + String newXML = newProcess.getRootOperator().getXML(true); + if (!newXML.equals(oldXML)) { + newProcess.setProcessLocation(oldProcess.getProcessLocation()); + mainFrame.setProcess(newProcess, false); + processRenderer.getModel().setProcess(newProcess, false, false); + } else { + setText(oldXML); } } diff --git a/src/main/java/com/rapidminer/gui/processeditor/search/OperatorGlobalSearchGUIProvider.java b/src/main/java/com/rapidminer/gui/processeditor/search/OperatorGlobalSearchGUIProvider.java index 383b9748f..9d0512de5 100644 --- a/src/main/java/com/rapidminer/gui/processeditor/search/OperatorGlobalSearchGUIProvider.java +++ b/src/main/java/com/rapidminer/gui/processeditor/search/OperatorGlobalSearchGUIProvider.java @@ -29,13 +29,17 @@ import java.awt.event.InputEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; import java.util.Collections; import java.util.logging.Level; import javax.swing.BorderFactory; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.SwingWorker; import javax.swing.border.Border; +import javax.swing.event.AncestorEvent; +import javax.swing.event.AncestorListener; import org.apache.lucene.document.Document; @@ -54,6 +58,7 @@ import com.rapidminer.tools.LogService; import com.rapidminer.tools.OperatorService; import com.rapidminer.tools.documentation.GroupDocumentation; +import com.rapidminer.tools.usagestats.DefaultUsageLoggable; /** @@ -67,7 +72,7 @@ public class OperatorGlobalSearchGUIProvider implements GlobalSearchableGUIProvi /** * Drag & Drop support for operators. */ - private static final class OperatorDragGesture implements DragGestureListener { + private static final class OperatorDragGesture extends DefaultUsageLoggable implements DragGestureListener { private final Operator operator; @@ -90,8 +95,15 @@ public void dragGestureRecognized(DragGestureEvent event) { } // set the recommended operator as the Transferable - event.startDrag(cursor, new TransferableOperator(new Operator[]{operator})); + TransferableOperator transferable = new TransferableOperator(new Operator[]{operator}); + if (usageLogger != null) { + transferable.setUsageStatsLogger(usageLogger); + } else if (usageObject != null) { + transferable.setUsageObject(usageObject); + } + event.startDrag(cursor, transferable); } + } private static final Border ICON_EMPTY_BORDER = BorderFactory.createEmptyBorder(0, 5, 0, 15); @@ -130,7 +142,7 @@ public JComponent getGUIListComponentForDocument(final Document document, final name = GlobalSearchGUIUtilities.INSTANCE.createHTMLHighlightFromString(name, bestFragments); // add the "drag here" label to the process panel - mainListPanel.addMouseListener(new MouseAdapter() { + MouseListener hoverAdapter = new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { @@ -141,19 +153,25 @@ public void mouseEntered(MouseEvent e) { public void mouseExited(MouseEvent e) { handleHoverOverOperator(false); } + }; + mainListPanel.addMouseListener(hoverAdapter); + + // remove hover listener when ancestor is being removed + // reason is that it takes enough time after inserting an operator for the original hover listener to fire again + mainListPanel.addAncestorListener(new AncestorListener() { + @Override + public void ancestorAdded(AncestorEvent event) { + // not needed + } + + @Override + public void ancestorRemoved(AncestorEvent event) { + mainListPanel.removeMouseListener(hoverAdapter); + } - /** - * Handle the hover-over-operator events. - * - * @param hovered - * {@code true} if hovering over an operator; {@code false} otherwise - */ - private void handleHoverOverOperator(final boolean hovered) { - ProcessRendererModel modelRenderer = RapidMinerGUI.getMainFrame().getProcessPanel().getProcessRenderer() - .getModel(); - - modelRenderer.setOperatorSourceHovered(hovered); - modelRenderer.fireMiscChanged(); + @Override + public void ancestorMoved(AncestorEvent event) { + // not needed } }); @@ -183,6 +201,9 @@ public void searchResultTriggered(final Document document, final Veto veto) { MainFrame mainFrame = RapidMinerGUI.getMainFrame(); mainFrame.getActions().insert(Collections.singletonList(operator)); mainFrame.getPerspectiveController().showPerspective(PerspectiveModel.DESIGN); + + // make sure "drop here" message vanishes after insert + handleHoverOverOperator(false); } catch (OperatorCreationException e) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.processeditor.global_search.OperatorSearchManager.error.operator_creation_error", e.getMessage()); } @@ -196,12 +217,19 @@ public void searchResultBrowsed(final Document document) { return; } - try { - Operator operator = OperatorService.getOperatorDescription(operatorKey).createOperatorInstance(); - RapidMinerGUI.getMainFrame().getOperatorDocViewer().setDisplayedOperator(operator); - } catch (OperatorCreationException e) { - LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.processeditor.global_search.OperatorSearchManager.error.operator_browse_error", e.getMessage()); - } + new SwingWorker() { + + @Override + protected Void doInBackground() { + try { + Operator operator = OperatorService.getOperatorDescription(operatorKey).createOperatorInstance(); + RapidMinerGUI.getMainFrame().getOperatorDocViewer().setDisplayedOperator(operator); + } catch (OperatorCreationException e) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.processeditor.global_search.OperatorSearchManager.error.operator_browse_error", e.getMessage()); + } + return null; + } + }.execute(); } @Override @@ -224,6 +252,20 @@ public DragGestureListener getDragAndDropSupport(final Document document) { } } + /** + * Handle the hover-over-operator events. + * + * @param hovered + * {@code true} if hovering over an operator; {@code false} otherwise + */ + private void handleHoverOverOperator(final boolean hovered) { + ProcessRendererModel modelRenderer = RapidMinerGUI.getMainFrame().getProcessPanel().getProcessRenderer() + .getModel(); + + modelRenderer.setOperatorSourceHovered(hovered); + modelRenderer.fireMiscChanged(); + } + /** * Creates the full group path for an operator in human-readable form. * diff --git a/src/main/java/com/rapidminer/gui/properties/ListPropertyTable2.java b/src/main/java/com/rapidminer/gui/properties/ListPropertyTable2.java index a535db0b3..2eefb1594 100644 --- a/src/main/java/com/rapidminer/gui/properties/ListPropertyTable2.java +++ b/src/main/java/com/rapidminer/gui/properties/ListPropertyTable2.java @@ -20,12 +20,15 @@ import java.awt.Component; import java.awt.Point; +import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; +import java.util.EventObject; import java.util.LinkedList; import java.util.List; - import javax.swing.JComponent; import javax.swing.JTable; +import javax.swing.KeyStroke; +import javax.swing.event.ChangeEvent; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; @@ -96,6 +99,14 @@ private ListPropertyTable2(ParameterType[] types, List parameterList, requestFocusForLastEditableCell(); } + @Override + protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) { + if (e.getKeyCode() == KeyEvent.VK_TAB) { + stopEditing(); + } + return super.processKeyBinding(ks, e, condition, pressed); + } + private static List to2DimList(List parameterList) { List result = new LinkedList<>(); for (String v : parameterList) { @@ -105,6 +116,7 @@ private static List to2DimList(List parameterList) { } public void addRow() { + stopEditing(); ((ListTableModel) getModel()).addRow(); fillEditors(); @@ -116,6 +128,12 @@ public boolean isEmpty() { return renderers.isEmpty(); } + @Override + public boolean editCellAt(int row, int column, EventObject e){ + stopEditing(); + return super.editCellAt(row,column,e); + } + protected void fillEditors() { while (editors.size() < getModel().getRowCount()) { TableCellRenderer rowRenderers[] = new TableCellRenderer[types.length]; @@ -187,7 +205,11 @@ public void storeParameterList(List parameterList2) { public void stopEditing() { TableCellEditor editor = getCellEditor(); if (editor != null) { - editor.stopCellEditing(); + boolean stoppedCellEditing = editor.stopCellEditing(); + //remove the editor if stopping was successful + if (stoppedCellEditing) { + removeEditor(); + } } } @@ -228,6 +250,21 @@ public String getToolTipText(MouseEvent e) { return toolTipText; } + /** + * This is needed in order to allow auto completion: Otherwise the editor will be immediately + * removed after setting the first selected value and loosing its focus. This way it is ensured + * that the editor won't be removed. + */ + @Override + public void editingStopped(ChangeEvent e) { + + TableCellEditor editor = getCellEditor(); + if (editor != null) { + Object value = editor.getCellEditorValue(); + setValueAt(value, editingRow, editingColumn); + } + } + /* * DO NOT ENABLE THE LIST SELECTION CHANGE METHODS! If you do so and request the focus when * changing selection this leads to strange side effects (like losing focus directly after diff --git a/src/main/java/com/rapidminer/gui/properties/OperatorPropertyPanel.java b/src/main/java/com/rapidminer/gui/properties/OperatorPropertyPanel.java index e8f9ba239..91738c7ec 100644 --- a/src/main/java/com/rapidminer/gui/properties/OperatorPropertyPanel.java +++ b/src/main/java/com/rapidminer/gui/properties/OperatorPropertyPanel.java @@ -33,6 +33,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -158,11 +159,12 @@ public class OperatorPropertyPanel extends PropertyPanel implements Dockable, Pr public void update(Observable observable, String key) { PropertyValueCellEditor editor = getEditorForKey(key); if (editor != null) { + Object editorValueObject = editor.getCellEditorValue(); ParameterType type = operator.getParameters().getParameterType(key); - String editorValue = type.toString(editor.getCellEditorValue()); + String editorValue = type.toString(editorValueObject); String opValue = operator.getParameters().getParameterOrNull(key); - if (opValue != null && editorValue == null || opValue == null && editorValue != null || opValue != null - && editorValue != null && !opValue.equals(editorValue)) { + // Second check prevents an endless validation loop in case opValue and editorValueObject are both null + if (!Objects.equals(opValue, editorValue) && opValue != editorValueObject) { editor.getTableCellEditorComponent(null, opValue, false, 0, 1); } } else { diff --git a/src/main/java/com/rapidminer/gui/properties/PropertyPanel.java b/src/main/java/com/rapidminer/gui/properties/PropertyPanel.java index f500ba33c..f7e953b65 100644 --- a/src/main/java/com/rapidminer/gui/properties/PropertyPanel.java +++ b/src/main/java/com/rapidminer/gui/properties/PropertyPanel.java @@ -35,6 +35,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.logging.Level; import java.util.regex.Pattern; @@ -333,10 +334,9 @@ public void editingCanceled(ChangeEvent e) {} public void editingStopped(ChangeEvent e) { Object valueObj = editor.getCellEditorValue(); String value = type.toString(valueObj); - String last; - last = getValue(type); - if (value != null && last == null || last == null && value != null - || value != null && last != null && !value.equals(last)) { + String last = getValue(type); + // Second check prevents an endless validation loop in case valueObj and last are both null + if (!Objects.equals(value, last) && valueObj != last) { setValue(typesOperator, type, value, false); } } @@ -372,8 +372,6 @@ public void editingStopped(ChangeEvent e) { * * @param type * The ParameterType, for which the panel is created - * @param tooltipText - * The tool tip for the current ParameterType * @param editor * Editor for the current ParameterType * @param editorComponent @@ -499,7 +497,15 @@ protected int getNumberOfEditors() { return currentEditors.size(); } - protected PropertyValueCellEditor getEditorForKey(String key) { + /** + * Returns the editor for the given parameter key. + * + * @param key + * the key + * @return the editor or {@code null} if there is no editor for the given key. + * @since 8.2 + */ + public PropertyValueCellEditor getEditorForKey(String key) { return currentEditors.get(key); } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeFileValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeFileValueCellEditor.java index 964a065a4..786cc3cee 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeFileValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeFileValueCellEditor.java @@ -45,9 +45,11 @@ public class AttributeFileValueCellEditor extends FileValueCellEditor { private transient Operator exampleSource; + private JButton button; + public AttributeFileValueCellEditor(ParameterTypeAttributeFile type) { super(type); - JButton button = new JButton(new ResourceAction(true, "edit_attributefile") { + button = new JButton(new ResourceAction(true, "edit_attributefile") { private static final long serialVersionUID = 1L; @@ -81,4 +83,9 @@ private void buttonPressed() { public boolean rendersLabel() { return false; } + + @Override + public void activate() { + button.doClick(); + } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeOrderingCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeOrderingCellEditor.java index 90a571a3b..2fe658b4a 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeOrderingCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributeOrderingCellEditor.java @@ -130,4 +130,8 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole return button; } + @Override + public void activate() { + button.doClick(); + } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributesValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributesValueCellEditor.java index 966f18704..dc2985c16 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributesValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/AttributesValueCellEditor.java @@ -128,4 +128,9 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole return button; } + @Override + public void activate() { + button.doClick(); + } + } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/ColorValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ColorValueCellEditor.java index 85709f808..8c4236b92 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/ColorValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ColorValueCellEditor.java @@ -18,21 +18,20 @@ */ package com.rapidminer.gui.properties.celleditors.value; -import com.rapidminer.gui.RapidMinerGUI; -import com.rapidminer.gui.tools.components.ColorIcon; -import com.rapidminer.operator.Operator; -import com.rapidminer.parameter.ParameterTypeColor; - import java.awt.Color; import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; - import javax.swing.AbstractCellEditor; import javax.swing.JButton; import javax.swing.JColorChooser; import javax.swing.JTable; +import com.rapidminer.gui.RapidMinerGUI; +import com.rapidminer.gui.tools.components.ColorIcon; +import com.rapidminer.operator.Operator; +import com.rapidminer.parameter.ParameterTypeColor; + /** * Cell editor consisting of a colored button which opens a color chooser as action. Currently only @@ -112,4 +111,9 @@ private Color transformString2Color(String value) { public boolean rendersLabel() { return false; } + + @Override + public void activate() { + button.doClick(); + } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/ConfigurationWizardValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ConfigurationWizardValueCellEditor.java index 76702d7f6..d23b9b07d 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/ConfigurationWizardValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ConfigurationWizardValueCellEditor.java @@ -18,18 +18,17 @@ */ package com.rapidminer.gui.properties.celleditors.value; -import com.rapidminer.gui.tools.ResourceAction; -import com.rapidminer.gui.wizards.ConfigurationWizardCreator; -import com.rapidminer.operator.Operator; -import com.rapidminer.parameter.ParameterTypeConfiguration; - import java.awt.Component; import java.awt.event.ActionEvent; - import javax.swing.AbstractCellEditor; import javax.swing.JButton; import javax.swing.JTable; +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.gui.wizards.ConfigurationWizardCreator; +import com.rapidminer.operator.Operator; +import com.rapidminer.parameter.ParameterTypeConfiguration; + /** * Cell editor consisting of a simple button which opens a configuration wizard for the @@ -95,4 +94,9 @@ public boolean useEditorAsRenderer() { public boolean rendersLabel() { return true; } + + @Override + public void activate() { + button.doClick(); + } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/CronExpressionCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/CronExpressionCellEditor.java index 5248c49e5..536bf141c 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/CronExpressionCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/CronExpressionCellEditor.java @@ -18,11 +18,6 @@ */ package com.rapidminer.gui.properties.celleditors.value; -import com.rapidminer.gui.dialog.CronEditorDialog; -import com.rapidminer.gui.tools.ResourceAction; -import com.rapidminer.operator.Operator; -import com.rapidminer.parameter.ParameterTypeCronExpression; - import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -31,13 +26,17 @@ import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; - import javax.swing.AbstractCellEditor; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.JTextField; +import com.rapidminer.gui.dialog.CronEditorDialog; +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.operator.Operator; +import com.rapidminer.parameter.ParameterTypeCronExpression; + /** * A cell editor for cron expression parameters. Supports the direct specification of cron @@ -165,4 +164,9 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole textField.setText((value == null) ? "" : value.toString()); return panel; } + + @Override + public void activate() { + button.doClick(); + } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/DateFormatValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/DateFormatValueCellEditor.java index f1cdeb205..f851734a8 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/DateFormatValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/DateFormatValueCellEditor.java @@ -214,4 +214,9 @@ public Component getTableCellEditorComponent(JTable table, Object value, boolean public Object getCellEditorValue() { return formatCombo.getSelectedItem(); } + + @Override + public void activate() { + selectButton.doClick(); + } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/DateValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/DateValueCellEditor.java index a7e9b5b1d..c21fa47c6 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/DateValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/DateValueCellEditor.java @@ -165,4 +165,5 @@ private void updateComponents(Object value) { } updatingComponents.set(false); } + } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/EnumerationValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/EnumerationValueCellEditor.java index 1256b0a95..2cef4bd96 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/EnumerationValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/EnumerationValueCellEditor.java @@ -18,22 +18,22 @@ */ package com.rapidminer.gui.properties.celleditors.value; -import com.rapidminer.gui.properties.EnumerationPropertyDialog; -import com.rapidminer.gui.properties.ListPropertyDialog; -import com.rapidminer.gui.tools.ResourceAction; -import com.rapidminer.operator.Operator; -import com.rapidminer.parameter.ParameterTypeEnumeration; - import java.awt.Component; import java.awt.Insets; import java.awt.event.ActionEvent; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; - import javax.swing.AbstractCellEditor; import javax.swing.JButton; import javax.swing.JTable; +import com.rapidminer.gui.properties.EnumerationPropertyDialog; +import com.rapidminer.gui.properties.ListPropertyDialog; +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.operator.Operator; +import com.rapidminer.parameter.ParameterTypeEnumeration; + /** * A cell editor with a button that opens a {@link ListPropertyDialog}. Values generated by this @@ -75,7 +75,6 @@ public void loggedActionPerformed(ActionEvent e) { } }); button.setMargin(new Insets(0, 0, 0, 0)); - // button.setToolTipText(type.getDescription()); setButtonText(); } @@ -87,13 +86,11 @@ public Object getCellEditorValue() { @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int col) { if (value != null) { - LinkedList values = new LinkedList(); - for (String string : ParameterTypeEnumeration.transformString2Enumeration((String) value)) { - values.add(string); - } + LinkedList values = new LinkedList<>(); + values.addAll(Arrays.asList(ParameterTypeEnumeration.transformString2Enumeration((String) value))); this.valuesList = values; } else { - this.valuesList = new LinkedList(); + this.valuesList = new LinkedList<>(); } setButtonText(); return button; @@ -120,4 +117,11 @@ public boolean rendersLabel() { return false; } + @Override + public void activate() { + if (button != null) { + button.doClick(); + } + } + } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/ExpressionValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ExpressionValueCellEditor.java index 5d7373648..8df994b57 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/ExpressionValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ExpressionValueCellEditor.java @@ -225,6 +225,11 @@ public boolean useEditorAsRenderer() { return false; } + @Override + public void activate() { + button.doClick(); + } + /** set the text of the expression and update the panel */ protected void setText(String text) { if (text == null) { diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/FileValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/FileValueCellEditor.java index 67cfda11c..17d1a7d24 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/FileValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/FileValueCellEditor.java @@ -62,6 +62,8 @@ public abstract class FileValueCellEditor extends AbstractCellEditor implements private final GridBagLayout gridBagLayout = new GridBagLayout(); + private JButton button; + public FileValueCellEditor(ParameterTypeFile type) { this.type = type; panel.setLayout(gridBagLayout); @@ -119,6 +121,8 @@ protected void addButton(JButton button, int gridwidth) { c.insets = new Insets(0, 5, 0, 0); gridBagLayout.setConstraints(button, c); panel.add(button); + + this.button = button; } private void buttonPressed() { @@ -166,4 +170,11 @@ public boolean useEditorAsRenderer() { return true; } + @Override + public void activate() { + if (button != null) { + button.doClick(); + } + } + } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/FilterValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/FilterValueCellEditor.java index c5e2c028d..b931982a1 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/FilterValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/FilterValueCellEditor.java @@ -18,20 +18,19 @@ */ package com.rapidminer.gui.properties.celleditors.value; -import com.rapidminer.gui.properties.FilterPropertyDialog; -import com.rapidminer.gui.tools.ResourceAction; -import com.rapidminer.operator.Operator; -import com.rapidminer.parameter.ParameterTypeFilter; - import java.awt.Component; import java.awt.Insets; import java.awt.event.ActionEvent; - import javax.swing.AbstractCellEditor; import javax.swing.CellEditor; import javax.swing.JButton; import javax.swing.JTable; +import com.rapidminer.gui.properties.FilterPropertyDialog; +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.operator.Operator; +import com.rapidminer.parameter.ParameterTypeFilter; + /** * The {@link CellEditor} for the {@link ParameterTypeFilter}. Does nothing except providing a @@ -100,4 +99,9 @@ public boolean rendersLabel() { return false; } + @Override + public void activate() { + button.doClick(); + } + } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/LinkButtonValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/LinkButtonValueCellEditor.java index da3d7f72f..a20b6b968 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/LinkButtonValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/LinkButtonValueCellEditor.java @@ -21,14 +21,14 @@ import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; - -import javax.swing.JComponent; +import java.awt.event.ActionEvent; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.JTextField; import com.rapidminer.gui.properties.DefaultRMCellEditor; import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.gui.tools.components.AbstractLinkButton; import com.rapidminer.gui.tools.components.LinkLocalButton; import com.rapidminer.gui.tools.components.LinkRemoteButton; import com.rapidminer.operator.Operator; @@ -49,6 +49,8 @@ public class LinkButtonValueCellEditor extends DefaultRMCellEditor implements Pr private final JPanel container; + private AbstractLinkButton linkButton; + /** * Creates either a {@link LinkLocalButton} or a {@link LinkRemoteButton} that executes the * action stored in type. @@ -63,7 +65,6 @@ public LinkButtonValueCellEditor(final ParameterTypeLinkButton type) { GridBagConstraints gbc = new GridBagConstraints(); - JComponent linkButton; if (type.isLocalAction()) { linkButton = new LinkLocalButton(type.getAction()); } else { @@ -105,4 +106,9 @@ public Object getCellEditorValue() { public void setOperator(Operator operator) { // do nothing } + + @Override + public void activate() { + linkButton.getAction().actionPerformed(new ActionEvent(linkButton, ActionEvent.ACTION_PERFORMED, "primary_parameter_activated")); + } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/ListValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ListValueCellEditor.java index 403960c79..08b557cf8 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/ListValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ListValueCellEditor.java @@ -18,21 +18,20 @@ */ package com.rapidminer.gui.properties.celleditors.value; -import com.rapidminer.gui.properties.ListPropertyDialog; -import com.rapidminer.gui.tools.ResourceAction; -import com.rapidminer.operator.Operator; -import com.rapidminer.parameter.ParameterTypeList; - import java.awt.Component; import java.awt.Insets; import java.awt.event.ActionEvent; import java.util.LinkedList; import java.util.List; - import javax.swing.AbstractCellEditor; import javax.swing.JButton; import javax.swing.JTable; +import com.rapidminer.gui.properties.ListPropertyDialog; +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.operator.Operator; +import com.rapidminer.parameter.ParameterTypeList; + /** * A cell editor with a button that opens a {@link ListPropertyDialog}. Values generated by this @@ -74,7 +73,6 @@ public void loggedActionPerformed(ActionEvent e) { } }); button.setMargin(new Insets(0, 0, 0, 0)); - // button.setToolTipText(type.getDescription()); setButtonText(); } @@ -111,4 +109,11 @@ public boolean rendersLabel() { return false; } + @Override + public void activate() { + if (button != null) { + button.doClick(); + } + } + } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/MatrixValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/MatrixValueCellEditor.java index ea6635691..4fdd57ff1 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/MatrixValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/MatrixValueCellEditor.java @@ -18,19 +18,18 @@ */ package com.rapidminer.gui.properties.celleditors.value; -import com.rapidminer.gui.properties.MatrixPropertyDialog; -import com.rapidminer.gui.tools.ResourceAction; -import com.rapidminer.operator.Operator; -import com.rapidminer.parameter.ParameterTypeMatrix; -import com.rapidminer.tools.math.StringToMatrixConverter; - import java.awt.Component; import java.awt.event.ActionEvent; - import javax.swing.AbstractCellEditor; import javax.swing.JButton; import javax.swing.JTable; +import com.rapidminer.gui.properties.MatrixPropertyDialog; +import com.rapidminer.gui.tools.ResourceAction; +import com.rapidminer.operator.Operator; +import com.rapidminer.parameter.ParameterTypeMatrix; +import com.rapidminer.tools.math.StringToMatrixConverter; + /** * A cell editor with a button that opens a {@link MatrixPropertyDialog}. Values generated by this @@ -119,4 +118,9 @@ private void setButtonText() { public boolean rendersLabel() { return false; } + + @Override + public void activate() { + button.doClick(); + } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/OAuthValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/OAuthValueCellEditor.java index d7dd331d3..b45e11999 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/OAuthValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/OAuthValueCellEditor.java @@ -52,6 +52,8 @@ public class OAuthValueCellEditor extends DefaultRMCellEditor implements Propert private JPanel container; + private JButton authButton; + public OAuthValueCellEditor(final ParameterTypeOAuth type) { super(new JPasswordField()); this.container = new JPanel(new GridBagLayout()); @@ -89,7 +91,7 @@ public void changedUpdate(DocumentEvent e) { } }); - JButton authButton = new JButton(new ResourceAction(true, "generate_auth_key") { + authButton = new JButton(new ResourceAction(true, "generate_auth_key") { private static final long serialVersionUID = 1L; @@ -147,4 +149,9 @@ public Object getCellEditorValue() { @Override public void setOperator(Operator operator) {} + + @Override + public void activate() { + authButton.doClick(); + } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/PreviewValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/PreviewValueCellEditor.java index 81cdf236e..3a7360a6c 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/PreviewValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/PreviewValueCellEditor.java @@ -18,18 +18,17 @@ */ package com.rapidminer.gui.properties.celleditors.value; -import com.rapidminer.gui.wizards.PreviewCreator; -import com.rapidminer.operator.Operator; -import com.rapidminer.parameter.ParameterTypePreview; - import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; - import javax.swing.AbstractCellEditor; import javax.swing.JButton; import javax.swing.JTable; +import com.rapidminer.gui.wizards.PreviewCreator; +import com.rapidminer.operator.Operator; +import com.rapidminer.parameter.ParameterTypePreview; + /** * Cell editor consisting of a simple button which opens a preview for the corresponding operator. @@ -93,4 +92,9 @@ public boolean useEditorAsRenderer() { public boolean rendersLabel() { return false; } + + @Override + public void activate() { + button.doClick(); + } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/ProcessLocationValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ProcessLocationValueCellEditor.java index d4b34cdf4..7a98e16f7 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/ProcessLocationValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/ProcessLocationValueCellEditor.java @@ -127,6 +127,15 @@ public Component getTableCellEditorComponent(JTable table, Object value, boolean return surroundingPanel; } + @Override + public void activate() { + if (openProcessButton.isEnabled()) { + openProcessButton.doClick(); + } else { + super.activate(); + } + } + /** * Checks whether the provided repository location is valid and is a process. */ diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/PropertyValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/PropertyValueCellEditor.java index eb59617ee..6fb1d56ef 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/PropertyValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/PropertyValueCellEditor.java @@ -18,12 +18,12 @@ */ package com.rapidminer.gui.properties.celleditors.value; -import com.rapidminer.operator.Operator; -import com.rapidminer.parameter.ParameterType; - import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; +import com.rapidminer.operator.Operator; +import com.rapidminer.parameter.ParameterType; + /** * Interface for all value cell editors of property / parameter tables. Please note that the objects @@ -36,17 +36,30 @@ public interface PropertyValueCellEditor extends TableCellEditor, TableCellRenderer { /** This method can be implemented to perform operator specific settings. */ - public void setOperator(Operator operator); + void setOperator(Operator operator); /** * Returns true if this editor should also be used as renderer. Should not be the case for * components with frames around the component like JTextFields. */ - public boolean useEditorAsRenderer(); + boolean useEditorAsRenderer(); /** * Indicates whether this editor renders the parameter type key and does not need to be rendered * by the PropertyPanel. */ - public boolean rendersLabel(); + boolean rendersLabel(); + + /** + * Activates this editor as if the user chose the main action (e.g. clicked the only button). Does nothing by default. + * This is used for interacting with parameter editors, e.g. by double-clicking in the process view on an operator for the primary parameter type. + *

+ * If your custom parameter type should be able to use the primary parameter functionality via {@link ParameterType#setPrimary(boolean)}, your editor must overwrite this method. + *

+ * + * @since 8.2 + */ + default void activate() { + // does nothing by default + } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/RegexpValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RegexpValueCellEditor.java index 758311650..f24ac7eb3 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/RegexpValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RegexpValueCellEditor.java @@ -156,4 +156,9 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole textField.setText(value == null ? "" : value.toString()); return panel; } + + @Override + public void activate() { + button.doClick(); + } } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/RemoteFileValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RemoteFileValueCellEditor.java index 16a3463d9..f681a49f9 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/RemoteFileValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RemoteFileValueCellEditor.java @@ -57,6 +57,8 @@ public class RemoteFileValueCellEditor extends DefaultRMCellEditor implements Pr private JPanel container; + private JButton fileOpenButton; + public RemoteFileValueCellEditor(final ParameterTypeRemoteFile type) { super(new JTextField()); this.container = new JPanel(new GridBagLayout()); @@ -92,7 +94,7 @@ public void changedUpdate(DocumentEvent e) { } }); - final JButton fileOpenButton = new JButton(); + fileOpenButton = new JButton(); fileOpenButton.setAction(new ResourceAction(true, "choose_remote_file") { private static final long serialVersionUID = 1L; @@ -193,4 +195,9 @@ public Object getCellEditorValue() { @Override public void setOperator(Operator operator) {} + @Override + public void activate() { + fileOpenButton.doClick(); + } + } diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/RepositoryLocationValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RepositoryLocationValueCellEditor.java index 2452fc21d..9c34f82cf 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/RepositoryLocationValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/RepositoryLocationValueCellEditor.java @@ -56,6 +56,8 @@ public class RepositoryLocationValueCellEditor extends AbstractCellEditor implem private Operator operator; + private final JButton button; + public RepositoryLocationValueCellEditor(final ParameterTypeRepositoryLocation type) { GridBagLayout gridBagLayout = new GridBagLayout(); panel.setLayout(gridBagLayout); @@ -76,7 +78,7 @@ public void actionPerformed(ActionEvent e) { c.gridwidth = GridBagConstraints.RELATIVE; panel.add(textField, c); - final JButton button = new JButton(new ResourceAction(true, "repository_select_location") { + button = new JButton(new ResourceAction(true, "repository_select_location") { private static final long serialVersionUID = 1L; { @@ -98,17 +100,6 @@ public void loggedActionPerformed(ActionEvent e) { String locationName = RepositoryLocationChooser.selectLocation(processLocation, textField.getText(), panel, type.isAllowEntries(), type.isAllowFolders(), false, type.isEnforceValidRepositoryEntryName(), type.isOnlyWriteableLocations()); - // if (locationName != null) { - // if ((operator != null) && (operator.getProcess() != null)) { - // try { - // RepositoryLocation loc = new RepositoryLocation(processLocation, locationName); - // locationName = operator.getProcess().makeRelativeRepositoryLocation(loc); - // } catch (Exception ex) { - // LogService.getRoot().log(Level.WARNING, - // "Cannot make relative process location for '"+locationName+"': "+ex, ex); - // } - // } - // } if (locationName != null) { textField.setText(locationName); } @@ -200,6 +191,11 @@ public void setOperator(Operator operator) { this.operator = operator; } + @Override + public void activate() { + button.doClick(); + } + /** * @return the panel storing the cell editor text field and button */ diff --git a/src/main/java/com/rapidminer/gui/properties/celleditors/value/TextValueCellEditor.java b/src/main/java/com/rapidminer/gui/properties/celleditors/value/TextValueCellEditor.java index 096c1942f..5f90d531e 100644 --- a/src/main/java/com/rapidminer/gui/properties/celleditors/value/TextValueCellEditor.java +++ b/src/main/java/com/rapidminer/gui/properties/celleditors/value/TextValueCellEditor.java @@ -112,4 +112,9 @@ private void setButtonText() { public boolean rendersLabel() { return false; } + + @Override + public void activate() { + button.doClick(); + } } diff --git a/src/main/java/com/rapidminer/gui/search/GlobalSearchCategoryPanel.java b/src/main/java/com/rapidminer/gui/search/GlobalSearchCategoryPanel.java index 41e0de94e..c32bff368 100644 --- a/src/main/java/com/rapidminer/gui/search/GlobalSearchCategoryPanel.java +++ b/src/main/java/com/rapidminer/gui/search/GlobalSearchCategoryPanel.java @@ -43,6 +43,7 @@ import javax.swing.border.Border; import javax.swing.event.EventListenerList; +import com.rapidminer.tools.usagestats.UsageLoggable; import com.rapidminer.gui.look.Colors; import com.rapidminer.gui.search.event.GlobalSearchInteractionEvent; import com.rapidminer.gui.search.event.GlobalSearchInteractionEvent.InteractionEvent; @@ -56,6 +57,7 @@ import com.rapidminer.tools.LogService; import com.rapidminer.tools.Tools; import com.rapidminer.tools.usagestats.ActionStatisticsCollector; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector.UsageObject; /** @@ -66,13 +68,38 @@ */ class GlobalSearchCategoryPanel extends JPanel { + /** + * Simple POJO implementation of {@link UsageObject} to log stats + * using {@link ActionStatisticsCollector#logGlobalSearchAction(String, String, String)}. + * + * @since 8.1.2 + * @author Jan Czogalla + */ + private static final class GlobalSearchActionUsageObject implements UsageObject{ + + private final String query; + private final String categoryID; + private final String rowID; + + private GlobalSearchActionUsageObject(String query, String categoryID, String rowID) { + this.query = query; + this.categoryID = categoryID; + this.rowID = rowID; + } + + @Override + public void logUsage() { + ActionStatisticsCollector.getInstance().logGlobalSearchAction(query, categoryID, rowID); + } + } private static final Color BORDER_COLOR = Colors.BUTTON_BORDER_DISABLED; private static final Border TOP_BORDER = BorderFactory.createMatteBorder(1, 0, 0, 0, BORDER_COLOR); private static final Border DIVIDER_BORDER = BorderFactory.createMatteBorder(0, 1, 0, 0, BORDER_COLOR); private static final Border CATEGORY_LABEL_EMPTY_BORDER = BorderFactory.createEmptyBorder(10, 10, 10, 10); private static final Border CATEGORY_COMPONENT_EMPTY_BORDER = BorderFactory.createEmptyBorder(4, 5, 4, 0); - private static final Dimension I18N_NAME_SIZE = new Dimension(100, 30); + public static final int I18N_NAME_WIDTH = 100; + private static final Dimension I18N_NAME_SIZE = new Dimension(I18N_NAME_WIDTH, 30); /** event listener for this panel */ private final EventListenerList eventListener; @@ -288,7 +315,7 @@ public void mousePressed(MouseEvent e) { provider.searchResultTriggered(row.getDoc(), providerVeto); // only fire interaction if provider did not veto it if (!providerVeto.isVeto()) { - ActionStatisticsCollector.getInstance().logGlobalSearchAction(controller.getLastQuery(), categoryId, row.getDoc().getField(GlobalSearchUtilities.FIELD_NAME).stringValue()); + ActionStatisticsCollector.getInstance().logGlobalSearchAction(controller.getLastQuery(), categoryId, row.getDoc().getField(GlobalSearchUtilities.FIELD_UNIQUE_ID).stringValue()); fireInteraction(InteractionEvent.RESULT_ACTIVATED, row); @@ -319,6 +346,9 @@ public void mouseExited(MouseEvent e) { if (provider.isDragAndDropSupported(row.getDoc())) { DragGestureListener dragSupport = provider.getDragAndDropSupport(row.getDoc()); if (dragSupport != null) { + if (dragSupport instanceof UsageLoggable) { + ((UsageLoggable) dragSupport).setUsageObject(new GlobalSearchActionUsageObject(controller.getLastQuery(), categoryId, row.getDoc().getField(GlobalSearchUtilities.FIELD_UNIQUE_ID).stringValue())); + } DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(component, DnDConstants.ACTION_MOVE, dragSupport); } else { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.search.globalsearchdialog.missing_drag_support", categoryId); @@ -344,5 +374,4 @@ private void fireInteraction(final InteractionEvent type, final GlobalSearchRow listener.interaction(e); } } - } diff --git a/src/main/java/com/rapidminer/gui/search/GlobalSearchController.java b/src/main/java/com/rapidminer/gui/search/GlobalSearchController.java index 963d47a74..98158e63a 100644 --- a/src/main/java/com/rapidminer/gui/search/GlobalSearchController.java +++ b/src/main/java/com/rapidminer/gui/search/GlobalSearchController.java @@ -48,6 +48,7 @@ final class GlobalSearchController { /** number of search results that are displayed by default if only a single category is searched */ private static final int NUMBER_OF_RESULTS_SINGLE_CAT = 10; + private static final int LOGGING_DELAY = 1000; private final GlobalSearchModel model; private final GlobalSearchPanel searchPanel; @@ -57,6 +58,7 @@ final class GlobalSearchController { /** counts the number of global searches. Always incrementing when a new search is triggered */ private final AtomicInteger searchCounter; + private Timer updateTimer; /** @@ -172,8 +174,6 @@ private void searchOrAppendForCategory(final ScoreDoc offset, final String query final int searchCountSnapshot = searchCounter.get(); SwingWorker worker = new SwingWorker() { - private Timer updateTimer; - @Override protected GlobalSearchResult doInBackground() throws Exception { model.setPending(categoryId, true); @@ -195,10 +195,13 @@ protected void done() { // no error when grabbing result? Good, search worked, reset error on model and provide results to it model.setError(null); - if(updateTimer != null) { + if (updateTimer != null) { updateTimer.stop(); } - updateTimer = new Timer(1000, e -> logSearch(result)); + updateTimer = new Timer(LOGGING_DELAY, e -> { + logSearch(result); + updateTimer = null; + }); updateTimer.setRepeats(false); updateTimer.start(); @@ -221,7 +224,7 @@ protected void done() { * @param result from a search in the global search framework */ private void logSearch(GlobalSearchResult result) { - if(result != null) { + if (result != null) { String searchTerm = lastQuery; long numResults = result.getPotentialNumberOfResults(); ActionStatisticsCollector.getInstance().logGlobalSearch(ActionStatisticsCollector.VALUE_TIMEOUT, searchTerm, categoryId, numResults); @@ -242,7 +245,7 @@ private void handleSearchError(ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceof ParseException) { message = cause.getMessage() != null ? cause.getMessage() : cause.toString(); - } else if (cause == null ){ + } else if (cause == null) { message = e.getMessage(); } diff --git a/src/main/java/com/rapidminer/gui/search/GlobalSearchPanel.java b/src/main/java/com/rapidminer/gui/search/GlobalSearchPanel.java index 0a4e42c79..416e10136 100644 --- a/src/main/java/com/rapidminer/gui/search/GlobalSearchPanel.java +++ b/src/main/java/com/rapidminer/gui/search/GlobalSearchPanel.java @@ -21,10 +21,13 @@ import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.List; @@ -32,7 +35,6 @@ import javax.swing.ButtonGroup; import javax.swing.ImageIcon; import javax.swing.InputMap; -import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JPopupMenu; @@ -44,7 +46,6 @@ import javax.swing.event.PopupMenuListener; import com.rapidminer.gui.ApplicationFrame; -import com.rapidminer.gui.look.Colors; import com.rapidminer.gui.look.RapidLookTools; import com.rapidminer.gui.properties.PropertyPanel; import com.rapidminer.gui.search.event.GlobalSearchCategoryEvent; @@ -58,7 +59,6 @@ import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.TextFieldWithAction; import com.rapidminer.gui.tools.components.DropDownPopupButton; -import com.rapidminer.gui.tools.components.composite.CompositeButton; import com.rapidminer.search.GlobalSearchCategory; import com.rapidminer.search.GlobalSearchRegistry; import com.rapidminer.search.GlobalSearchResult; @@ -88,17 +88,15 @@ public final class GlobalSearchPanel extends JPanel { private static final int MAX_RESULT_DIALOG_HEIGHT = 820; private static final int SEARCH_FIELD_WIDTH = 205; private static final int FILTER_BUTTON_WIDTH = 90; - private static final int SEARCH_BUTTON_WIDTH = 90; private static final int MAX_CATEGORY_I18N_LENGTH = 11; - public static final int PREFERRED_WIDTH = SEARCH_FIELD_WIDTH + FILTER_BUTTON_WIDTH + SEARCH_BUTTON_WIDTH; + public static final int PREFERRED_WIDTH = SEARCH_FIELD_WIDTH + FILTER_BUTTON_WIDTH; private FilterTextField searchField; private TextFieldWithAction searchActionField; private DropDownPopupButton filterButton; - private JButton searchButton; private JPopupMenu filterMenu; private JRadioButtonMenuItem allCategoriesButton; @@ -177,6 +175,7 @@ public boolean requestFocusInWindow() { return searchField.requestFocusInWindow(); } else { searchField.selectAll(); + reopenResults(); return true; } } @@ -201,8 +200,8 @@ public void initializeSearchResultVisualization() { @Override public Dimension getPreferredSize() { - // make sure width is same as this panel - int width = searchActionField.getWidth() + filterButton.getWidth() + searchButton.getWidth(); + // make sure width is same as this panel + width of the i18n label + int width = PREFERRED_WIDTH + GlobalSearchCategoryPanel.I18N_NAME_WIDTH + 1; Dimension prefSize = super.getPreferredSize(); prefSize.width = width; @@ -279,7 +278,9 @@ private void setPending(final boolean pending) { private void showResults() { // show result dialog immediately to visualize search is ongoing if (resultDialog != null) { - resultDialog.setLocation(searchActionField.getLocationOnScreen().x, searchActionField.getLocationOnScreen().y + searchActionField.getHeight() - 1); + // align dialog to the right + resultDialog.setLocation(searchActionField.getLocationOnScreen().x - GlobalSearchCategoryPanel.I18N_NAME_WIDTH - 1, + searchActionField.getLocationOnScreen().y + searchActionField.getHeight() - 1); resultDialog.setVisible(true); } } @@ -408,10 +409,11 @@ public void selected() { }); // clicking away from the search field should close the search popup // this is the next best thing because we cannot detect focus loss on dialog as dialog cannot be focused anymore + // also open dialog again if the search field gains focus again searchField.addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { - // not needed + reopenResults(); } @Override @@ -424,6 +426,16 @@ public void focusLost(FocusEvent e) { } } }); + // open dialog after it was closed with escape + searchField.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (!SwingUtilities.isLeftMouseButton(e)) { + return; + } + reopenResults(); + } + }); searchActionField = new TextFieldWithAction(searchField, clearFilterAction, CLEAR_FILTER_HOVERED_ICON) { @@ -446,7 +458,7 @@ public Dimension getPreferredSize() { gbc.weightx = 0.0d; gbc.fill = GridBagConstraints.NONE; DropDownPopupButton.DropDownPopupButtonBuilder builder = new DropDownPopupButton.DropDownPopupButtonBuilder(); - filterButton = builder.with(new ResourceActionAdapter("global_search.toolbar")).setComposite(SwingUtilities.CENTER).build(); + filterButton = builder.with(new ResourceActionAdapter("global_search.toolbar")).setComposite(SwingUtilities.RIGHT).build(); filterButton.addPopupMenuListener(new PopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { @@ -454,7 +466,12 @@ public void popupMenuWillBecomeVisible(PopupMenuEvent e) { populateFilterPopup((JPopupMenu) source); filterMenu = (JPopupMenu) source; - filterMenu.setPreferredSize(new Dimension(filterButton.getWidth() + searchButton.getWidth(), filterMenu.getPreferredSize().height)); + // align popup to the right and make it at most as wide as the whole search panel + Dimension preferredSize = filterMenu.getPreferredSize(); + int width = Math.min(PREFERRED_WIDTH, preferredSize.width); + filterMenu.setPreferredSize(new Dimension(width, preferredSize.height)); + Point buttonLocation = filterButton.getLocationOnScreen(); + filterMenu.setLocation(buttonLocation.x + filterButton.getWidth() - width, buttonLocation.y + filterButton.getHeight() - 1); } @Override @@ -473,21 +490,6 @@ public void popupMenuCanceled(PopupMenuEvent e) { filterButton.setPreferredSize(new Dimension(FILTER_BUTTON_WIDTH, PropertyPanel.VALUE_CELL_EDITOR_HEIGHT)); add(filterButton, gbc); - gbc.gridx += 1; - searchButton = new CompositeButton(I18N.getGUILabel("global_search.run_search.label"), SwingConstants.RIGHT); - searchButton.setToolTipText(I18N.getGUILabel("global_search.run_search.tip")); - searchButton.addActionListener(e -> { - - controller.handleSearch(searchField.getText(), categoryFilter); - // give focus back to search field - requestFocusInWindow(); - }); - searchButton.setPreferredSize(new Dimension(SEARCH_BUTTON_WIDTH, PropertyPanel.VALUE_CELL_EDITOR_HEIGHT)); - searchButton.putClientProperty(RapidLookTools.PROPERTY_BUTTON_HIGHLIGHT, Boolean.TRUE); - searchButton.putClientProperty(RapidLookTools.PROPERTY_BUTTON_DARK_BORDER, Boolean.TRUE); - searchButton.setForeground(Colors.WHITE); - add(searchButton, gbc); - InputMap inputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); ActionMap actionMap = getActionMap(); @@ -502,6 +504,23 @@ public void actionPerformed(ActionEvent e) { }); } + /** + * Opens the result dialog if it is not visible right now and the search field is not empty. This method is used to + * support one-click functionality, i.e. showing the results after either the search field gains focus again or + * was clicked after the result dialog was closed with the ESC key. + * + * @since 8.2 + */ + private void reopenResults() { + if (searchField.getText().trim().isEmpty()) { + return; + } + if (resultDialog != null && resultDialog.isVisible()) { + return; + } + showResults(); + } + /** * Log the values in case the global search focus was lost intentionally. Make sure this is only called if the user * cancels it. diff --git a/src/main/java/com/rapidminer/gui/security/Wallet.java b/src/main/java/com/rapidminer/gui/security/Wallet.java index b00cf484d..5be4959d4 100644 --- a/src/main/java/com/rapidminer/gui/security/Wallet.java +++ b/src/main/java/com/rapidminer/gui/security/Wallet.java @@ -36,6 +36,10 @@ import com.rapidminer.gui.tools.VersionNumber; import com.rapidminer.io.Base64; import com.rapidminer.io.process.XMLTools; +import com.rapidminer.repository.Repository; +import com.rapidminer.repository.RepositoryManager; +import com.rapidminer.repository.RepositoryManagerListener; +import com.rapidminer.repository.internal.remote.RemoteRepository; import com.rapidminer.tools.FileSystemService; import com.rapidminer.tools.GlobalAuthenticator; import com.rapidminer.tools.I18N; @@ -85,11 +89,11 @@ public class Wallet { * * @return */ - public static Wallet getInstance() { + public static synchronized Wallet getInstance() { return instance; } - public static void setInstance(Wallet wallet) { + public static synchronized void setInstance(Wallet wallet) { instance = wallet; } @@ -225,6 +229,22 @@ public int size() { */ public Wallet() { super(); + RepositoryManager.getInstance(null).addRepositoryManagerListener(new RepositoryManagerListener() { + @Override + public void repositoryWasAdded(Repository repository) { + //Not needed since this only cares about deletions. + } + + @Override + public void repositoryWasRemoved(Repository repository) { + if (repository instanceof RemoteRepository) { + String url = ((RemoteRepository) repository).getBaseUrl().toString(); + String id = ((RemoteRepository) repository).getAlias(); + Wallet.getInstance().removeEntry(id, url); + Wallet.getInstance().saveCache(); + } + } + }); } /** diff --git a/src/main/java/com/rapidminer/gui/tools/ExtendedJList.java b/src/main/java/com/rapidminer/gui/tools/ExtendedJList.java index 4e0984475..d233ece66 100644 --- a/src/main/java/com/rapidminer/gui/tools/ExtendedJList.java +++ b/src/main/java/com/rapidminer/gui/tools/ExtendedJList.java @@ -21,15 +21,13 @@ import java.awt.Dimension; import java.awt.event.MouseEvent; -import javax.swing.JList; - /** * Extended JList which provides tool tips in combination with an {@link ExtendedListModel}. * * @author Tobias Malbrecht, Ingo Mierswa */ -public class ExtendedJList extends JList { +public class ExtendedJList extends MenuShortcutJList { public static final long serialVersionUID = 9032182018402L; diff --git a/src/main/java/com/rapidminer/gui/tools/ExtendedJScrollPane.java b/src/main/java/com/rapidminer/gui/tools/ExtendedJScrollPane.java index e343a15f2..234eb76cd 100644 --- a/src/main/java/com/rapidminer/gui/tools/ExtendedJScrollPane.java +++ b/src/main/java/com/rapidminer/gui/tools/ExtendedJScrollPane.java @@ -19,7 +19,7 @@ package com.rapidminer.gui.tools; import java.awt.Component; - +import javax.swing.JScrollBar; import javax.swing.JScrollPane; @@ -48,6 +48,31 @@ public ExtendedJScrollPane(Component component) { ExtendedJTable table = (ExtendedJTable) component; table.setExtendedScrollPane(this); } + + // scrollpane in scrollpane support + addMouseWheelListener(e -> { + + // horizontal scrolling is done with shift held down + // this works for both Windows as well as OS X touchpad + boolean scrollVertical = !e.isShiftDown() || !getHorizontalScrollBar().isShowing(); + JScrollBar scrollBar = scrollVertical ? getVerticalScrollBar() : getHorizontalScrollBar(); + // if no scrollbar is shown, just dispatch scroll event to parent so parent scrollpane can actually scroll + if (!scrollBar.isShowing()) { + getParent().dispatchEvent(e); + } + // if vertical scrollbar is at top position, allow parent scrollpane to scroll up as well + // OR + // if horizontal scrollbar is at leftmost position, allow parent scrollpane to scroll left as well + if (e.getWheelRotation() < 0 && scrollBar.getValue() == scrollBar.getMinimum()) { + getParent().dispatchEvent(e); + } + // if vertical scrollbar is at bottom position, allow parent scrollpane to scroll down as well + // OR + // if horizontal scrollbar is at rightmost position, allow parent scrollpane to scroll right as well + if (e.getWheelRotation() > 0 && scrollBar.getValue() + scrollBar.getVisibleAmount() == scrollBar.getMaximum()) { + getParent().dispatchEvent(e); + } + }); } @Override diff --git a/src/main/java/com/rapidminer/gui/tools/LeanFormatter.java b/src/main/java/com/rapidminer/gui/tools/LeanFormatter.java index f280d7a65..61d4a449a 100644 --- a/src/main/java/com/rapidminer/gui/tools/LeanFormatter.java +++ b/src/main/java/com/rapidminer/gui/tools/LeanFormatter.java @@ -24,6 +24,8 @@ import java.util.logging.Formatter; import java.util.logging.LogRecord; +import com.rapidminer.parameter.ParameterTypeDateFormat; + /** * A simple log formatter for dates. It outputs the format "yyyy-MM-dd HH:mm:ss" @@ -33,7 +35,7 @@ */ public class LeanFormatter extends Formatter { - private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private final DateFormat dateFormat = new SimpleDateFormat(ParameterTypeDateFormat.DATE_TIME_FORMAT_YYYY_MM_DD_HH_MM_SS); @Override public String format(LogRecord record) { diff --git a/src/main/java/com/rapidminer/gui/tools/MenuShortcutJList.java b/src/main/java/com/rapidminer/gui/tools/MenuShortcutJList.java new file mode 100644 index 000000000..2413449bb --- /dev/null +++ b/src/main/java/com/rapidminer/gui/tools/MenuShortcutJList.java @@ -0,0 +1,110 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.gui.tools; + +import java.awt.Component; +import java.awt.Toolkit; +import java.awt.event.MouseEvent; +import java.util.Vector; +import javax.swing.JList; +import javax.swing.ListModel; +import javax.swing.ListSelectionModel; + + +/** + * A simple {@link JList} that can ignore the menu shortcut key when clicking. + * Ignoring that key might implicate a constraint to be single selection only. + * + * @since 8.2 + * @author Jan Czogalla + */ +public class MenuShortcutJList extends JList { + + private boolean controlEnabled = true; + + public MenuShortcutJList() { + super(); + } + + public MenuShortcutJList(boolean controlEnabled) { + super(); + setControlEnabled(controlEnabled); + } + + public MenuShortcutJList(E[] listData) { + super(listData); + } + + public MenuShortcutJList(E[] listData, boolean controlEnabled) { + super(listData); + setControlEnabled(controlEnabled); + } + + public MenuShortcutJList(Vector listData) { + super(listData); + } + + public MenuShortcutJList(Vector listData, boolean controlEnabled) { + super(listData); + setControlEnabled(controlEnabled); + } + + public MenuShortcutJList(ListModel dataModel) { + super(dataModel); + } + + public MenuShortcutJList(ListModel dataModel, boolean controlEnabled) { + super(dataModel); + setControlEnabled(controlEnabled); + } + + @Override + public void processMouseEvent(MouseEvent e) { + if (!controlEnabled && SwingTools.isControlOrMetaDown(e)) { + // remove menu shortcut key mask from event + int modifiers = e.getModifiers(); + modifiers &= ~Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); + e = new MouseEvent((Component) e.getSource(), e.getID(), e.getWhen(), modifiers, + e.getX(), e.getY(), e.getClickCount(), e.isPopupTrigger()); + } + super.processMouseEvent(e); + } + + /** + * @return whether the menu shortcut key is taken into account. + */ + public boolean isControlEnabled() { + return controlEnabled; + } + + /** + * Sets whether the menu shortcut key should be taken into account. If it should be ignored, + * i.e. {@code controlEnabled} is {@code false}, the selection mode will be automatically + * set to {@link ListSelectionModel#SINGLE_SELECTION}. + * + * @param controlEnabled + * whether the menu shortcut key should be taken into account + */ + public void setControlEnabled(boolean controlEnabled) { + this.controlEnabled = controlEnabled; + if (!controlEnabled) { + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + } + } +} diff --git a/src/main/java/com/rapidminer/gui/tools/MouseWheelController.java b/src/main/java/com/rapidminer/gui/tools/MouseWheelController.java new file mode 100644 index 000000000..1a06f2d52 --- /dev/null +++ b/src/main/java/com/rapidminer/gui/tools/MouseWheelController.java @@ -0,0 +1,148 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.gui.tools; + +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.util.Objects; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; + + +/** + * This class allows you to control the units scrolled for each mouse wheel rotation relative to + * the unit increment value of the {@link JScrollBar}. Specifying a scroll amount of 1, is equivalent to clicking + * the unit scroll button of the scroll bar once. + * + * @author Rob Camick, Jan Czogalla + * @see Mouse Wheel Controller + * @see Licensing + * @since 8.2 + */ +public class MouseWheelController implements MouseWheelListener { + private JScrollPane scrollPane; + private int scrollAmount = 0; + private MouseWheelListener[] realListeners; + + /** + * Convenience constructor to create the class with a scroll amount of 1. + * + * @param scrollPane + * the scroll pane being used by the mouse wheel + */ + public MouseWheelController(JScrollPane scrollPane) { + this(scrollPane, 1); + } + + /** + * Create the class with the specified scroll amount. + * + * @param scrollAmount + * the scroll amount to by used for this scroll pane + * @param scrollPane + * the scroll pane being used by the mouse wheel + */ + public MouseWheelController(JScrollPane scrollPane, int scrollAmount) { + this.scrollPane = Objects.requireNonNull(scrollPane); + setScrollAmount(scrollAmount); + install(); + } + + /** + * @returns the scroll amount. + */ + public int getScrollAmount() { + return scrollAmount; + } + + /** + * Set the scroll amount. Controls the amount the {@link JScrollPane} will scroll for each mouse wheel rotation. + * The amount is relative to the unit increment value of the scrollbar being scrolled. + * + * @param scrollAmount + * an integer value. A value of zero will use the + * default scroll amount for your OS. + */ + public void setScrollAmount(int scrollAmount) { + this.scrollAmount = scrollAmount; + } + + /** + * Install this class as the default {@link MouseWheelListener} for {@link MouseWheelEvent MouseWheelEvents}. + * + * Original listeners will be moved to the realListeners. Can be undone via {@link MouseWheelController#uninstall()}. + */ + public void install() { + if (realListeners != null) { + return; + } + + // Keep track of original listeners so we can use them to redispatch an altered MouseWheelEvent + realListeners = scrollPane.getMouseWheelListeners(); + + for (MouseWheelListener mwl : realListeners) { + scrollPane.removeMouseWheelListener(mwl); + } + + // Intercept events so they can be redispatched + scrollPane.addMouseWheelListener(this); + } + + /** + * Remove the class as the default {@link MouseWheelListener} and reinstall the original listeners. + */ + public void uninstall() { + if (realListeners == null) { + return; + } + + // Remove this class as the default listener + scrollPane.removeMouseWheelListener(this); + + // Install the default listeners + for (MouseWheelListener mwl : realListeners) { + scrollPane.addMouseWheelListener(mwl); + } + + realListeners = null; + } + + /** + * Redispatch a {@link MouseWheelEvent} to the real {@link MouseWheelListener MouseWheelListeners} + */ + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + // Create an altered event to redispatch + if (scrollAmount != 0) { + e = createScrollAmountEvent(e); + } + + // Redispatch the event to original MouseWheelListener + for (MouseWheelListener mwl : realListeners) { + mwl.mouseWheelMoved(e); + } + } + + private MouseWheelEvent createScrollAmountEvent(MouseWheelEvent e) { + // Reset the scroll amount + return new MouseWheelEvent(e.getComponent(), e.getID(), e.getWhen(), e.getModifiers(), + e.getX(), e.getY(), e.getXOnScreen(), e.getYOnScreen(), + e.getClickCount(), e.isPopupTrigger(), e.getScrollType(), scrollAmount, e.getWheelRotation()); + } +} diff --git a/src/main/java/com/rapidminer/gui/tools/OperatorList.java b/src/main/java/com/rapidminer/gui/tools/OperatorList.java index 33044f284..cfbfc2337 100644 --- a/src/main/java/com/rapidminer/gui/tools/OperatorList.java +++ b/src/main/java/com/rapidminer/gui/tools/OperatorList.java @@ -26,9 +26,7 @@ import java.util.Vector; import javax.swing.Action; -import javax.swing.JList; import javax.swing.JPopupMenu; -import javax.swing.ListSelectionModel; import com.rapidminer.gui.dnd.OperatorTransferHandler; import com.rapidminer.gui.operatortree.actions.InfoOperatorAction; @@ -45,7 +43,7 @@ * * @author Helge Homburg, Ingo Mierswa */ -public class OperatorList extends JList implements MouseListener { +public class OperatorList extends MenuShortcutJList implements MouseListener { private static final long serialVersionUID = -2719941529572427942L; @@ -79,6 +77,7 @@ public OperatorList() { /** Creates a new instance of OperatorList */ public OperatorList(boolean horizontalWrap, boolean coloredCellBackgrounds) { + super(false); operatorDialogCellRenderer = new OperatorListCellRenderer(coloredCellBackgrounds); if (horizontalWrap) { setLayoutOrientation(HORIZONTAL_WRAP); @@ -86,7 +85,6 @@ public OperatorList(boolean horizontalWrap, boolean coloredCellBackgrounds) { } setFixedCellHeight(PropertyPanel.VALUE_CELL_EDITOR_HEIGHT); setCellRenderer(operatorDialogCellRenderer); - setSelectionMode(ListSelectionModel.SINGLE_SELECTION); addMouseListener(this); setDragEnabled(true); diff --git a/src/main/java/com/rapidminer/gui/tools/ProcessGUITools.java b/src/main/java/com/rapidminer/gui/tools/ProcessGUITools.java index e36df3638..35ffbf53d 100644 --- a/src/main/java/com/rapidminer/gui/tools/ProcessGUITools.java +++ b/src/main/java/com/rapidminer/gui/tools/ProcessGUITools.java @@ -22,13 +22,14 @@ import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.lang.ref.WeakReference; - +import java.util.Collections; +import java.util.List; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; +import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.KeyStroke; @@ -59,12 +60,16 @@ import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.OutputPort; import com.rapidminer.operator.ports.Port; +import com.rapidminer.operator.ports.quickfix.ConnectLastOperatorToOutputPortsQuickFix; +import com.rapidminer.operator.ports.quickfix.ParameterSettingQuickFix; +import com.rapidminer.operator.ports.quickfix.QuickFix; import com.rapidminer.parameter.CombinedParameterType; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeAttributes; import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.tools.I18N; import com.rapidminer.tools.ParameterService; +import com.rapidminer.tools.container.Pair; /** @@ -81,7 +86,7 @@ private static class BubbleDelegator { private WeakReference bubble; private void setBubbleWindow(BubbleWindow bubble) { - this.bubble = new WeakReference(bubble); + this.bubble = new WeakReference<>(bubble); } /** @@ -114,64 +119,38 @@ public static BubbleWindow displayBubbleForUserError(final UserError error) { if (error instanceof PortUserError) { PortUserError userError = (PortUserError) error; - if (userError.getCode() == 149) { + Port port = userError.getPort(); + int errorCode = error.getCode(); + if (errorCode == 149) { // for "no data" errors we display an error bubble instead of a dialog - return displayInputPortNoDataInformation(userError.getPort()); - } else if (userError.getCode() == 156) { - return displayInputPortWrongTypeInformation(userError.getPort(), userError.getExpectedType(), - userError.getActualType()); - } else { - return displayGenericPortError(userError); + return displayInputPortNoDataInformation(port); } - } else if (error.getClass().equals(UndefinedParameterError.class)) { - UndefinedParameterError userError = (UndefinedParameterError) error; - if (userError.getCode() == 205 || userError.getCode() == 217) { - // for "missing mandatory parameter" errors we display an error bubble - // instead of a dialog - Operator op = userError.getOperator(); - ParameterType param = op != null ? op.getParameterType(userError.getKey()) : null; - if (op != null && param != null) { - return displayMissingMandatoryParameterInformation(op, param); - } else { - return displayGenericUserError(error); - } - } else { - Operator op = userError.getOperator(); - ParameterType param = op != null ? op.getParameterType(userError.getKey()) : null; - if (op != null && param != null) { - return displayMissingMandatoryParameterInformation(op, param); - } else { - return displayGenericUserError(error); - } + if (errorCode == 156) { + return displayInputPortWrongTypeInformation(port, userError.getExpectedType(), userError.getActualType()); } - } else if (error instanceof ParameterError) { + return displayGenericPortError(userError); + } + if (error instanceof ParameterError) { ParameterError userError = (ParameterError) error; Operator op = userError.getOperator(); - ParameterType param = op != null ? op.getParameterType(userError.getKey()) : null; + String errorKey = userError.getKey(); + ParameterType param = op != null ? op.getParameterType(errorKey) : null; if (op != null && param != null) { + if (userError.getClass() == UndefinedParameterError.class) { + return displayMissingMandatoryParameterInformation(op, param); + } + if (userError instanceof AttributeNotFoundError) { + return displayAttributeNotFoundParameterInformation((AttributeNotFoundError) userError); + } return displayGenericParameterError(userError); - } else { - return displayGenericUserError(error); - } - } else if (error instanceof AttributeNotFoundError) { - AttributeNotFoundError userError = (AttributeNotFoundError) error; - Operator op = userError.getOperator(); - ParameterType param = op != null ? op.getParameterType(userError.getKey()) : null; - if (op != null && param != null) { - return displayAttributeNotFoundParameterInformation(userError); - } else { - return displayGenericUserError(error); } } else if (error instanceof ProcessExecutionUserErrorError) { ProcessExecutionUserErrorError userError = (ProcessExecutionUserErrorError) error; if (userError.getUserError() != null && userError.getUserError().getOperator() != null) { return displayUserErrorInExecutedProcess(userError); - } else { - return displayGenericUserError(error); } - } else { - return displayGenericUserError(error); } + return displayGenericUserError(error); } /** @@ -191,15 +170,29 @@ public static PortInfoBubble displayPrecheckNoResultPortInformation(final Proces throw new IllegalArgumentException("port must not be null!"); } + final BubbleDelegator bubbleDelegator = new BubbleDelegator(); Port firstResultPort = process.getRootOperator().getSubprocess(0).getInnerSinks().getPortByIndex(0); - JButton ackButton = new JButton(I18N.getGUIMessage("gui.bubble.process_unconnected_result_port.button.label")); - ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble.process_unconnected_result_port.button.tip")); - final BubbleDelegator bubbleDelegator = new BubbleDelegator(); + int enabledOperatorsSize = process.getRootOperator().getSubprocess(0).getEnabledOperators().size(); + Operator lastOperator = process.getRootOperator().getSubprocess(0).getEnabledOperators().get(enabledOperatorsSize - 1); + final JCheckBox dismissForeverCheckBox = new JCheckBox( I18N.getGUIMessage("gui.bubble.process_unconnected_result_port.button_dismiss.label")); dismissForeverCheckBox .setToolTipText(I18N.getGUIMessage("gui.bubble.process_unconnected_result_port.button_dismiss.tip")); + + JButton button = createFixOrAckButton("process_unconnected_result_port", Collections.singletonList( + new ConnectLastOperatorToOutputPortsQuickFix(lastOperator)), bubbleDelegator); + button.addActionListener(e -> { + + if (dismissForeverCheckBox.isSelected()) { + // store setting + ParameterService.setParameterValue(RapidMinerGUI.PROPERTY_SHOW_NO_RESULT_WARNING, + String.valueOf(Boolean.FALSE)); + ParameterService.saveParameters(); + } + }); + ResourceAction runAnywayAction = new ResourceAction("process_unconnected_result_port.button_run_anyway", "F11") { private static final long serialVersionUID = 1L; @@ -248,7 +241,7 @@ public void loggedActionPerformed(final ActionEvent e) { gbc.gridy += 1; gbc.gridwidth = 1; gbc.insets = new Insets(0, 0, 0, 10); - actionPanel.add(ackButton, gbc); + actionPanel.add(button, gbc); gbc.gridx += 1; actionPanel.add(runAnywayButton, gbc); @@ -256,26 +249,59 @@ public void loggedActionPerformed(final ActionEvent e) { final PortInfoBubble noResultConnectionBubble = builder.setHideOnConnection(true).setAlignment(AlignedSide.LEFT) .setStyle(BubbleStyle.WARNING).setHideOnProcessRun(false).setEnsureVisible(true).hideCloseButton() .setAdditionalComponents(new JComponent[] { actionPanel }).build(); - ackButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - noResultConnectionBubble.killBubble(true); - - if (dismissForeverCheckBox.isSelected()) { - // store setting - ParameterService.setParameterValue(RapidMinerGUI.PROPERTY_SHOW_NO_RESULT_WARNING, - String.valueOf(Boolean.FALSE)); - ParameterService.saveParameters(); - } - } - }); bubbleDelegator.setBubbleWindow(noResultConnectionBubble); noResultConnectionBubble.setVisible(true); return noResultConnectionBubble; } + /** + * Displays a warning bubble that alerts the user that a mandatory parameter of an operator + * needs a different value because the provided value does not work. The bubble is located at the operator and the + * process view will change to said operator. This is a bubble which occurs during the check for + * errors before process execution so it contains a link button to run the process anyway. + * + * @param op + * the operator for which to display the warning + * @param parameterKey + * the parameter key for which to display the warning + * @return the {@link OperatorInfoBubble} instance, never {@code null} + * @since 8.2 + */ + public static OperatorInfoBubble displayPrecheckBrokenMandatoryParameterWarning(final Operator op, final String parameterKey) { + return displayPrecheckBrokenMandatoryParameterWarning(op, parameterKey, true); + } + + /** + * DDisplays a warning bubble that alerts the user that a mandatory parameter of an operator + * needs a different value because the provided value does not work. The bubble is located at the operator and the + * process view will change to said operator. This is a bubble which occurs during the check for + * errors before process execution so it contains a link button to run the process anyway. + * + * @param op + * the operator for which to display the warning + * @param parameterKey + * the parameter key for which to display the warning + * @param showRunAnyway + * whether the run anyway button should be shown + * @return the {@link OperatorInfoBubble} instance, never {@code null} + * @since 8.2 + */ + public static OperatorInfoBubble displayPrecheckBrokenMandatoryParameterWarning(final Operator op, + final String parameterKey, boolean showRunAnyway) { + if (op == null) { + throw new IllegalArgumentException("op must not be null!"); + } + if (parameterKey == null) { + throw new IllegalArgumentException("parameterKey must not be null!"); + } + + // not the user entered name because that could be god knows how long + String opName = op.getOperatorDescription().getName(); + return displayPrecheckMissingOrBrokenParameterWarning(op, op.getParameterType(parameterKey), false, showRunAnyway, + "process_precheck_mandatory_parameter_broken", opName, parameterKey); + } + /** * Displays a warning bubble that alerts the user that a mandatory parameter of an operator * needs a value because it has no default value. The bubble is located at the operator and the @@ -318,7 +344,7 @@ public static OperatorInfoBubble displayPrecheckMissingMandatoryParameterWarning // not the user entered name because that could be god knows how long String opName = op.getOperatorDescription().getName(); - return displayPrecheckMissingParameterWarning(op, param, false, showRunAnyway, + return displayPrecheckMissingOrBrokenParameterWarning(op, param, false, showRunAnyway, "process_precheck_mandatory_parameter_unset", opName, param.getKey()); } @@ -328,12 +354,12 @@ public static OperatorInfoBubble displayPrecheckMissingMandatoryParameterWarning * change to said port. This is a bubble which occurs during the check for errors before process * execution so it contains a link button to run the process anyway. * - * @param port - * the port for which to display the error + * @param portAndError + * the port and the meta data error for which to display the error * @return the {@link PortInfoBubble} instance, never {@code null} */ - public static PortInfoBubble displayPrecheckInputPortDisconnectedWarning(final Port port) { - return displayPrecheckInputPortDisconnectedWarning(port, true); + public static PortInfoBubble displayPrecheckInputPortDisconnectedWarning(final Pair portAndError) { + return displayPrecheckInputPortDisconnectedWarning(portAndError, true); } /** @@ -343,27 +369,27 @@ public static PortInfoBubble displayPrecheckInputPortDisconnectedWarning(final P * execution so it contains a link button to run the process anyway if showRunAnyway is * {@code true}. * - * @param port - * the port for which to display the error + * @param portAndError + * the port and the meta data error for which to display the error * @param showRunAnyway * whether the run anyway button should be shown * @return the {@link PortInfoBubble} instance, never {@code null} */ - public static PortInfoBubble displayPrecheckInputPortDisconnectedWarning(final Port port, boolean showRunAnyway) { - if (port == null) { - throw new IllegalArgumentException("port must not be null!"); + public static PortInfoBubble displayPrecheckInputPortDisconnectedWarning(final Pair portAndError, boolean showRunAnyway) { + if (portAndError == null) { + throw new IllegalArgumentException("portAndError must not be null!"); } // PortOwner is an interface only implemented from anonymous inner classes // so check enclosing class and differentiate operator input ports and subprocess // (result) input ports String key; - if (ExecutionUnit.class.isAssignableFrom(port.getPorts().getOwner().getClass().getEnclosingClass())) { + if (ExecutionUnit.class.isAssignableFrom(portAndError.getFirst().getPorts().getOwner().getClass().getEnclosingClass())) { key = "process_precheck_mandatory_input_port_unconnected_inner"; } else { key = "process_precheck_mandatory_input_port_unconnected"; } - return displayPrecheckMissingInputPortWarning(port, true, false, showRunAnyway, key); + return displayPrecheckMissingInputPortWarning(portAndError, true, false, showRunAnyway, key); } /** @@ -588,49 +614,26 @@ public static OperatorInfoBubble displayGenericParameterError(final ParameterErr * optional i18n arguments * @return the {@link OperatorInfoBubble} instance, never {@code null} */ - private static OperatorInfoBubble displayPrecheckMissingParameterWarning(final Operator op, final ParameterType param, + private static OperatorInfoBubble displayPrecheckMissingOrBrokenParameterWarning(final Operator op, final ParameterType param, final boolean isError, final boolean showRunAnyway, final String i18nKey, final Object... arguments) { final BubbleDelegator bubbleDelegator = new BubbleDelegator(); - final JPanel linkPanel = new JPanel(); - if (showRunAnyway) { - ResourceAction runAnywayAction = new ResourceAction(i18nKey + ".button_run_anyway", "F11") { - - private static final long serialVersionUID = 1L; - - @Override - public void loggedActionPerformed(final ActionEvent e) { - BubbleWindow bubble = bubbleDelegator.getBubble(); - if (bubble != null) { - bubble.killBubble(true); - } - // run process without checking for problems - RapidMinerGUI.getMainFrame().runProcess(false); - } - }; - LinkLocalButton runAnywayButton = new LinkLocalButton(runAnywayAction); - runAnywayButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button_run_anyway.tip")); - runAnywayButton.registerKeyboardAction(runAnywayAction, KeyStroke.getKeyStroke(KeyEvent.VK_F11, 0), - JComponent.WHEN_IN_FOCUSED_WINDOW); - linkPanel.add(runAnywayButton); + List errorList = op.getErrorList(); + List quickFixes = null; + if (!errorList.isEmpty()) { + quickFixes = errorList.get(0).getQuickFixes(); } - - final JButton ackButton = new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label", arguments)); - ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip")); - + JComponent[] additionalComponents = new JComponent[2]; + additionalComponents[0] = createFixOrAckButton(i18nKey, showRunAnyway ? quickFixes : null, bubbleDelegator); + additionalComponents[1] = showRunAnyway ? createRunAnyWayButton(i18nKey, bubbleDelegator) : createQuickFixButton(quickFixes, bubbleDelegator); ParameterErrorBubbleBuilder builder = new ParameterErrorBubbleBuilder(RapidMinerGUI.getMainFrame(), op, param, "mandatory_parameter_decoration", i18nKey, arguments); final OperatorInfoBubble missingParameterBubble = builder.setHideOnDisable(true).setAlignment(AlignedSide.BOTTOM) .setStyle(isError ? BubbleStyle.ERROR : BubbleStyle.WARNING).setEnsureVisible(true).hideCloseButton() - .setHideOnProcessRun(true).setAdditionalComponents(new JComponent[] { ackButton, linkPanel }).build(); - ackButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - missingParameterBubble.killBubble(true); - } - }); + .setHideOnProcessRun(true) + .setAdditionalComponents(additionalComponents) + .build(); bubbleDelegator.setBubbleWindow(missingParameterBubble); missingParameterBubble.setVisible(true); @@ -642,8 +645,8 @@ public void actionPerformed(ActionEvent e) { * input but that there was a problem. The bubble is located at the port and the process view * will change to said port. execution. * - * @param port - * the port for which to display the warning + * @param portAndError + * the port and the meta data error for which to display the warning * @param i18nKey * the i18n key which defines the title, text and button label for the bubble. Format * is "gui.bubble.{i18nKey}.title", "gui.bubble.{i18nKey}.body" and @@ -659,47 +662,19 @@ public void actionPerformed(ActionEvent e) { * optional i18n arguments * @return the {@link PortInfoBubble} instance, never {@code null} */ - private static PortInfoBubble displayPrecheckMissingInputPortWarning(final Port port, final boolean hideOnConnection, + private static PortInfoBubble displayPrecheckMissingInputPortWarning(final Pair portAndError, final boolean hideOnConnection, final boolean isError, final boolean showRunAnyway, final String i18nKey, final Object... arguments) { final BubbleDelegator bubbleDelegator = new BubbleDelegator(); - final JPanel linkPanel = new JPanel(); - if (showRunAnyway) { - ResourceAction runAnywayAction = new ResourceAction(i18nKey + ".button_run_anyway", "F11") { - - private static final long serialVersionUID = 1L; - - @Override - public void loggedActionPerformed(final ActionEvent e) { - BubbleWindow bubble = bubbleDelegator.getBubble(); - if (bubble != null) { - bubble.killBubble(true); - } - - // run process without checking for problems - RapidMinerGUI.getMainFrame().runProcess(false); - } - }; - LinkLocalButton runAnywayButton = new LinkLocalButton(runAnywayAction); - runAnywayButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button_run_anyway.tip")); - runAnywayButton.registerKeyboardAction(runAnywayAction, KeyStroke.getKeyStroke(KeyEvent.VK_F11, 0), - JComponent.WHEN_IN_FOCUSED_WINDOW); - } - - final JButton ackButton = new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label", arguments)); - ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip")); - - PortBubbleBuilder builder = new PortBubbleBuilder(RapidMinerGUI.getMainFrame(), port, i18nKey, arguments); + JComponent[] additionalComponents = new JComponent[2]; + List quickFixes = portAndError.getSecond().getQuickFixes(); + additionalComponents[0] = createFixOrAckButton(i18nKey, showRunAnyway ? quickFixes : null, bubbleDelegator); + additionalComponents[1] = showRunAnyway ? createRunAnyWayButton(i18nKey, bubbleDelegator) : createQuickFixButton(quickFixes, bubbleDelegator); + PortBubbleBuilder builder = new PortBubbleBuilder(RapidMinerGUI.getMainFrame(), portAndError.getFirst(), i18nKey, arguments); final PortInfoBubble missingInputBubble = builder.setHideOnConnection(hideOnConnection).setHideOnDisable(true) .setAlignment(AlignedSide.LEFT).setStyle(isError ? BubbleStyle.ERROR : BubbleStyle.WARNING) .setEnsureVisible(true).hideCloseButton().setHideOnProcessRun(true) - .setAdditionalComponents(new JComponent[] { ackButton, linkPanel }).build(); - ackButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - missingInputBubble.killBubble(true); - } - }); + .setAdditionalComponents(additionalComponents) + .build(); bubbleDelegator.setBubbleWindow(missingInputBubble); missingInputBubble.setVisible(true); @@ -730,27 +705,17 @@ private static OperatorInfoBubble displayAttributeNotFoundParameterInformation(f final boolean isError, final String i18nKey, final Object... arguments) { final Operator op = error.getOperator(); final ParameterType param = op.getParameterType(error.getKey()); - final JButton ackButton = new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label", arguments)); - ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip")); + BubbleDelegator bubbleDelegator = new BubbleDelegator(); String decoratorKey = param instanceof CombinedParameterType || param instanceof ParameterTypeAttributes ? "attributes_not_found_decoration" : "attribute_not_found_decoration"; - ParameterErrorBubbleBuilder builder = new ParameterErrorBubbleBuilder(RapidMinerGUI.getMainFrame(), op, param, decoratorKey, i18nKey, arguments); final ParameterErrorInfoBubble attributeNotFoundParameterBubble = builder.setHideOnDisable(true) .setAlignment(AlignedSide.BOTTOM).setStyle(isError ? BubbleStyle.ERROR : BubbleStyle.WARNING) .setEnsureVisible(true).hideCloseButton().setHideOnProcessRun(true) - .setAdditionalComponents(new JComponent[] { ackButton }).build(); - - ackButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - attributeNotFoundParameterBubble.killBubble(true); - } - }); - + .setAdditionalComponents(new JComponent[] {createAckButton(i18nKey, bubbleDelegator, arguments)}).build(); + bubbleDelegator.setBubbleWindow(attributeNotFoundParameterBubble); attributeNotFoundParameterBubble.setVisible(true); return attributeNotFoundParameterBubble; } @@ -778,22 +743,18 @@ public void actionPerformed(ActionEvent e) { */ private static OperatorInfoBubble displayMissingMandatoryParameterInformation(final Operator op, final ParameterType param, final boolean isError, final String i18nKey, final Object... arguments) { - final JButton ackButton = new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label", arguments)); - ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip")); + + final BubbleDelegator bubbleDelegator = new BubbleDelegator(); ParameterErrorBubbleBuilder builder = new ParameterErrorBubbleBuilder(RapidMinerGUI.getMainFrame(), op, param, "mandatory_parameter_decoration", i18nKey, arguments); final OperatorInfoBubble missingParameterBubble = builder.setHideOnDisable(true).setAlignment(AlignedSide.BOTTOM) .setStyle(isError ? BubbleStyle.ERROR : BubbleStyle.WARNING).setEnsureVisible(true).hideCloseButton() - .setHideOnProcessRun(true).setAdditionalComponents(new JComponent[] { ackButton }).build(); - - ackButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - missingParameterBubble.killBubble(true); - } - }); + .setHideOnProcessRun(true).setAdditionalComponents(new JComponent[] { + createAckButton(i18nKey, bubbleDelegator), + createQuickFixButton(Collections.singletonList(new ParameterSettingQuickFix(op, param.getKey())), bubbleDelegator)}) + .build(); + bubbleDelegator.setBubbleWindow(missingParameterBubble); missingParameterBubble.setVisible(true); return missingParameterBubble; @@ -822,21 +783,14 @@ public void actionPerformed(ActionEvent e) { */ private static PortInfoBubble displayMissingInputPortInformation(final Port port, final boolean hideOnConnection, final boolean isError, final String i18nKey, final Object... arguments) { - final JButton ackButton = new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label", arguments)); - ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip")); - + BubbleDelegator bubbleDelegator = new BubbleDelegator(); PortBubbleBuilder builder = new PortBubbleBuilder(RapidMinerGUI.getMainFrame(), port, i18nKey, arguments); final PortInfoBubble missingInputBubble = builder.setHideOnConnection(hideOnConnection).setHideOnDisable(true) .setAlignment(AlignedSide.LEFT).setStyle(isError ? BubbleStyle.ERROR : BubbleStyle.WARNING) .setEnsureVisible(true).hideCloseButton().setHideOnProcessRun(true) - .setAdditionalComponents(new JComponent[] { ackButton }).build(); - ackButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - missingInputBubble.killBubble(true); - } - }); + .setAdditionalComponents(new JComponent[] {createAckButton(i18nKey, bubbleDelegator, arguments)}) + .build(); + bubbleDelegator.setBubbleWindow(missingInputBubble); missingInputBubble.setVisible(true); return missingInputBubble; } @@ -860,31 +814,8 @@ public void actionPerformed(ActionEvent e) { */ private static OperatorInfoBubble displayGenericParameterError(final ParameterError error, final Operator op, final ParameterType param, final String i18nKey) { - final JButton ackButton = new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label")); - ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip")); - final BubbleDelegator bubbleDelegator = new BubbleDelegator(); final String message = removeTerminationCharacters(error.getMessage()); - final JPanel linkPanel = new JPanel(); - LinkLocalButton showDetailsButton = new LinkLocalButton(new ResourceAction(i18nKey + ".button_show_details") { - - private static final long serialVersionUID = 1L; - - @Override - public void loggedActionPerformed(final ActionEvent e) { - BubbleWindow bubble = bubbleDelegator.getBubble(); - if (bubble != null) { - String text = I18N.getMessage(I18N.getGUIBundle(), "gui.bubble." + i18nKey + ".body", message, - error.getDetails()); - bubble.setMainText(text); - - linkPanel.removeAll(); - bubble.pack(); - } - } - }); - showDetailsButton.setToolTipText(I18N.getGUIMessage("gui.action." + i18nKey + ".button_show_details.tip")); - linkPanel.add(showDetailsButton); ParameterErrorBubbleBuilder builder = new ParameterErrorBubbleBuilder(RapidMinerGUI.getMainFrame(), error.getOperator(), param, "generic_parameter_decoration", i18nKey, message, ""); @@ -893,14 +824,10 @@ public void loggedActionPerformed(final ActionEvent e) { ? AlignedSide.MIDDLE : AlignedSide.BOTTOM; final OperatorInfoBubble userErrorBubble = builder.setHideOnDisable(true).setAlignment(prefSide) .setStyle(BubbleStyle.ERROR).setEnsureVisible(true).hideCloseButton().setHideOnProcessRun(true) - .setAdditionalComponents(new JComponent[] { ackButton, linkPanel }).build(); - ackButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - userErrorBubble.killBubble(true); - } - }); + .setAdditionalComponents(new JComponent[] { + createAckButton(i18nKey, bubbleDelegator), + createShowDetailsButton(error, i18nKey, bubbleDelegator, message)}) + .build(); bubbleDelegator.setBubbleWindow(userErrorBubble); if (error.getErrorName() != null && !error.getErrorName().trim().isEmpty()) { @@ -942,31 +869,8 @@ private static String removeTerminationCharacters(final String message) { * @return the {@link OperatorInfoBubble} instance, never {@code null} */ private static OperatorInfoBubble displayGenericUserError(final UserError error, final String i18nKey) { - final JButton ackButton = new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label")); - ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip")); - final BubbleDelegator bubbleDelegator = new BubbleDelegator(); final String message = removeTerminationCharacters(error.getMessage()); - final JPanel linkPanel = new JPanel(); - LinkLocalButton showDetailsButton = new LinkLocalButton(new ResourceAction(i18nKey + ".button_show_details") { - - private static final long serialVersionUID = 1L; - - @Override - public void loggedActionPerformed(final ActionEvent e) { - BubbleWindow bubble = bubbleDelegator.getBubble(); - if (bubble != null) { - String text = I18N.getMessage(I18N.getGUIBundle(), "gui.bubble." + i18nKey + ".body", message, - error.getDetails()); - bubble.setMainText(text); - - linkPanel.removeAll(); - bubble.pack(); - } - } - }); - showDetailsButton.setToolTipText(I18N.getGUIMessage("gui.action." + i18nKey + ".button_show_details.tip")); - linkPanel.add(showDetailsButton); OperatorBubbleBuilder builder = new OperatorBubbleBuilder(RapidMinerGUI.getMainFrame(), error.getOperator(), i18nKey, message, ""); @@ -976,14 +880,10 @@ public void loggedActionPerformed(final ActionEvent e) { : AlignedSide.BOTTOM; final OperatorInfoBubble userErrorBubble = builder.setHideOnDisable(true).setAlignment(prefSide) .setStyle(BubbleStyle.ERROR).setEnsureVisible(true).hideCloseButton().setHideOnProcessRun(true) - .setAdditionalComponents(new JComponent[] { ackButton, linkPanel }).build(); - ackButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - userErrorBubble.killBubble(true); - } - }); + .setAdditionalComponents(new JComponent[] { + createAckButton(i18nKey, bubbleDelegator), + createShowDetailsButton(error, i18nKey, bubbleDelegator, message)}) + .build(); bubbleDelegator.setBubbleWindow(userErrorBubble); if (error.getErrorName() != null && !error.getErrorName().trim().isEmpty()) { @@ -1006,46 +906,19 @@ public void actionPerformed(ActionEvent e) { * @return the {@link PortInfoBubble} instance, never {@code null} */ private static PortInfoBubble displayGenericPortError(final PortUserError error, final String i18nKey) { - final JButton ackButton = new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label")); - ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip")); - final BubbleDelegator bubbleDelegator = new BubbleDelegator(); final String message = removeTerminationCharacters(error.getMessage()); - final JPanel linkPanel = new JPanel(); - LinkLocalButton showDetailsButton = new LinkLocalButton(new ResourceAction(i18nKey + ".button_show_details") { - - private static final long serialVersionUID = 1L; - - @Override - public void loggedActionPerformed(final ActionEvent e) { - BubbleWindow bubble = bubbleDelegator.getBubble(); - if (bubble != null) { - String text = I18N.getMessage(I18N.getGUIBundle(), "gui.bubble." + i18nKey + ".body", message, - error.getDetails()); - bubble.setMainText(text); - - linkPanel.removeAll(); - bubble.pack(); - } - } - }); - showDetailsButton.setToolTipText(I18N.getGUIMessage("gui.action." + i18nKey + ".button_show_details.tip")); - linkPanel.add(showDetailsButton); // input ports (located left) show the "hook" of the bubble on the left and vice versa AlignedSide prefSide = error.getPort() instanceof InputPort ? AlignedSide.LEFT : AlignedSide.RIGHT; - PortBubbleBuilder builder = new PortBubbleBuilder(RapidMinerGUI.getMainFrame(), error.getPort(), i18nKey, message, - ""); + PortBubbleBuilder builder = new PortBubbleBuilder( + RapidMinerGUI.getMainFrame(), error.getPort(), i18nKey, message, ""); final PortInfoBubble portErrorBubble = builder.setHideOnDisable(true).setAlignment(prefSide) .setStyle(BubbleStyle.ERROR).setEnsureVisible(true).hideCloseButton().setHideOnProcessRun(true) - .setAdditionalComponents(new JComponent[] { ackButton, linkPanel }).build(); - ackButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - portErrorBubble.killBubble(true); - } - }); + .setAdditionalComponents(new JComponent[] { + createAckButton(i18nKey, bubbleDelegator), + createShowDetailsButton(error, i18nKey, bubbleDelegator, message)}) + .build(); bubbleDelegator.setBubbleWindow(portErrorBubble); if (error.getErrorName() != null && !error.getErrorName().trim().isEmpty()) { @@ -1071,9 +944,6 @@ public void actionPerformed(ActionEvent e) { */ private static OperatorInfoBubble displayUserErrorInExecutedProcess(final ProcessExecutionUserErrorError error, final String i18nKey, final Object... arguments) { - final JButton ackButton = new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label", arguments)); - ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip")); - final BubbleDelegator bubbleDelegator = new BubbleDelegator(); LinkLocalButton showDetailsButton = new LinkLocalButton(new ResourceAction(i18nKey + ".button_show_details") { @@ -1098,21 +968,14 @@ public void loggedActionPerformed(final ActionEvent e) { } } }); - showDetailsButton.setToolTipText(I18N.getGUIMessage("gui.action." + i18nKey + ".button_show_details.tip")); OperatorBubbleBuilder builder = new OperatorBubbleBuilder(RapidMinerGUI.getMainFrame(), error.getOperator(), i18nKey, arguments); final OperatorInfoBubble userErrorBubble = builder.setHideOnDisable(true).setHideOnProcessRun(true) .setAlignment(AlignedSide.BOTTOM).setStyle(BubbleStyle.ERROR).setEnsureVisible(true).hideCloseButton() - .setHideOnProcessRun(true).setAdditionalComponents(new JComponent[] { ackButton, showDetailsButton }) + .setHideOnProcessRun(true).setAdditionalComponents( + new JComponent[] {createAckButton(i18nKey, bubbleDelegator, arguments), showDetailsButton }) .build(); - ackButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - userErrorBubble.killBubble(true); - } - }); bubbleDelegator.setBubbleWindow(userErrorBubble); userErrorBubble.setVisible(true); @@ -1129,20 +992,10 @@ public void actionPerformed(ActionEvent e) { */ public static OperatorInfoBubble displayProcessSetupError(final ProcessSetupError processSetupError) { final String i18nKey = "process_setup_error"; - final JButton ackButton = new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label")); - ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip")); final BubbleDelegator bubbleDelegator = new BubbleDelegator(); final String message = removeTerminationCharacters(processSetupError.getMessage()); - final JPanel linkPanel = new JPanel(); - LinkLocalButton showQuickFixesButton = null; - if (!processSetupError.getQuickFixes().isEmpty()) { - showQuickFixesButton = new LinkLocalButton(new ResourceActionAdapter(i18nKey + ".button_show_quickfixes")); - showQuickFixesButton.setToolTipText(I18N.getGUIMessage("gui.action." + i18nKey + ".button_show_quickfixes.tip")); - linkPanel.add(showQuickFixesButton); - } - OperatorBubbleBuilder builder = new OperatorBubbleBuilder(RapidMinerGUI.getMainFrame(), processSetupError.getOwner().getOperator(), i18nKey, message, ""); @@ -1150,32 +1003,220 @@ public static OperatorInfoBubble displayProcessSetupError(final ProcessSetupErro : BubbleStyle.WARNING; final OperatorInfoBubble processSetupBubble = builder.setHideOnDisable(true).setAlignment(AlignedSide.BOTTOM) .setStyle(style).setEnsureVisible(true).hideCloseButton().setHideOnProcessRun(true) - .setAdditionalComponents(new JComponent[] { ackButton, linkPanel }).build(); + .setAdditionalComponents(new JComponent[]{ + createAckButton(i18nKey, bubbleDelegator), + createQuickFixButton(processSetupError.getQuickFixes(), bubbleDelegator)}) + .build(); - if (!processSetupError.getQuickFixes().isEmpty()) { - showQuickFixesButton.setAction(new ResourceAction(i18nKey + ".button_show_quickfixes") { + bubbleDelegator.setBubbleWindow(processSetupBubble); - private static final long serialVersionUID = 1L; + processSetupBubble.setVisible(true); + return processSetupBubble; + } - @Override - public void loggedActionPerformed(final ActionEvent e) { - // kill bubble when quick fix dialog is shown - processSetupBubble.killBubble(true); - new QuickFixDialog(processSetupError.getQuickFixes()).setVisible(true); - } - }); + /** + * Displays an information bubble pointing to an operator to indicate that the process needs to be saved for that operator settings. + * + * @param op + * the {@link Operator} that caused the error + * @param processSetupError + * the error that caused this bubble + * @return the {@link OperatorInfoBubble} instance, never {@code null} + * @since 8.2 + */ + public static OperatorInfoBubble displayPrecheckProcessNotSavedWarning(final Operator op, final ProcessSetupError processSetupError) { + final String i18nKey = "process_not_saved"; + + final BubbleDelegator bubbleDelegator = new BubbleDelegator(); + final String message = removeTerminationCharacters(processSetupError.getMessage()); + + List errorList = op.getErrorList(); + List quickFixes = processSetupError.getQuickFixes(); + // try to use quickfixes from given process setup error, otherwise use quickfixes from operator + if ((quickFixes == null || quickFixes.isEmpty()) && !errorList.isEmpty()) { + quickFixes = errorList.get(0).getQuickFixes(); + } + OperatorBubbleBuilder builder = new OperatorBubbleBuilder(RapidMinerGUI.getMainFrame(), + processSetupError.getOwner().getOperator(), i18nKey, message, ""); + + BubbleStyle style = processSetupError.getSeverity() == Severity.INFORMATION ? BubbleStyle.INFORMATION + : BubbleStyle.WARNING; + final OperatorInfoBubble processNotSavedBubble = builder.setHideOnDisable(true).setAlignment(AlignedSide.BOTTOM) + .setStyle(style).setEnsureVisible(true).hideCloseButton().setHideOnProcessRun(true) + .setAdditionalComponents(new JComponent[] { + createAckButton(i18nKey, bubbleDelegator), + createQuickFixButton(quickFixes, bubbleDelegator)}) + .build(); + + bubbleDelegator.setBubbleWindow(processNotSavedBubble); + + processNotSavedBubble.setVisible(true); + return processNotSavedBubble; + } + + /** + * Creates a {@link JButton} with the given i18n key and arguments to construct the label text and tool tip. + * Sets the action to kill the bubble provided by the {@link BubbleDelegator}. + * + * @param i18nKey + * the i18n key + * @param bubbleDelegator + * the bubble delegator + * @param arguments + * the i18n arguments + * @since 8.2 + */ + private static JButton createAckButton(String i18nKey, BubbleDelegator bubbleDelegator, Object... arguments) { + String completeKey = "gui.bubble." + i18nKey + ".button."; + final JButton ackButton = new JButton(I18N.getGUIMessage(completeKey + "label", arguments)); + ackButton.setToolTipText(I18N.getGUIMessage(completeKey + "tip")); + ackButton.addActionListener(e -> { + BubbleWindow bubble = bubbleDelegator.getBubble(); + if (bubble != null) { + bubble.killBubble(true); + } + }); + return ackButton; + } + + /** + * Creates either the "Got it" or "Fix now" button, depending on if the {@link QuickFix} list + * is empty (or {@code null}) or not. + * + * @param i18nKey + * the i18n key + * @param quickFixes + * the quickfixes for fixing the problem, can be empty or {@code null} + * @param bubbleDelegator + * the bubble delegator + * @return the button + * @since 8.2 + */ + private static JButton createFixOrAckButton(String i18nKey, List quickFixes, BubbleDelegator bubbleDelegator) { + if (quickFixes == null || quickFixes.isEmpty()) { + return createAckButton(i18nKey, bubbleDelegator); + } + String completeKey = "gui.bubble." + i18nKey + ".fix.button."; + final JButton fixButton = new JButton(I18N.getGUIMessage(completeKey + "label")); + fixButton.setToolTipText(I18N.getGUIMessage(completeKey + "tip")); + fixButton.addActionListener(e -> showOrApplyQuickFixes(bubbleDelegator, quickFixes)); + return fixButton; + } + + /** + * Creates a {@link LinkLocalButton} for the list of {@link QuickFix QuickFixes} or an empty {@link JLabel} if the given list is empty. + * + * @param quickFixes + * the list of quickfixes; can be {@code null} or empty + * @param bubbleDelegator + * the delegator to the {@link BubbleWindow} + * @return a button to show the quickfixes or an empty label + * @since 8.2 + */ + private static JComponent createQuickFixButton(List quickFixes, BubbleDelegator bubbleDelegator) { + if (quickFixes == null || quickFixes.isEmpty()) { + return new JLabel(); } - ackButton.addActionListener(new ActionListener() { + return new LinkLocalButton(new ResourceAction("process_setup_error.button_show_quickfixes") { + + private static final long serialVersionUID = 1L; @Override - public void actionPerformed(ActionEvent e) { - processSetupBubble.killBubble(true); + public void loggedActionPerformed(final ActionEvent e) { + // kill bubble when quick fix dialog is shown + showOrApplyQuickFixes(bubbleDelegator, quickFixes); } }); - bubbleDelegator.setBubbleWindow(processSetupBubble); + } - processSetupBubble.setVisible(true); - return processSetupBubble; + /** + * Shows a dialog with the list of {@link QuickFix QuickFixes} or just applies a single {@link QuickFix}. + * Will also kill the bubble provided by the {@link BubbleDelegator}. + * + * @param bubbleDelegator + * the bubble delegator + * @param quickFixes + * the list of quick fixes; must not be non-empty + * @since 8.2 + */ + private static void showOrApplyQuickFixes(BubbleDelegator bubbleDelegator, List quickFixes) { + BubbleWindow bubble = bubbleDelegator.getBubble(); + if (bubble != null) { + bubble.killBubble(true); + } + if (quickFixes.size() == 1) { + quickFixes.get(0).apply(); + return; + } + new QuickFixDialog(quickFixes).setVisible(true); + } + + /** + * Creates a {@link LinkLocalButton} for the "run anyway" action. + * + * @param i18nKey + * the i18n key + * @param bubbleDelegator + * the delegator to the {@link BubbleWindow} + * @return a button to show the "run anyway" action or an empty label + * @since 8.2 + */ + private static LinkLocalButton createRunAnyWayButton(String i18nKey, BubbleDelegator bubbleDelegator) { + ResourceAction runAnywayAction = new ResourceAction(i18nKey + ".button_run_anyway", "F11") { + + private static final long serialVersionUID = 1L; + + @Override + public void loggedActionPerformed(final ActionEvent e) { + BubbleWindow bubble = bubbleDelegator.getBubble(); + if (bubble != null) { + bubble.killBubble(true); + } + // run process without checking for problems + RapidMinerGUI.getMainFrame().runProcess(false); + } + }; + LinkLocalButton button = new LinkLocalButton(runAnywayAction); + button.registerKeyboardAction(runAnywayAction, KeyStroke.getKeyStroke(KeyEvent.VK_F11, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + return button; + } + + /** + * Creates the "show details" link button in a {@link JPanel} for more error message details. + * + * @param error + * the error + * @param i18nKey + * the i18n key + * @param bubbleDelegator + * the bubble for which the button is + * @param message + * the error message + * @return the link button panel + * @since 8.2 + */ + private static JPanel createShowDetailsButton(UserError error, String i18nKey, BubbleDelegator bubbleDelegator, String message) { + JPanel linkPanel = new JPanel(); + LinkLocalButton button = new LinkLocalButton(new ResourceAction(i18nKey + ".button_show_details") { + + private static final long serialVersionUID = 1L; + + @Override + public void loggedActionPerformed(final ActionEvent e) { + BubbleWindow bubble = bubbleDelegator.getBubble(); + if (bubble != null) { + String text = I18N.getMessage(I18N.getGUIBundle(), "gui.bubble." + i18nKey + ".body", message, + error.getDetails()); + bubble.setMainText(text); + + linkPanel.removeAll(); + bubble.pack(); + } + } + }); + linkPanel.add(button); + return linkPanel; } } diff --git a/src/main/java/com/rapidminer/gui/tools/ResourceLabel.java b/src/main/java/com/rapidminer/gui/tools/ResourceLabel.java index 2615c2ce7..15d1ce71c 100644 --- a/src/main/java/com/rapidminer/gui/tools/ResourceLabel.java +++ b/src/main/java/com/rapidminer/gui/tools/ResourceLabel.java @@ -55,7 +55,7 @@ public ResourceLabel(String i18nKey, Object... i18nArgs) { ImageIcon iicon = SwingTools.createIcon(icon); setIcon(iicon); } - if (mne != null) { + if (mne != null && !mne.isEmpty()) { setDisplayedMnemonic(mne.charAt(0)); } } diff --git a/src/main/java/com/rapidminer/gui/tools/ResourceMenu.java b/src/main/java/com/rapidminer/gui/tools/ResourceMenu.java index 8441ad294..7ef29c02f 100644 --- a/src/main/java/com/rapidminer/gui/tools/ResourceMenu.java +++ b/src/main/java/com/rapidminer/gui/tools/ResourceMenu.java @@ -34,7 +34,7 @@ * * @author Simon Fischer, Sebastian Land */ -public class ResourceMenu extends JMenu { +public class ResourceMenu extends JMenu { private static final long serialVersionUID = -7711922457461154801L; diff --git a/src/main/java/com/rapidminer/gui/tools/ScrollableJPopupMenu.java b/src/main/java/com/rapidminer/gui/tools/ScrollableJPopupMenu.java index abc3d7c0a..0e5a95da3 100644 --- a/src/main/java/com/rapidminer/gui/tools/ScrollableJPopupMenu.java +++ b/src/main/java/com/rapidminer/gui/tools/ScrollableJPopupMenu.java @@ -1,30 +1,31 @@ /** * Copyright (C) 2001-2018 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero 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 * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.gui.tools; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.FocusTraversalPolicy; +import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; - +import java.util.Arrays; import javax.swing.AbstractAction; import javax.swing.BoxLayout; import javax.swing.JComponent; @@ -34,6 +35,8 @@ import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.KeyStroke; +import javax.swing.MenuElement; +import javax.swing.plaf.basic.BasicMenuItemUI; import com.rapidminer.gui.look.Colors; @@ -43,49 +46,58 @@ * be set as well as a custom width of this popup menu. Furthermore, focus traversal via TAB works * for any {@link Component} added to this popupmenu, not only for {@link JMenuItem}s. * - * @author Marco Boeck - * + * @author Marco Boeck, Jan Czogalla */ public class ScrollableJPopupMenu extends JPopupMenu { private static final long serialVersionUID = 7440641917394853639L; + /** + * Empty space to have enough width to show the included components properly + */ private static final int INSETS = 5; + /** + * Additional height to suppress undesired scrolling + */ + private static final int SCROLLING_MARGIN = 2; + + /** + * Optional heights for this {@link ScrollableJPopupMenu} + */ public static final int SIZE_TINY = 100; public static final int SIZE_SMALL = 200; public static final int SIZE_NORMAL = 400; public static final int SIZE_LARGE = 600; public static final int SIZE_HUGE = 800; - /** the scrollpane which allows scrolling */ + /** The {@link JScrollPane} which allows scrolling */ private JScrollPane scrollPane; - /** the inner panel which houses all components */ - private JPanel innerPanel; + /** The inner {@link JPanel} which houses all components */ + protected JPanel innerPanel; - /** the title text for the menu, displayed above scrollpane */ + /** The title text for the menu, displayed above scrollpane */ private String title; - /** the max height in pixel for the scrollpane */ + /** The maximum height in pixel for the scrollpane */ private int maxHeight; - /** the max width in pixel for the scrollpane */ + /** The maximum width in pixel for the scrollpane */ private int maxWidth; - /** if not null, will be used to determine the width of the scrollpane */ + /** If not null, will be used to determine the width of the scrollpane. See {@link ScrollableJPopupMenu#setCustomWidth(Integer)} } */ private Integer customWidth; /** - * Creates a new {@link ScrollJPopupMenu} instance with the default max height. - * + * Creates a new {@link ScrollableJPopupMenu} instance with the default max height. */ public ScrollableJPopupMenu() { this(null, SIZE_NORMAL); } /** - * Creates a new {@link ScrollJPopupMenu} instance with the specified max height. + * Creates a new {@link ScrollableJPopupMenu} instance with the specified max height. * * @param maxHeight */ @@ -94,7 +106,7 @@ public ScrollableJPopupMenu(int maxHeight) { } /** - * Creates a new {@link ScrollJPopupMenu} instance with the specified title. + * Creates a new {@link ScrollableJPopupMenu} instance with the specified title. * * @param title */ @@ -103,19 +115,16 @@ public ScrollableJPopupMenu(String title) { } /** - * Creates a new {@link ScrollJPopupMenu} instance with the specified max height and title. + * Creates a new {@link ScrollableJPopupMenu} instance with the specified max height and title. * * @param title * @param maxHeight */ public ScrollableJPopupMenu(String title, int maxHeight) { super(); - if (maxHeight < SIZE_TINY) { - throw new IllegalArgumentException("size must not be smaller than " + SIZE_TINY); - } + setMaxHeight(maxHeight); this.title = title; - this.maxHeight = maxHeight; - this.maxWidth = 800; + this.maxWidth = SIZE_HUGE; initGUI(); } @@ -223,6 +232,32 @@ public Component getComponentAfter(Container aContainer, Component aComponent) { }); } + /** + * Updates the preferred size of the scrollpane depending on the components of this popup menu. + */ + protected void resizeScrollPane() { + setPreferredSize(null); + Dimension preferredSize = getPreferredSize(); + int height = preferredSize.height; + final int heightWithMargin = maxHeight + SCROLLING_MARGIN; + boolean needsScrolling = height > heightWithMargin; + if (needsScrolling) { + height = heightWithMargin; + } + int width; + if (customWidth != null) { + width = customWidth; + } else { + width = preferredSize.width; + if (needsScrolling) { + width += scrollPane.getVerticalScrollBar().getPreferredSize().width + INSETS; + } + } + setPopupSize(width, height); + revalidate(); + repaint(); + } + @Override public Component add(final Component comp) { innerPanel.add(comp); @@ -238,13 +273,18 @@ public void remove(Component comp) { } /** - * Updates the preferred size of the scrollpane depending on the components of this popup menu. + * Sets the maximum height for this popup. + * + * @since 8.2 */ - private void resizeScrollPane() { - int width = customWidth == null ? innerPanel.getPreferredSize().width - + scrollPane.getVerticalScrollBar().getPreferredSize().width + INSETS : customWidth - INSETS; - scrollPane - .setPreferredSize(new Dimension(width, Math.min(maxHeight, innerPanel.getPreferredSize().height + INSETS))); + public void setMaxHeight(int maxHeight) { + if (maxHeight < SIZE_TINY) { + throw new IllegalArgumentException("size must not be smaller than " + SIZE_TINY); + } + this.maxHeight = maxHeight; + if (innerPanel != null) { + resizeScrollPane(); + } } /** @@ -258,16 +298,24 @@ public void setCustomWidth(Integer customWidth) { } /** - * Returns all {@link Component}s inside the scrollpane. + * @return the {@link JScrollPane} used inside + * @since 8.2 + */ + public JScrollPane getScrollPane() { + return scrollPane; + } + + /** + * Returns all {@link Component Components} inside the scrollpane. * - * @return + * @return Array of all included {@link Component Components} */ public Component[] getComponentsInsideScrollpane() { return innerPanel.getComponents(); } /** - * Requets the focus on the first component inside the scrollpane. Does nothing if no components + * Requests the focus on the first component inside the scrollpane. Does nothing if no components * exist. * * @return {@link Component#requestFocusInWindow()} @@ -283,4 +331,66 @@ private boolean requestFocusForFirstComponent() { public boolean requestFocusInWindow() { return requestFocusForFirstComponent(); } + + /** + * Adds the given {@link JMenuItem} to the inner {@link JPanel}. Makes sure that it is in the visible area when selected, + * especially for keyboard navigation. + * + * @since 8.2 + */ + @Override + public JMenuItem add(JMenuItem menuItem) { + innerPanel.add(menuItem); + menuItem.addChangeListener(e -> ((JComponent) menuItem.getComponent()).scrollRectToVisible(new Rectangle(0, 0, menuItem.getWidth(), menuItem.getHeight()))); + resizeScrollPane(); + + return menuItem; + } + + /** + * {@inheritDoc} + * + * @return the {@link MenuElement} items inside the inner {@link JPanel}. + * @since 8.2 + */ + @Override + public MenuElement[] getSubElements() { + return Arrays.stream(innerPanel.getComponents()). + filter(c -> c instanceof MenuElement).toArray(MenuElement[]::new); + } + + /** + * Removes the component specified by the index from the inner {@link JPanel}. + * + * @since 8.2 + */ + @Override + public void remove(int index) { + innerPanel.remove(index); + resizeScrollPane(); + } + + /** + * Clears the inner {@link JPanel}. + * + * @since 8.2 + */ + @Override + public void removeAll() { + innerPanel.removeAll(); + resizeScrollPane(); + innerPanel.scrollRectToVisible(new Rectangle(0, 0, 0, 0)); + } + + /** + * Returns the inner {@link JPanel} that holds the actual components. + * This is used in {@link BasicMenuItemUI#getPath()} for example to determine menu hierarchy on mouse events. + * + * @return the inner panel + * @since 8.2 + */ + @Override + public Component getComponent() { + return innerPanel; + } } diff --git a/src/main/java/com/rapidminer/gui/tools/autocomplete/AutoCompleteComboBoxAddition.java b/src/main/java/com/rapidminer/gui/tools/autocomplete/AutoCompleteComboBoxAddition.java index b9b7b0d28..42b1520ff 100644 --- a/src/main/java/com/rapidminer/gui/tools/autocomplete/AutoCompleteComboBoxAddition.java +++ b/src/main/java/com/rapidminer/gui/tools/autocomplete/AutoCompleteComboBoxAddition.java @@ -18,23 +18,8 @@ */ package com.rapidminer.gui.tools.autocomplete; -import java.awt.event.FocusAdapter; -import java.awt.event.FocusEvent; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Locale; -import java.util.Vector; - import javax.swing.ComboBoxModel; import javax.swing.JComboBox; -import javax.swing.JTextField; -import javax.swing.SwingUtilities; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.plaf.basic.BasicComboBoxEditor; -import javax.swing.text.BadLocationException; -import javax.swing.text.Document; /** @@ -46,90 +31,8 @@ */ public class AutoCompleteComboBoxAddition { - private final class AutoCompletionDocumentListener implements DocumentListener { - - @Override - public void insertUpdate(DocumentEvent e) { - try { - if (!allowAutoFill) { - // see #focusGained() down below - return; - } - - Vector vectorOfStrings = new Vector(); - for (int i = 0; i < comboBox.getModel().getSize(); i++) { - vectorOfStrings.add(String.valueOf(comboBox.getModel().getElementAt(i))); - } - Document document = e.getDocument(); - final String documentText = document.getText(0, document.getLength()); - final String result = checkForMatch(documentText, vectorOfStrings, caseSensitive); - final JTextField editorComponent = (JTextField) comboBox.getEditor().getEditorComponent(); - // reset the comboBox selection if there is no match - if (result == null) { - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - comboBox.getModel().setSelectedItem(documentText); - } - }); - return; - } - final int startSelect = document.getLength(); - final int endSelect = result.length(); - - if (startSelect == e.getOffset() + e.getLength()) { - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - comboBox.getModel().setSelectedItem(result); - editorComponent.getDocument().removeDocumentListener(docListener); - editorComponent.setText(result); - editorComponent.getDocument().addDocumentListener(docListener); - editorComponent.setCaretPosition(startSelect); - editorComponent.setSelectionStart(startSelect); - editorComponent.setSelectionEnd(endSelect); - } - }); - } - } catch (BadLocationException e1) { - } - } - - @Override - public void removeUpdate(DocumentEvent e) { - // not needed, only match on new input - } - - @Override - public void changedUpdate(DocumentEvent e) { - // never fired for Document - } - } - - private final class AutoCompletionComboBoxEditor extends BasicComboBoxEditor { - - @Override - public void setItem(Object anObject) { - ((JTextField) getEditorComponent()).getDocument().removeDocumentListener(docListener); - super.setItem(anObject); - ((JTextField) getEditorComponent()).getDocument().addDocumentListener(docListener); - } - } - - /** only set this to true after the first time we gained the focus */ - private boolean allowAutoFill; - - /** if set to true, matching will be case sensitive; false otherwise */ - private boolean caseSensitive; - /** the document listener for the combo box editor */ - private final DocumentListener docListener; - - /** the JComboBox to which it is attached */ - private final JComboBox comboBox; - private final BasicComboBoxEditor comboBoxEditor; + private final AutoCompletionDocumentListener docListener; /** * Adds an auto completion feature to the given JComboBox. Will set @@ -141,43 +44,7 @@ public void setItem(Object anObject) { * the JComboBox which should get the auto completion feature */ public AutoCompleteComboBoxAddition(JComboBox box) { - comboBox = box; - - caseSensitive = false; - allowAutoFill = false; - comboBox.setEditable(true); - comboBoxEditor = new AutoCompletionComboBoxEditor(); - comboBox.setEditor(comboBoxEditor); - docListener = new AutoCompletionDocumentListener(); - ((JTextField) comboBox.getEditor().getEditorComponent()).getDocument().addDocumentListener(docListener); - - // workaround for java bug #6433257 - comboBoxEditor.getEditorComponent().addFocusListener(new FocusAdapter() { - - @Override - public void focusGained(FocusEvent e) { - // if we did not prevent this, it would auto fill each time the combo box is - // re-created after a - // custom value has been entered, - // therefore overwriting any value which happens to be a prefix of something with - // the first match. - // this happens due to some RapidLookComboBoxEditor mechanics. - allowAutoFill = true; - - // needed because otherwise the border will not update itself - comboBox.repaint(); - } - - @Override - public void focusLost(FocusEvent e) { - allowAutoFill = false; - if (!e.isTemporary()) { - final JTextField editorComponent = (JTextField) comboBox.getEditor().getEditorComponent(); - editorComponent.setCaretPosition(editorComponent.getCaretPosition()); - } - } - - }); + docListener = new AutoCompletionDocumentListener<>(box); } /** @@ -187,43 +54,7 @@ public void focusLost(FocusEvent e) { * If set to true, matching is case sensitive; false otherwise */ public void setCaseSensitive(boolean caseSensitive) { - this.caseSensitive = caseSensitive; - } - - /** - * Returns the first string which starts with the given String. - * - * @param givenString - * the result must start with this string - * @param collectionOfStrings - * the collection of strings to match against the given string - * @param caseSensitive - * @see {@link #setCaseSensitive(boolean)} - * @return the first match or {@code null} if no match is found - */ - private String checkForMatch(String givenString, Collection collectionOfStrings, boolean caseSensitive) { - if (givenString == null || collectionOfStrings == null) { - return null; - } - String returnString = null; - Collections.sort(new ArrayList(collectionOfStrings)); - for (String vectorString : collectionOfStrings) { - if (vectorString == null) { - continue; - } - if (caseSensitive) { - if (vectorString.startsWith(givenString)) { - returnString = vectorString; - break; - } - } else { - if (vectorString.toLowerCase(Locale.ENGLISH).startsWith(givenString.toLowerCase(Locale.ENGLISH))) { - returnString = vectorString; - break; - } - } - } - return returnString; + this.docListener.setCaseSensitive(caseSensitive); } } diff --git a/src/main/java/com/rapidminer/gui/tools/autocomplete/AutoCompletionDocumentListener.java b/src/main/java/com/rapidminer/gui/tools/autocomplete/AutoCompletionDocumentListener.java new file mode 100644 index 000000000..541227e53 --- /dev/null +++ b/src/main/java/com/rapidminer/gui/tools/autocomplete/AutoCompletionDocumentListener.java @@ -0,0 +1,321 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.gui.tools.autocomplete; + +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; +import javax.swing.ComboBoxEditor; +import javax.swing.ComboBoxModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.plaf.basic.BasicComboBoxEditor; +import javax.swing.text.JTextComponent; + +import com.rapidminer.gui.tools.SwingTools; + + +/** + * Listener which allows a JComboBox to auto fills itself if the entered string is prefix of any + * combo box item. + * + * @author Marco Boeck, Sebastian Land, Jonas Wilms-Pfau + * @since 8.1.2 + */ +class AutoCompletionDocumentListener implements DocumentListener { + + /** Delay in milliseconds for the autocomplete to begin */ + private static final int AUTOCOMPLETE_DELAY_MS = 250; + + /** Delay in milliseconds for the autosave to begin */ + private static final int AUTOSAVE_DELAY_MS = 1100; + + /** Property names of JComboBox components */ + private static final String MODEL = "model"; + private static final String EDITOR = "editor"; + + /** The comboBox to autocomplete */ + private final JComboBox comboBox; + + /** The input field of the comboBox editor */ + private volatile JTextComponent comboBoxInput; + + /** The current underlying comboBoxModel */ + private volatile ComboBoxModel comboBoxModel; + + /** If set to true, matching will be case sensitive; false otherwise */ + private volatile boolean caseSensitive; + + /** If this is true the autocomplete updated the value itself */ + private volatile boolean selfUpdate; + + /** Only set this to true after the first time we gained the focus */ + private volatile boolean hasFocus; + + /** Indicate if events should be ignored since it's loading in the moment */ + private volatile boolean isLoading; + + /** Triggers autosave after a specific time */ + private final SuccessiveExecutionTimer autoSave = new SuccessiveExecutionTimer(AUTOSAVE_DELAY_MS, new SaveRunnable()); + + /** Triggers autocomplete after a specific time */ + private final SuccessiveExecutionTimer autoComplete = new SuccessiveExecutionTimer(AUTOCOMPLETE_DELAY_MS, new MakeSuggestionRunnable()); + + /** Updates the hasFocus property and fires the ActionEvents on focus lost */ + private final FocusListener focusChangeListener = new FocusAdapter() { + + @Override + public void focusGained(FocusEvent e) { + /* if we did not prevent this, it would auto fill each time the combo box is + * re-created after a custom value has been entered, therefore overwriting any value + * which happens to be a prefix of something with the first match. + * This happens due to some RapidLookComboBoxEditor mechanics. */ + hasFocus = true; + + // needed because otherwise the border will not update itself + comboBox.repaint(); + } + + @Override + public void focusLost(FocusEvent e) { + hasFocus = false; + autoSave.runNow(); + if (e.isTemporary()) { + comboBoxInput.setCaretPosition(comboBoxInput.getCaretPosition()); + } + } + }; + + /** Allows the use of the enter key to save the current comboBox value */ + private final KeyListener updateOnEnterKey = new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (!comboBox.isPopupVisible() && e.getKeyCode() == KeyEvent.VK_ENTER) { + comboBox.setSelectedItem(comboBoxInput.getText()); + comboBoxInput.setCaretPosition(comboBoxInput.getText().length()); + } + } + }; + + AutoCompletionDocumentListener(JComboBox box) { + if (box == null) { + throw new IllegalArgumentException("comboBox must not be null"); + } + comboBox = box; + comboBox.setEditable(true); + if (comboBox.getEditor() == null) { + comboBox.setEditor(new BasicComboBoxEditor()); + } + //Only do this once + comboBox.setEditor(new ComboBoxEditorWrapper(comboBox.getEditor()) { + @Override + public void setItem(Object anObject) { + isLoading = true; + super.setItem(anObject); + isLoading = false; + } + }); + if (comboBox.getModel() == null) { + comboBox.setModel(new DefaultComboBoxModel<>()); + } + comboBox.addPropertyChangeListener(MODEL, this::initComboBoxModel); + comboBox.addPropertyChangeListener(EDITOR, this::initComboBoxEditor); + initComboBoxModel(); + initComboBoxEditor(); + } + + /** + * Sets the auto-fill feature to case sensitive. + * + * @param caseSensitive + * If set to true, matching is case sensitive; false otherwise + */ + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + @Override + public void insertUpdate(DocumentEvent e) { + if (!hasFocus || selfUpdate || isLoading) { + return; + } + autoSave.restart(); + autoComplete.restart(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + autoSave.restart(); + autoComplete.stop(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + // never fired for Document + } + + /** + * Initializes the ComboBoxInput variable + * + * @param ignored not used + */ + private void initComboBoxEditor(Object... ignored) { + ComboBoxEditor editor = comboBox.getEditor(); + if (editor == null) { + return; + } + if (editor.getEditorComponent() instanceof JTextComponent) { + if (comboBoxInput != null) { + //Clear old listener + comboBoxInput.getDocument().removeDocumentListener(this); + comboBoxInput.removeFocusListener(focusChangeListener); + comboBoxInput.removeKeyListener(updateOnEnterKey); + } + comboBoxInput = ((JTextComponent) editor.getEditorComponent()); + comboBoxInput.getDocument().addDocumentListener(this); + // workaround for java bug #6433257 + comboBoxInput.addFocusListener(focusChangeListener); + // allow enter to use current value + comboBoxInput.addKeyListener(updateOnEnterKey); + } + } + + /** + * Initializes the comboBoxModel and comboBoxModelIterable variable + * + * @param ignored not used + */ + private void initComboBoxModel(Object... ignored) { + if (comboBox.getModel() != null) { + comboBoxModel = comboBox.getModel(); + } + } + + /** + * Saves the current value + * + * @author Jonas Wilms-Pfau + * @since 8.1.2 + */ + private class SaveRunnable implements Runnable { + + /** + * Reference to the last saved value + */ + private final AtomicReference savedValue = new AtomicReference<>(); + + @Override + public void run() { + String newValue = comboBoxInput.getText(); + BooleanSupplier sameValue = () -> String.valueOf(comboBoxModel.getSelectedItem()).equals(newValue); + BooleanSupplier alreadyStored = () -> Objects.equals(newValue, savedValue.getAndSet(newValue)); + if (comboBoxInput.hasFocus() && !sameValue.getAsBoolean() && !alreadyStored.getAsBoolean()) { + final int start = comboBoxInput.getSelectionStart(); + final int end = comboBoxInput.getSelectionEnd(); + SwingTools.invokeAndWait(() -> { + comboBox.setSelectedItem(comboBoxInput.getText()); + comboBoxInput.setCaretPosition(end); + comboBoxInput.moveCaretPosition(start); + }); + } + } + } + + /** + * Tries to autocomplete the currently entered letters + * + * @author Jonas Wilms-Pfau + * @since 8.1.2 + */ + private class MakeSuggestionRunnable implements Runnable { + + @Override + public void run() { + //Should be thread-safe thanks to {@link AbstractDocument} + final E result = checkForMatch(comboBoxInput.getText()); + + // abort if there is no match + if (result == null || result.equals(comboBoxInput.getText())) { + return; + } + + SwingTools.invokeAndWait(() -> { + final String userInput = comboBoxInput.getText(); + final String resultString = result.toString(); + //last check if user typed in the meantime + if (!result.equals(userInput) && startsWith(resultString, userInput)) { + selfUpdate = true; + comboBoxInput.setText(resultString); + selfUpdate = false; + comboBoxInput.setCaretPosition(resultString.length()); + comboBoxInput.moveCaretPosition(userInput.length()); + } + }); + } + + /** + * Returns the first string which starts with the given String. + * + * @param givenString + * the result must start with this string + * @return the first match or {@code null} if no match is found + */ + private E checkForMatch(String givenString) { + if (givenString == null || givenString.isEmpty()) { + return null; + } + for (int i = 0; i < comboBoxModel.getSize(); i++) { + E currentValue = comboBoxModel.getElementAt(i); + if (currentValue != null) { + String currentString = String.valueOf(currentValue); + if (startsWith(currentString, givenString)) { + return currentValue; + } + } + } + return null; + } + + /** + * Check if fullString starts with prefix + * + * @param fullString + * the full string + * @param prefix + * the prefix that the fullString should start with + * @return true if fullString startsWith prefix + */ + private boolean startsWith(String fullString, String prefix) { + if (fullString != null && prefix != null && prefix.length() <= fullString.length()) { + return fullString.regionMatches(!caseSensitive, 0, prefix, 0, prefix.length()); + } else { + return false; + } + } + } + +} diff --git a/src/main/java/com/rapidminer/gui/tools/autocomplete/ComboBoxEditorWrapper.java b/src/main/java/com/rapidminer/gui/tools/autocomplete/ComboBoxEditorWrapper.java new file mode 100644 index 000000000..fd9768fd6 --- /dev/null +++ b/src/main/java/com/rapidminer/gui/tools/autocomplete/ComboBoxEditorWrapper.java @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.gui.tools.autocomplete; + +import java.awt.Component; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import javax.swing.ComboBoxEditor; + + +/** + * Wrapper class for ComboBoxEditor + *

+ * Allows to use a custom styled ComboBoxEditor together with AutoComplete + * + * @since 8.1.2 + * @author Jonas Wilms-Pfau + */ +class ComboBoxEditorWrapper implements ComboBoxEditor, FocusListener { + + /** The wrapped ComboBoxEditor */ + private final ComboBoxEditor wrapped; + + ComboBoxEditorWrapper(ComboBoxEditor wrapped) { + if (wrapped == null) { + throw new IllegalArgumentException("wrapped ComboBoxEditor must not be null"); + } + this.wrapped = wrapped; + } + + @Override + public Component getEditorComponent() { + return wrapped.getEditorComponent(); + } + + @Override + public void setItem(Object anObject) { + wrapped.setItem(anObject); + } + + @Override + public Object getItem() { + return wrapped.getItem(); + } + + @Override + public void selectAll() { + wrapped.selectAll(); + } + + @Override + public void addActionListener(ActionListener l) { + wrapped.addActionListener(l); + } + + @Override + public void removeActionListener(ActionListener l) { + wrapped.removeActionListener(l); + } + + @Override + public void focusGained(FocusEvent e) { + if (wrapped instanceof FocusListener) { + ((FocusListener) wrapped).focusGained(e); + } + } + + @Override + public void focusLost(FocusEvent e) { + if (wrapped instanceof FocusListener) { + ((FocusListener) wrapped).focusLost(e); + } + } +} diff --git a/src/main/java/com/rapidminer/gui/tools/autocomplete/SuccessiveExecutionTimer.java b/src/main/java/com/rapidminer/gui/tools/autocomplete/SuccessiveExecutionTimer.java new file mode 100644 index 000000000..c5d6b09bf --- /dev/null +++ b/src/main/java/com/rapidminer/gui/tools/autocomplete/SuccessiveExecutionTimer.java @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.gui.tools.autocomplete; + +import java.awt.event.ActionEvent; +import java.lang.ref.WeakReference; +import java.util.concurrent.Future; +import javax.swing.SwingWorker; +import javax.swing.Timer; + + +/** + * Wrapper class for {@link Timer}, that ensures successive execution of given Runnable on the SwingWorker ThreadPool. + * + * @since 8.1.2 + * @author Jonas Wilms-Pfau + */ +class SuccessiveExecutionTimer { + + /** Reference to the last execution */ + private volatile WeakReference> lastExecutionReference = new WeakReference<>(null); + + /** Synchronization lock for the execution */ + private final Object executionLock = new Object(); + + /** The runnable to execute */ + private final Runnable runnable; + + /** Timer used to trigger the execution */ + private final Timer timer; + + /** + * Creates a new SuccessiveExecutionTimer + * + * @param delayMs + * milliseconds for the initial and between-event delay + * @param runnable + * the runnable to run + */ + SuccessiveExecutionTimer(int delayMs, Runnable runnable) { + if (runnable == null) { + throw new IllegalArgumentException("runnable must not be null"); + } + this.runnable = runnable; + timer = new Timer(delayMs, this::execute); + timer.setRepeats(false); + } + + /** + * Restarts the timer + * + * @see Timer#restart + */ + void restart() { + timer.restart(); + } + + /** + * Stops already scheduled, but not started executions + * + * @see Timer#stop + */ + void stop() { + timer.stop(); + } + + /** + * Executes the given Runnable immediately on the callers thread. + * + * @see Runnable#run + */ + void run() { + runnable.run(); + } + + /** + * Stops the Timer and runs the Runnable immediately on the callers thread. + * + * @see #stop + * @see #run + */ + void runNow(){ + stop(); + run(); + } + + /** + * Executes the runnable on the SwingWorker thread pool. + *

+ * If the runnable is already executing at the moment it will restart the timer. + *

+ * + * @see java.awt.event.ActionListener#actionPerformed(ActionEvent) + */ + private void execute(ActionEvent ae) { + //Do not stack endless waiting threads + Future lastExecution = lastExecutionReference.get(); + if (lastExecution != null && !lastExecution.isDone()) { + restart(); + } else { + SwingWorker newExecution = new SwingWorker() { + @Override + protected Void doInBackground() { + synchronized (executionLock) { + runnable.run(); + } + return null; + } + }; + lastExecutionReference = new WeakReference<>(newExecution); + newExecution.execute(); + } + } +} + diff --git a/src/main/java/com/rapidminer/gui/tools/components/ButtonBarCardPanel.java b/src/main/java/com/rapidminer/gui/tools/components/ButtonBarCardPanel.java index b370ce789..2a7167a31 100644 --- a/src/main/java/com/rapidminer/gui/tools/components/ButtonBarCardPanel.java +++ b/src/main/java/com/rapidminer/gui/tools/components/ButtonBarCardPanel.java @@ -46,6 +46,7 @@ import com.rapidminer.gui.look.Colors; import com.rapidminer.gui.tools.ExtendedJScrollPane; import com.rapidminer.gui.tools.ListHoverHelper; +import com.rapidminer.gui.tools.MenuShortcutJList; /** @@ -98,7 +99,7 @@ public ButtonBarCardPanel(Set noCardKeys, boolean showCards) { this.noCardKeys = noCardKeys; this.showCards = showCards; - navigation = new JList(cardListModel) { + navigation = new MenuShortcutJList(cardListModel, false) { private static final long serialVersionUID = -5414386397971825656L; diff --git a/src/main/java/com/rapidminer/gui/tools/components/FeedbackForm.java b/src/main/java/com/rapidminer/gui/tools/components/FeedbackForm.java new file mode 100644 index 000000000..e11e311dd --- /dev/null +++ b/src/main/java/com/rapidminer/gui/tools/components/FeedbackForm.java @@ -0,0 +1,281 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.gui.tools.components; + +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JToggleButton; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import org.jdesktop.swingx.prompt.PromptSupport; + +import com.rapidminer.gui.MainFrame; +import com.rapidminer.gui.look.Colors; +import com.rapidminer.gui.look.RapidLookTools; +import com.rapidminer.gui.tools.ExtendedJScrollPane; +import com.rapidminer.gui.tools.NotificationPopup; +import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.tools.I18N; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; + + +/** + * Displays a "was this helpful?" feedback form with thumbs-up/down buttons and a free text field. + *

+ * Ratings are submitted if the user presses the submit button, and are written to the {@link com.rapidminer.tools.usagestats.UsageStatistics} under the provided key. + *

+ * + * @author Marco Boeck + * @since 8.1.2 + */ +public class FeedbackForm extends JPanel { + + + /** + * Indicate if the user chose thumbs-up/down or nothing yet. + */ + private enum FeedbackState { + NONE, + + POSITIVE, + + NEGATIVE + + } + + private static final Dimension MIN_SIZE_FREETEXT = new Dimension(50, 100); + private static final float FONT_SIZE_QUESTION = 14f; + + /** the duration the submission "thank you" note popup appears before fading out */ + private static final int NOTIFICATION_DURATION = 3000; + + + private final String key; + private final String id; + + private FeedbackState state; + + private JButton submitButton; + + + /** + * Create a new feedback form with the given category and id. + * + * @param key + * the key that is used to submit user feedback into the usage statistics + * @param id + * the id that is used to submit user feedback into the usage statistics + */ + public FeedbackForm(final String key, final String id) { + this.key = key; + this.id = id; + this.state = FeedbackState.NONE; + + initGUI(); + } + + /** + * Initializes the GUI. + */ + private void initGUI() { + setLayout(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + + JLabel questionLabel = new JLabel(I18N.getGUILabel("feedback_form.helpful.label")); + questionLabel.setFont(questionLabel.getFont().deriveFont(Font.BOLD).deriveFont(FONT_SIZE_QUESTION)); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.weightx = 1.0f; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.gridwidth = 3; + gbc.insets = new Insets(20, 15, 5, 0); + add(questionLabel, gbc); + + JToggleButton yesButton = new JToggleButton(); + yesButton.putClientProperty(RapidLookTools.PROPERTY_BUTTON_CIRCLE, Boolean.TRUE); + yesButton.setIcon(SwingTools.createIcon("24/" + I18N.getGUILabel("feedback_form.yes.icon"))); + yesButton.setToolTipText(I18N.getGUILabel("feedback_form.yes.tip")); + yesButton.setOpaque(false); + yesButton.setFocusPainted(false); + yesButton.addActionListener(actionEvent -> { + + enableSubmit(true); + state = FeedbackState.POSITIVE; + + // we also log just thumbs-up/down clicks to also capture users who never click "Submit" + submitToUsageStats(false, ""); + }); + gbc.gridy += 1; + gbc.weightx = 0.0f; + gbc.fill = GridBagConstraints.NONE; + gbc.gridwidth = 1; + gbc.insets = new Insets(10, 8, 0, 0); + add(yesButton, gbc); + + JToggleButton noButton = new JToggleButton(); + noButton.putClientProperty(RapidLookTools.PROPERTY_BUTTON_CIRCLE, Boolean.TRUE); + noButton.setIcon(SwingTools.createIcon("24/" + I18N.getGUILabel("feedback_form.no.icon"))); + noButton.setToolTipText(I18N.getGUILabel("feedback_form.no.tip")); + noButton.setOpaque(false); + noButton.setFocusPainted(false); + noButton.addActionListener(actionEvent -> { + + enableSubmit(true); + state = FeedbackState.NEGATIVE; + + // we also log just thumbs-up/down clicks to also capture users who never click "Submit" + submitToUsageStats(false, ""); + }); + gbc.gridx = 1; + gbc.weightx = 0.0f; + gbc.fill = GridBagConstraints.NONE; + add(noButton, gbc); + + // add horizontal filler + gbc.gridx = 2; + gbc.weightx = 1.0f; + gbc.fill = GridBagConstraints.HORIZONTAL; + add(new JLabel(), gbc); + + JTextArea freeTextArea = new JTextArea(5, 20); + freeTextArea.setLineWrap(true); + freeTextArea.setWrapStyleWord(true); + freeTextArea.setBorder(BorderFactory.createEmptyBorder()); + freeTextArea.getDocument().addDocumentListener(new DocumentListener() { + + @Override + public void insertUpdate(DocumentEvent e) { + updateSubmitStatus(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + updateSubmitStatus(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + updateSubmitStatus(); + } + + private void updateSubmitStatus() { + enableSubmit(!freeTextArea.getText().trim().isEmpty() || state != FeedbackState.NONE); + } + }); + PromptSupport.setFontStyle(Font.ITALIC, freeTextArea); + PromptSupport.setPrompt(I18N.getGUILabel("feedback_form.freetext.prompt"), freeTextArea); + gbc.gridx = 0; + gbc.gridy += 1; + gbc.weightx = 1.0f; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.gridwidth = 3; + gbc.insets = new Insets(12, 10, 0, 10); + JScrollPane scrollPaneText = new ExtendedJScrollPane(freeTextArea); + scrollPaneText.setBorder(BorderFactory.createLineBorder(Colors.TEXTFIELD_BORDER, 1, true)); + scrollPaneText.setMinimumSize(MIN_SIZE_FREETEXT); + add(scrollPaneText, gbc); + + submitButton = new JButton(I18N.getGUILabel("feedback_form.submit.label")); + submitButton.putClientProperty(RapidLookTools.PROPERTY_BUTTON_HIGHLIGHT_DARK, Boolean.TRUE); + submitButton.putClientProperty(RapidLookTools.PROPERTY_BUTTON_DARK_BORDER, Boolean.TRUE); + submitButton.setForeground(Colors.WHITE); + submitButton.setEnabled(false); + submitButton.setToolTipText(I18N.getGUILabel("feedback_form.submit_disabled.tip")); + submitButton.addActionListener(actionEvent -> { + + // prevent multiple submissions and hide feedback form + submitButton.setEnabled(false); + FeedbackForm.this.setVisible(false); + + submitToUsageStats(true, freeTextArea.getText().trim()); + + NotificationPopup.showFadingPopup( + SwingTools.createNotificationPanel("gui.dialog.message.feedback_form.success.icon", + "gui.dialog.message.feedback_form.success.message"), + MainFrame.getApplicationFrame(), NotificationPopup.PopupLocation.LOWER_RIGHT, NOTIFICATION_DURATION, 30, 40); + }); + gbc.gridy += 1; + gbc.gridwidth = 2; + gbc.anchor = GridBagConstraints.WEST; + gbc.weightx = 0.0f; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(10, 10, 10, 0); + add(submitButton, gbc); + + // add horizontal filler + gbc.gridx = 2; + gbc.weightx = 1.0f; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(0, 0, 0, 0); + add(new JLabel(), gbc); + + // add horizontal filler + gbc.gridx = 0; + gbc.gridy += 1; + gbc.weighty = 1.0f; + gbc.fill = GridBagConstraints.BOTH; + gbc.gridwidth = 3; + add(new JLabel(), gbc); + + + ButtonGroup feedbackGroup = new ButtonGroup(); + feedbackGroup.add(yesButton); + feedbackGroup.add(noButton); + } + + /** + * Enables the "Submit" button. + * @param enable + * if {@code true}, the submit button will be enabled and scrolled to visible; otherwise it will be disabled + * + */ + private void enableSubmit(boolean enable) { + submitButton.setEnabled(enable); + submitButton.setToolTipText(I18N.getGUILabel(enable ? "feedback_form.submit.tip" : "feedback_form.submit_disabled.tip")); + + if (enable) { + submitButton.scrollRectToVisible(submitButton.getBounds()); + } + } + + /** + * Submits the given rating to the usage stats. + * + * @param submitted + * {@code true} if user clicked "Submit" button; {@code false} if user just clicked thumbs-up/down buttons + * @param freeText + * the free text the user entered. Can be empty + */ + private void submitToUsageStats(final boolean submitted, final String freeText) { + ActionStatisticsCollector.INSTANCE.logFeedback(key, id, submitted, state.name(), freeText); + + } + +} diff --git a/src/main/java/com/rapidminer/gui/tools/dialogs/ButtonDialog.java b/src/main/java/com/rapidminer/gui/tools/dialogs/ButtonDialog.java index 3b3335291..fdc2a60c4 100644 --- a/src/main/java/com/rapidminer/gui/tools/dialogs/ButtonDialog.java +++ b/src/main/java/com/rapidminer/gui/tools/dialogs/ButtonDialog.java @@ -387,6 +387,8 @@ public ButtonDialog build() { public static final int GAP = 6; + public static final String WINDOW_CLOSING_EVENT_STRING = "WINDOW_CLOSING"; + protected static final Insets INSETS = new Insets(GAP, GAP, GAP, GAP); protected FixedWidthEditorPane infoTextLabel; diff --git a/src/main/java/com/rapidminer/gui/tools/dialogs/ConfirmDialog.java b/src/main/java/com/rapidminer/gui/tools/dialogs/ConfirmDialog.java index b15a87a6e..c22e6f26b 100644 --- a/src/main/java/com/rapidminer/gui/tools/dialogs/ConfirmDialog.java +++ b/src/main/java/com/rapidminer/gui/tools/dialogs/ConfirmDialog.java @@ -212,7 +212,11 @@ public void loggedActionPerformed(ActionEvent e) { } protected JButton makeNoButton() { - ResourceAction noAction = new ResourceAction("confirm.no") { + return makeNoButtonInternal("confirm.no"); + } + + protected JButton makeNoButtonInternal(String i18nKey) { + ResourceAction noAction = new ResourceAction(i18nKey) { private static final long serialVersionUID = -8887199234055845095L; diff --git a/src/main/java/com/rapidminer/gui/tools/dialogs/SetParameterDialog.java b/src/main/java/com/rapidminer/gui/tools/dialogs/SetParameterDialog.java index aa895faf0..363f97a4c 100644 --- a/src/main/java/com/rapidminer/gui/tools/dialogs/SetParameterDialog.java +++ b/src/main/java/com/rapidminer/gui/tools/dialogs/SetParameterDialog.java @@ -18,9 +18,6 @@ */ package com.rapidminer.gui.tools.dialogs; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; - import javax.swing.JButton; import javax.swing.JComponent; @@ -46,7 +43,6 @@ public class SetParameterDialog extends ButtonDialog { private final PropertyValueCellEditor editor; - private boolean canceled = false; public SetParameterDialog(final Operator operator, final ParameterType type) { super(ApplicationFrame.getApplicationFrame(), "set_parameter", ModalityType.MODELESS, new Object[] { type.getKey() @@ -56,22 +52,7 @@ public SetParameterDialog(final Operator operator, final ParameterType type) { editor = PropertyPanel.instantiateValueCellEditor(type, operator); JComponent editorComponent = (JComponent) editor.getTableCellEditorComponent(null, type.getDefaultValue(), false, 0, 1); - editorComponent.addKeyListener(new KeyListener() { - - @Override - public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { - canceled = true; - } - } - - @Override - public void keyReleased(KeyEvent e) {} - - @Override - public void keyTyped(KeyEvent e) {} - }); JButton okButton = makeOkButton("set_parameter_dialog_apply"); layoutDefault(editorComponent, okButton, makeCancelButton()); getRootPane().setDefaultButton(okButton); @@ -87,7 +68,7 @@ protected void ok() { Object value = editor.getCellEditorValue(); if (value != null && ((String) value).length() != 0) { operator.setParameter(type.getKey(), (String) value); - super.ok(); } + super.ok(); } } diff --git a/src/main/java/com/rapidminer/gui/viewer/PerformanceVectorViewer.java b/src/main/java/com/rapidminer/gui/viewer/PerformanceVectorViewer.java index c4e016e8e..824e41380 100644 --- a/src/main/java/com/rapidminer/gui/viewer/PerformanceVectorViewer.java +++ b/src/main/java/com/rapidminer/gui/viewer/PerformanceVectorViewer.java @@ -38,6 +38,7 @@ import com.rapidminer.gui.look.Colors; import com.rapidminer.gui.processeditor.results.ResultDisplayTools; import com.rapidminer.gui.tools.ExtendedJScrollPane; +import com.rapidminer.gui.tools.MenuShortcutJList; import com.rapidminer.gui.tools.ResourceLabel; import com.rapidminer.operator.IOContainer; import com.rapidminer.operator.performance.PerformanceCriterion; @@ -107,7 +108,7 @@ public PerformanceVectorViewer(final PerformanceVector performanceVector, final criteriaNameList.toArray(criteriaNames); // selection list - final JList criteriaList = new JList(criteriaNames) { + final JList criteriaList = new MenuShortcutJList(criteriaNames, false) { private static final long serialVersionUID = 3031125186920370793L; diff --git a/src/main/java/com/rapidminer/io/process/AutoModelProcessXMLFilter.java b/src/main/java/com/rapidminer/io/process/AutoModelProcessXMLFilter.java index 636c7e640..e6c586e75 100644 --- a/src/main/java/com/rapidminer/io/process/AutoModelProcessXMLFilter.java +++ b/src/main/java/com/rapidminer/io/process/AutoModelProcessXMLFilter.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2001-2017 by RapidMiner and the contributors + * Copyright (C) 2001-2018 by RapidMiner and the contributors * * Complete list of developers available at our web site: * diff --git a/src/main/java/com/rapidminer/io/process/ProcessLayoutXMLFilter.java b/src/main/java/com/rapidminer/io/process/ProcessLayoutXMLFilter.java index 28a4ccf63..764324c95 100644 --- a/src/main/java/com/rapidminer/io/process/ProcessLayoutXMLFilter.java +++ b/src/main/java/com/rapidminer/io/process/ProcessLayoutXMLFilter.java @@ -24,14 +24,18 @@ import java.util.Map; import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawUtils; +import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawer; import com.rapidminer.operator.ExecutionUnit; import com.rapidminer.operator.FlagUserData; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorChain; import com.rapidminer.operator.UserData; import com.rapidminer.operator.ports.Port; +import com.rapidminer.operator.ports.Ports; /** @@ -212,7 +216,13 @@ public void operatorImported(final Operator op, final Element opElement) { String x = opElement.getAttribute(XML_ATTRIBUTE_X_POSITION); String y = opElement.getAttribute(XML_ATTRIBUTE_Y_POSITION); String w = opElement.getAttribute(XML_ATTRIBUTE_WIDTH); + if (w == null || w.isEmpty()) { + w = Double.toString(ProcessDrawer.OPERATOR_WIDTH); + } String h = opElement.getAttribute(XML_ATTRIBUTE_HEIGHT); + if (h == null || h.isEmpty()) { + h = Double.toString(ProcessDrawUtils.calcHeighForOperator(op)); + } if (x != null && x.length() > 0) { try { Rectangle2D rect = new Rectangle2D.Double(Double.parseDouble(x), Double.parseDouble(y), @@ -230,35 +240,35 @@ public void operatorImported(final Operator op, final Element opElement) { @Override public void executionUnitImported(final ExecutionUnit process, final Element element) { NodeList children = element.getChildNodes(); - for (Port port : process.getInnerSources().getAllPorts()) { + setPortSpacings(process.getInnerSources(), children, XML_ATTRIBUTE_SOURCE); + setPortSpacings(process.getInnerSinks(), children, XML_ATTRIBUTE_SINK); + } + + /** + * Extracts port spacings for the given {@link Ports} instance. + * + * @param ports + * the ports to be spaced + * @param children + * the list of children to check + * @since 8.2 + */ + private void setPortSpacings(Ports ports, NodeList children, String portPrefix) { + for (Port port : ports.getAllPorts()) { for (int i = 0; i < children.getLength(); i++) { - if (children.item(i) instanceof Element - && XML_TAG_PORT_SPACING.equals(((Element) children.item(i)).getTagName())) { - Element psElement = (Element) children.item(i); - if ((XML_ATTRIBUTE_SOURCE + port.getName()).equals(psElement.getAttribute(XML_ATTRIBUTE_PORT))) { - try { - setPortSpacing(port, Integer.parseInt(psElement.getAttribute(XML_ATTRIBUTE_SPACING))); - } catch (NumberFormatException e) { - // do nothing - } - break; - } + Node item = children.item(i); + if (!(item instanceof Element) || !XML_TAG_PORT_SPACING.equals(((Element) item).getTagName())) { + continue; } - } - } - for (Port port : process.getInnerSinks().getAllPorts()) { - for (int i = 0; i < children.getLength(); i++) { - if (children.item(i) instanceof Element - && XML_TAG_PORT_SPACING.equals(((Element) children.item(i)).getTagName())) { - Element psElement = (Element) children.item(i); - if ((XML_ATTRIBUTE_SINK + port.getName()).equals(psElement.getAttribute(XML_ATTRIBUTE_PORT))) { - try { - setPortSpacing(port, Integer.parseInt(psElement.getAttribute(XML_ATTRIBUTE_SPACING))); - } catch (NumberFormatException e) { - // do nothing - } - break; + Element psElement = (Element) item; + String portName = psElement.getAttribute(XML_ATTRIBUTE_PORT); + if ((portPrefix + port.getName()).equals(portName)) { + try { + setPortSpacing(port, Integer.parseInt(psElement.getAttribute(XML_ATTRIBUTE_SPACING))); + } catch (NumberFormatException e) { + // do nothing } + break; } } } @@ -268,7 +278,7 @@ public void executionUnitImported(final ExecutionUnit process, final Element ele * Looks up the spacing of the specified {@link Port}. * * @param port - * The port. + * The port. * @return Additional spacing. */ public static int lookupPortSpacing(Port port) { @@ -292,9 +302,9 @@ public static int lookupPortSpacing(Port port) { * Sets the spacing of the specified {@link Port}. * * @param port - * The port. + * The port. * @param spacing - * The additional spacing. + * The additional spacing. */ public static void setPortSpacing(Port port, Integer spacing) { Operator operator = port.getPorts().getOwner().getOperator(); @@ -313,7 +323,7 @@ public static void setPortSpacing(Port port, Integer spacing) { * Resets the spacing of the specified {@link Port}. * * @param port - * The port. + * The port. */ public static void resetPortSpacing(Port port) { Operator operator = port.getPorts().getOwner().getOperator(); @@ -328,7 +338,7 @@ public static void resetPortSpacing(Port port) { * Looks up the position rectangle of the specified {@link Operator}. * * @param operator - * The operator. + * The operator. * @return The rectangle or null. */ public static Rectangle2D lookupOperatorRectangle(Operator operator) { @@ -344,9 +354,9 @@ public static Rectangle2D lookupOperatorRectangle(Operator operator) { * Sets the position rectangle of the specified {@link Operator}. * * @param operator - * The operator. + * The operator. * @param rect - * The rectangle. + * The rectangle. */ public static void setOperatorRectangle(Operator operator, Rectangle2D rect) { operator.setUserData(KEY_OPERATOR_RECTANGLE, new Rectangle2DWrapper(rect)); @@ -356,7 +366,7 @@ public static void setOperatorRectangle(Operator operator, Rectangle2D rect) { * Resets the position rectangle of the specified {@link Operator}. * * @param operator - * The operator. + * The operator. */ public static void resetOperatorRectangle(Operator operator) { operator.setUserData(KEY_OPERATOR_RECTANGLE, null); @@ -366,7 +376,7 @@ public static void resetOperatorRectangle(Operator operator) { * Looks up the view position of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain + * The operator chain * @return The position or null * @since 7.5 */ @@ -383,9 +393,9 @@ public static Point lookupOperatorChainPosition(OperatorChain operatorChain) { * Sets the view position of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain + * The operator chain * @param position - * The center position + * The center position * @since 7.5 */ public static void setOperatorChainPosition(OperatorChain operatorChain, Point position) { @@ -396,7 +406,7 @@ public static void setOperatorChainPosition(OperatorChain operatorChain, Point p * Resets the view position of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain + * The operator chain * @since 7.5 */ public static void resetOperatorChainPosition(OperatorChain operatorChain) { @@ -407,7 +417,7 @@ public static void resetOperatorChainPosition(OperatorChain operatorChain) { * Looks up the zoom of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain + * The operator chain * @return The zoom or null * @since 7.5 */ @@ -424,9 +434,9 @@ public static Double lookupOperatorChainZoom(OperatorChain operatorChain) { * Sets the zoom of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain + * The operator chain * @param zoom - * The zoom + * The zoom * @since 7.5 */ public static void setOperatorChainZoom(OperatorChain operatorChain, Double zoom) { @@ -437,7 +447,7 @@ public static void setOperatorChainZoom(OperatorChain operatorChain, Double zoom * Resets the zoom of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain + * The operator chain * @since 7.5 */ public static void resetOperatorChainZoom(OperatorChain operatorChain) { @@ -448,7 +458,7 @@ public static void resetOperatorChainZoom(OperatorChain operatorChain) { * Looks up the scroll position of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain + * The operator chain * @return The scroll position or null * @since 7.5 */ @@ -465,9 +475,9 @@ public static Point lookupScrollPosition(OperatorChain operatorChain) { * Sets the scroll position of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain + * The operator chain * @param scrollPos - * The scroll position + * The scroll position * @since 7.5 */ public static void setScrollPosition(OperatorChain operatorChain, Point scrollPos) { @@ -478,7 +488,7 @@ public static void setScrollPosition(OperatorChain operatorChain, Point scrollPo * Resets the scroll position of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain + * The operator chain * @since 7.5 */ public static void resetScrollPosition(OperatorChain operatorChain) { @@ -489,7 +499,7 @@ public static void resetScrollPosition(OperatorChain operatorChain) { * Looks up the scroll process index of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain + * The operator chain * @return The index or null * @since 7.5 */ @@ -506,9 +516,9 @@ public static Double lookupScrollIndex(OperatorChain operatorChain) { * Sets the scroll process index of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain + * The operator chain * @param index - * The process index + * The process index * @since 7.5 */ public static void setScrollIndex(OperatorChain operatorChain, Double index) { @@ -519,7 +529,7 @@ public static void setScrollIndex(OperatorChain operatorChain, Double index) { * Resets the scroll process index of the specified {@link OperatorChain}. * * @param operatorChain - * The operator chain. + * The operator chain. * @since 7.5 */ public static void resetScrollIndex(OperatorChain operatorChain) { @@ -530,7 +540,7 @@ public static void resetScrollIndex(OperatorChain operatorChain) { * Looks up the restore flag of the specified {@link Operator}. * * @param operator - * the operator + * the operator * @return the flag or null * @since 7.5 */ @@ -543,7 +553,7 @@ public static boolean lookupRestore(Operator operator) { * Sets the restore flag of the specified {@link Operator}. * * @param operator - * the operator + * the operator * @since 7.5 */ public static void setRestore(Operator operator) { @@ -554,7 +564,7 @@ public static void setRestore(Operator operator) { * Resets the restore flag of the specified {@link OperatorChain}. * * @param operator - * The operator + * The operator * @since 7.5 */ public static void resetRestore(Operator operator) { diff --git a/src/main/java/com/rapidminer/operator/AbstractExampleSetProcessing.java b/src/main/java/com/rapidminer/operator/AbstractExampleSetProcessing.java index b3c95dad1..8f1bac525 100644 --- a/src/main/java/com/rapidminer/operator/AbstractExampleSetProcessing.java +++ b/src/main/java/com/rapidminer/operator/AbstractExampleSetProcessing.java @@ -42,9 +42,30 @@ */ public abstract class AbstractExampleSetProcessing extends Operator { - private final InputPort exampleSetInput = getInputPorts().createPort("example set input"); - private final OutputPort exampleSetOutput = getOutputPorts().createPort("example set output"); - private final OutputPort originalOutput = getOutputPorts().createPort("original"); + /** + * name of the example set input port + * + * @since 8.2.0 + */ + public static final String EXAMPLE_SET_INPUT_PORT_NAME = "example set input"; + + /** + * name of the example set output port + * + * @since 8.2.0 + */ + public static final String EXAMPLE_SET_OUTPUT_PORT_NAME = "example set output"; + + /** + * name of the original output port + * + * @since 8.2.0 + */ + public static final String ORIGINAL_OUTPUT_PORT_NAME = "original"; + + private final InputPort exampleSetInput = getInputPorts().createPort(EXAMPLE_SET_INPUT_PORT_NAME); + private final OutputPort exampleSetOutput = getOutputPorts().createPort(EXAMPLE_SET_OUTPUT_PORT_NAME); + private final OutputPort originalOutput = getOutputPorts().createPort(ORIGINAL_OUTPUT_PORT_NAME); public AbstractExampleSetProcessing(OperatorDescription description) { super(description); diff --git a/src/main/java/com/rapidminer/operator/ExecutionUnit.java b/src/main/java/com/rapidminer/operator/ExecutionUnit.java index 19d52dae8..8caa45af6 100644 --- a/src/main/java/com/rapidminer/operator/ExecutionUnit.java +++ b/src/main/java/com/rapidminer/operator/ExecutionUnit.java @@ -1,964 +1,964 @@ -/** - * Copyright (C) 2001-2018 by RapidMiner and the contributors +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors * - * Complete list of developers available at our web site: + * Complete list of developers available at our web site: * - * http://rapidminer.com + * http://rapidminer.com * - * This program is free software: you can redistribute it and/or modify it under the terms of the - * GNU Affero 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 free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 - * Affero General Public License for more details. + * 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 + * Affero General Public License for more details. * - * You should have received a copy of the GNU Affero General Public License along with this program. - * If not, see http://www.gnu.org/licenses/. -*/ -package com.rapidminer.operator; - -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.PriorityQueue; -import java.util.Set; -import java.util.TreeMap; -import java.util.Vector; -import java.util.logging.Level; - -import com.rapidminer.Process; -import com.rapidminer.operator.execution.UnitExecutionFactory; -import com.rapidminer.operator.execution.UnitExecutor; -import com.rapidminer.operator.ports.InputPort; -import com.rapidminer.operator.ports.InputPorts; -import com.rapidminer.operator.ports.OutputPort; -import com.rapidminer.operator.ports.OutputPorts; -import com.rapidminer.operator.ports.Port; -import com.rapidminer.operator.ports.PortException; -import com.rapidminer.operator.ports.PortOwner; -import com.rapidminer.operator.ports.metadata.CompatibilityLevel; -import com.rapidminer.operator.ports.metadata.OperatorLoopError; -import com.rapidminer.tools.AbstractObservable; -import com.rapidminer.tools.DelegatingObserver; -import com.rapidminer.tools.LogService; -import com.rapidminer.tools.Observer; -import com.rapidminer.tools.Tools; - - -/** - * A process is a collection of operators whose ports can be wired. A process provides input and - * output ports to its contained operators. ExecutionUnit replaces the legacy OperatorChain. This - * class takes care of executing the operators in the correct order by sorting them topologically - * with respect to their dependencies. - * - * @author Simon Fischer - */ -public class ExecutionUnit extends AbstractObservable { - - private final PortOwner portOwner = new PortOwner() { - - @Override - public OperatorChain getPortHandler() { - return getEnclosingOperator(); - } - - @Override - public String getName() { - return ExecutionUnit.this.getName(); - } - - @Override - public Operator getOperator() { - return getEnclosingOperator(); - } - - @Override - public ExecutionUnit getConnectionContext() { - return ExecutionUnit.this; - } - }; - - private String name; - private final OperatorChain enclosingOperator; - - private final InputPorts innerInputPorts; - private final OutputPorts innerOutputPorts; - private Vector operators = new Vector(); - private Vector executionOrder; - - /** - * Container for user data. - */ - private Map> userData; - - /** - * Lock object for user data container. - */ - private final Object userDataLock = new Object(); - - private final Observer delegatingPortObserver = new DelegatingObserver(this, this); - private final Observer delegatingOperatorObserver = new DelegatingObserver(this, - this); - - public ExecutionUnit(OperatorChain enclosingOperator, String name) { - this.name = name; - - innerInputPorts = enclosingOperator.createInnerSinks(portOwner); - innerOutputPorts = enclosingOperator.createInnerSources(portOwner); - this.enclosingOperator = enclosingOperator; - innerInputPorts.addObserver(delegatingPortObserver, false); - innerOutputPorts.addObserver(delegatingPortObserver, false); - int index = 0; - do { - char c = name.charAt(index); - if (!(Character.isUpperCase(c) || Character.isDigit(c))) { - // LogService.getRoot().warning("Process name does not follow naming conventions: - // "+name+" (in "+enclosingOperator.getOperatorDescription().getName()+")"); - LogService.getRoot().log(Level.WARNING, - "com.rapidminer.operator.ExecutionUnit.process_name_does_not_follow_name_conventions", - new Object[] { name, enclosingOperator.getOperatorDescription().getName() }); - } - index = name.indexOf(' ', index) + 1; - } while (index != 0); - } - - public InputPorts getInnerSinks() { - return innerInputPorts; - } - - public OutputPorts getInnerSources() { - return innerOutputPorts; - } - - /** - * Same as {@link #addOperator(Operator, boolean)}. - */ - public int addOperator(Operator operator) { - return addOperator(operator, true); - } - - /** - * Adds the operator to this execution unit. - * - * @param registerWithProcess - * Typically true. If false, the operator will not be registered with its parent - * process. - * @return the new index of the operator. - */ - public int addOperator(Operator operator, boolean registerWithProcess) { - if (operator == null) { - throw new NullPointerException("operator cannot be null!"); - } - if (operator instanceof ProcessRootOperator) { - throw new IllegalArgumentException( - "'Process' operator cannot be added. It must always be the top-level operator!"); - } - operators.add(operator); - registerOperator(operator, registerWithProcess); - return operators.size() - 1; - } - - /** - * Adds the operator to this execution unit. The operator at this index and all subsequent - * operators are shifted to the right. The operator is registered automatically. - */ - public void addOperator(Operator operator, int index) { - if (operator == null) { - throw new NullPointerException("operator cannot be null!"); - } - if (operator instanceof ProcessRootOperator) { - throw new IllegalArgumentException( - "'Process' operator cannot be added. It must always be the top-level operator!"); - } - operators.add(index, operator); - registerOperator(operator, true); - } - - public int getIndexOfOperator(Operator operator) { - return operators.indexOf(operator); - } - - /** - * Looks up {@link UserData} entries. Returns null if key is unknown. - * - * @param The - * key of the user data. - * @return The user data. - */ - public UserData getUserData(String key) { - synchronized (this.userDataLock) { - if (this.userData == null) { - return null; - } else { - return this.userData.get(key); - } - } - } - - /** - * Stores arbitrary {@link UserData}. - * - * @param key - * The key to used to identify the data. - * @param data - * The user data. - */ - public void setUserData(String key, UserData data) { - synchronized (this.userDataLock) { - if (this.userData == null) { - this.userData = new TreeMap<>(); - } - this.userData.put(key, data); - } - } - - private void registerOperator(Operator operator, boolean registerWithProcess) { - operator.setEnclosingProcess(this); - Process process = getEnclosingOperator().getProcess(); - if (process != null && registerWithProcess) { - operator.registerOperator(process); - } - fireUpdate(this); - operator.addObserver(delegatingOperatorObserver, false); - operator.clear(Port.CLEAR_ALL); - if (process != null) { - process.fireOperatorAdded(operator); - } - } - - private void unregister(Operator operator) { - operator.removeObserver(delegatingOperatorObserver); - } - - /** - * Removes the given operator. Don't call this method directly but call - * {@link Operator#remove()}. - */ - protected void removeOperator(Operator operator) { - if (!operators.contains(operator)) { - throw new NoSuchElementException("Operator " + operator.getName() + " not contained in " + getName() + "!"); - } - int oldIndex = operators.indexOf(operator); - int oldIndexAmongEnabled = getEnabledOperators().indexOf(operator); - operators.remove(operator); - unregister(operator); - Process process = getEnclosingOperator().getProcess(); - if (process != null) { - process.fireOperatorRemoved(operator, oldIndex, oldIndexAmongEnabled); - } - operator.setEnclosingProcess(null); - fireUpdate(this); - } - - public void clear(int clearFlags) { - for (Operator operator : operators) { - operator.clear(clearFlags); - } - getInnerSinks().clear(clearFlags); - getInnerSources().clear(clearFlags); - } - + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.operator; + +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.PriorityQueue; +import java.util.Set; +import java.util.TreeMap; +import java.util.Vector; +import java.util.logging.Level; + +import com.rapidminer.Process; +import com.rapidminer.operator.execution.UnitExecutionFactory; +import com.rapidminer.operator.execution.UnitExecutor; +import com.rapidminer.operator.ports.InputPort; +import com.rapidminer.operator.ports.InputPorts; +import com.rapidminer.operator.ports.OutputPort; +import com.rapidminer.operator.ports.OutputPorts; +import com.rapidminer.operator.ports.Port; +import com.rapidminer.operator.ports.PortException; +import com.rapidminer.operator.ports.PortOwner; +import com.rapidminer.operator.ports.metadata.CompatibilityLevel; +import com.rapidminer.operator.ports.metadata.OperatorLoopError; +import com.rapidminer.tools.AbstractObservable; +import com.rapidminer.tools.DelegatingObserver; +import com.rapidminer.tools.LogService; +import com.rapidminer.tools.Observer; +import com.rapidminer.tools.Tools; + + +/** + * A process is a collection of operators whose ports can be wired. A process provides input and + * output ports to its contained operators. ExecutionUnit replaces the legacy OperatorChain. This + * class takes care of executing the operators in the correct order by sorting them topologically + * with respect to their dependencies. + * + * @author Simon Fischer + */ +public class ExecutionUnit extends AbstractObservable { + + private final PortOwner portOwner = new PortOwner() { + + @Override + public OperatorChain getPortHandler() { + return getEnclosingOperator(); + } + + @Override + public String getName() { + return ExecutionUnit.this.getName(); + } + + @Override + public Operator getOperator() { + return getEnclosingOperator(); + } + + @Override + public ExecutionUnit getConnectionContext() { + return ExecutionUnit.this; + } + }; + + private String name; + private final OperatorChain enclosingOperator; + + private final InputPorts innerInputPorts; + private final OutputPorts innerOutputPorts; + private Vector operators = new Vector<>(); + private Vector executionOrder; + + /** + * Container for user data. + */ + private Map> userData; + + /** + * Lock object for user data container. + */ + private final Object userDataLock = new Object(); + + private final Observer delegatingPortObserver = new DelegatingObserver<>(this, this); + private final Observer delegatingOperatorObserver = new DelegatingObserver<>(this, + this); + + public ExecutionUnit(OperatorChain enclosingOperator, String name) { + this.name = name; + + innerInputPorts = enclosingOperator.createInnerSinks(portOwner); + innerOutputPorts = enclosingOperator.createInnerSources(portOwner); + this.enclosingOperator = enclosingOperator; + innerInputPorts.addObserver(delegatingPortObserver, false); + innerOutputPorts.addObserver(delegatingPortObserver, false); + int index = 0; + do { + char c = name.charAt(index); + if (!(Character.isUpperCase(c) || Character.isDigit(c))) { + // LogService.getRoot().warning("Process name does not follow naming conventions: + // "+name+" (in "+enclosingOperator.getOperatorDescription().getName()+")"); + LogService.getRoot().log(Level.WARNING, + "com.rapidminer.operator.ExecutionUnit.process_name_does_not_follow_name_conventions", + new Object[]{name, enclosingOperator.getOperatorDescription().getName()}); + } + index = name.indexOf(' ', index) + 1; + } while (index != 0); + } + + public InputPorts getInnerSinks() { + return innerInputPorts; + } + + public OutputPorts getInnerSources() { + return innerOutputPorts; + } + + /** + * Same as {@link #addOperator(Operator, boolean) addOperator(Operator, true)}. + */ + public int addOperator(Operator operator) { + return addOperator(operator, true); + } + + /** + * Adds the operator to this execution unit. + * + * @param registerWithProcess + * Typically true. If false, the operator will not be registered with its parent process. + * @return the new index of the operator. + * @see #addOperator(Operator, int, boolean) addOperator(Operator, -1, boolean) + */ + public int addOperator(Operator operator, boolean registerWithProcess) { + addOperator(operator, -1, registerWithProcess); + return operators.size() - 1; + } + + /** + * Adds the operator to this execution unit. The operator at this index and all subsequent + * operators are shifted to the right. The operator is registered automatically. + * + * @see #addOperator(Operator, int, boolean) addOperator(Operator, index, true) + */ + public void addOperator(Operator operator, int index) { + addOperator(operator, index, true); + } + + /** + * Adds the operator to this execution unit. If the index is non-negative, the operator at that + * index and all subsequent operators are shifted to the right. The operator is registered if so + * specified. + * + * @param operator + * the operator to be added + * @param index + * the position where to add the operator, or {@code -1} to simply append. + * @param registerWithProcess + * Typically true. If false, the operator will not be registered with its parent + * process. + * @since 8.2 + */ + private void addOperator(Operator operator, int index, boolean registerWithProcess) { + if (operator == null) { + throw new NullPointerException("operator cannot be null!"); + } + if (operator instanceof ProcessRootOperator) { + throw new IllegalArgumentException( + "'Process' operator cannot be added. It must always be the top-level operator!"); + } + if (index == -1) { + operators.add(operator); + } else { + operators.add(index, operator); + } + registerOperator(operator, registerWithProcess); + } + + public int getIndexOfOperator(Operator operator) { + return operators.indexOf(operator); + } + + /** + * Looks up {@link UserData} entries. Returns null if key is unknown. + * + * @param The + * key of the user data. + * @return The user data. + */ + public UserData getUserData(String key) { + synchronized (this.userDataLock) { + if (this.userData == null) { + return null; + } else { + return this.userData.get(key); + } + } + } + + /** + * Stores arbitrary {@link UserData}. + * + * @param key + * The key to used to identify the data. + * @param data + * The user data. + */ + public void setUserData(String key, UserData data) { + synchronized (this.userDataLock) { + if (this.userData == null) { + this.userData = new TreeMap<>(); + } + this.userData.put(key, data); + } + } + + private void registerOperator(Operator operator, boolean registerWithProcess) { + operator.setEnclosingProcess(this); + Process process = getEnclosingOperator().getProcess(); + if (process != null && registerWithProcess) { + operator.registerOperator(process); + } + fireUpdate(this); + operator.addObserver(delegatingOperatorObserver, false); + operator.clear(Port.CLEAR_ALL); + if (process != null) { + process.fireOperatorAdded(operator); + } + } + + private void unregister(Operator operator) { + operator.removeObserver(delegatingOperatorObserver); + } + + /** + * Removes the given operator. Don't call this method directly but call + * {@link Operator#remove()}. + */ + protected void removeOperator(Operator operator) { + if (!operators.contains(operator)) { + throw new NoSuchElementException("Operator " + operator.getName() + " not contained in " + getName() + "!"); + } + int oldIndex = operators.indexOf(operator); + int oldIndexAmongEnabled = getEnabledOperators().indexOf(operator); + operators.remove(operator); + unregister(operator); + Process process = getEnclosingOperator().getProcess(); + if (process != null) { + process.fireOperatorRemoved(operator, oldIndex, oldIndexAmongEnabled); + } + operator.setEnclosingProcess(null); + fireUpdate(this); + } + + public void clear(int clearFlags) { + for (Operator operator : operators) { + operator.clear(clearFlags); + } + getInnerSinks().clear(clearFlags); + getInnerSources().clear(clearFlags); + } + /** Helper class to count the number of dependencies of an operator. */ - private static class EdgeCounter { - - private final Map numIncomingEdges = new LinkedHashMap(); - - private EdgeCounter(Collection operators) { - for (Operator op : operators) { - numIncomingEdges.put(op, 0); - } - } - - private void incNumEdges(Operator op) { - Integer num = numIncomingEdges.get(op); - if (num == null) { - // this can only happen if we add edges to inner ports of the enclosing operator. - return; - } - num = num + 1; - numIncomingEdges.put(op, num); - } - - private int decNumEdges(Operator op) { - Integer num = numIncomingEdges.get(op); - // this can only happen if we add edges to inner ports of the enclosing operator. - if (num == null) { - return -1; - } - num = num - 1; - assert num >= 0; - numIncomingEdges.put(op, num); - return num; - } - - private LinkedList getIndependentOperators() { - LinkedList independentOperators = new LinkedList(); - for (Map.Entry entry : numIncomingEdges.entrySet()) { - if (entry.getValue() == null || entry.getValue() == 0) { - independentOperators.add(entry.getKey()); - } - } - return independentOperators; - } - } - - /** - * Sorts the operators topologically, i.e. such that operator i in the returned - * ordering has dependencies (i.e. connected {@link InputPort}s) only from operators - * 0..i-1. - */ - public Vector topologicalSort() { - final Map originalIndices = new HashMap(); - for (int i = 0; i < operators.size(); i++) { - originalIndices.put(operators.get(i), i); - } - EdgeCounter counter = new EdgeCounter(operators); - for (Operator child : getOperators()) { - for (OutputPort out : child.getOutputPorts().getAllPorts()) { - InputPort dest = out.getDestination(); - if (dest != null) { - counter.incNumEdges(dest.getPorts().getOwner().getOperator()); - } - } - } - Vector sorted = new Vector(); - PriorityQueue independentOperators = new PriorityQueue(Math.max(1, operators.size()), - new Comparator() { - - @Override - public int compare(Operator o1, Operator o2) { - return originalIndices.get(o1) - originalIndices.get(o2); - } - }); - independentOperators.addAll(counter.getIndependentOperators()); - while (!independentOperators.isEmpty()) { - Operator first = independentOperators.poll(); - sorted.add(first); - for (OutputPort out : first.getOutputPorts().getAllPorts()) { - InputPort dest = out.getDestination(); - if (dest != null) { - Operator destOp = dest.getPorts().getOwner().getOperator(); - if (counter.decNumEdges(destOp) == 0) { - // independentOperators.addFirst(destOp); - independentOperators.add(destOp); - } - } - } - } - return sorted; - } - - protected void updateExecutionOrder() { - this.executionOrder = topologicalSort(); - if (!this.executionOrder.equals(operators)) { - if (operators.size() != executionOrder.size()) { - // we have a circle. without a check, operator vanishes. - return; - } - this.operators = this.executionOrder; - getEnclosingOperator().getProcess().fireExecutionOrderChanged(this); - } - for (Operator operator : this.operators) { - operator.updateExecutionOrder(); - } - } - - public void transformMetaData() { - List sorted = topologicalSort(); - for (Operator op : sorted) { - op.transformMetaData(); - } - if (sorted.size() != operators.size()) { - List remainder = new LinkedList(operators); - remainder.removeAll(sorted); - for (Operator nodeInCircle : remainder) { - for (OutputPort outputPort : nodeInCircle.getOutputPorts().getAllPorts()) { - InputPort destination = outputPort.getDestination(); - if (destination != null && remainder.contains(destination.getPorts().getOwner().getOperator())) { - if (destination.getSource() != null) { - // (source can be null *during* a disconnect in which case - // both the source and the destination fire an update - // which leads to this inconsistent state) - destination.addError(new OperatorLoopError(destination)); - } - outputPort.addError(new OperatorLoopError(outputPort)); - } - } - } - } - getInnerSinks().checkPreconditions(); - } - + private static class EdgeCounter { + + private final Map numIncomingEdges = new LinkedHashMap<>(); + + private EdgeCounter(Collection operators) { + for (Operator op : operators) { + numIncomingEdges.put(op, 0); + } + } + + private void incNumEdges(Operator op) { + Integer num = numIncomingEdges.get(op); + if (num == null) { + // this can only happen if we add edges to inner ports of the enclosing operator. + return; + } + num = num + 1; + numIncomingEdges.put(op, num); + } + + private int decNumEdges(Operator op) { + Integer num = numIncomingEdges.get(op); + // this can only happen if we add edges to inner ports of the enclosing operator. + if (num == null) { + return -1; + } + num = num - 1; + assert num >= 0; + numIncomingEdges.put(op, num); + return num; + } + + private LinkedList getIndependentOperators() { + LinkedList independentOperators = new LinkedList<>(); + for (Map.Entry entry : numIncomingEdges.entrySet()) { + if (entry.getValue() == null || entry.getValue() == 0) { + independentOperators.add(entry.getKey()); + } + } + return independentOperators; + } + } + + /** + * Sorts the operators topologically, i.e. such that operator i in the returned + * ordering has dependencies (i.e. connected {@link InputPort}s) only from operators + * 0..i-1. + */ + public Vector topologicalSort() { + final Map originalIndices = new HashMap<>(); + for (int i = 0; i < operators.size(); i++) { + originalIndices.put(operators.get(i), i); + } + EdgeCounter counter = new EdgeCounter(operators); + for (Operator child : getOperators()) { + for (OutputPort out : child.getOutputPorts().getAllPorts()) { + InputPort dest = out.getDestination(); + if (dest != null) { + counter.incNumEdges(dest.getPorts().getOwner().getOperator()); + } + } + } + Vector sorted = new Vector<>(); + PriorityQueue independentOperators = new PriorityQueue<>(Math.max(1, operators.size()), + Comparator.comparingInt(originalIndices::get)); + independentOperators.addAll(counter.getIndependentOperators()); + while (!independentOperators.isEmpty()) { + Operator first = independentOperators.poll(); + sorted.add(first); + for (OutputPort out : first.getOutputPorts().getAllPorts()) { + InputPort dest = out.getDestination(); + if (dest != null) { + Operator destOp = dest.getPorts().getOwner().getOperator(); + if (counter.decNumEdges(destOp) == 0) { + // independentOperators.addFirst(destOp); + independentOperators.add(destOp); + } + } + } + } + return sorted; + } + + protected void updateExecutionOrder() { + this.executionOrder = topologicalSort(); + if (!this.executionOrder.equals(operators)) { + if (operators.size() != executionOrder.size()) { + // we have a circle. without a check, operator vanishes. + return; + } + this.operators = this.executionOrder; + getEnclosingOperator().getProcess().fireExecutionOrderChanged(this); + } + for (Operator operator : this.operators) { + operator.updateExecutionOrder(); + } + } + + public void transformMetaData() { + List sorted = topologicalSort(); + for (Operator op : sorted) { + op.transformMetaData(); + } + if (sorted.size() != operators.size()) { + List remainder = new LinkedList<>(operators); + remainder.removeAll(sorted); + for (Operator nodeInCircle : remainder) { + for (OutputPort outputPort : nodeInCircle.getOutputPorts().getAllPorts()) { + InputPort destination = outputPort.getDestination(); + if (destination != null && remainder.contains(destination.getPorts().getOwner().getOperator())) { + if (destination.getSource() != null) { + // (source can be null *during* a disconnect in which case + // both the source and the destination fire an update + // which leads to this inconsistent state) + destination.addError(new OperatorLoopError(destination)); + } + outputPort.addError(new OperatorLoopError(outputPort)); + } + } + } + } + getInnerSinks().checkPreconditions(); + } + /** Returns an unmodifiable view of the operators contained in this process. */ - public List getOperators() { - return Collections.unmodifiableList(new ArrayList<>(operators)); - } - - /** - * Use this method only in cases where you are sure that you don't want a - * ConcurrentModificationException to occur when the list of operators is modified. - */ - public Enumeration getOperatorEnumeration() { - return operators.elements(); - } - + public List getOperators() { + return Collections.unmodifiableList(new ArrayList<>(operators)); + } + + /** + * Use this method only in cases where you are sure that you don't want a + * ConcurrentModificationException to occur when the list of operators is modified. + */ + public Enumeration getOperatorEnumeration() { + return operators.elements(); + } + /** Returns an unmodifiable view of the operators contained in this process. */ - public List getEnabledOperators() { - return new EnabledOperatorView(operators); - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - + public List getEnabledOperators() { + return new EnabledOperatorView(operators); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + /** Returns the operator that contains this process as a subprocess. */ - public OperatorChain getEnclosingOperator() { - return enclosingOperator; - } - - private void unwire(boolean recursive) { - getInnerSources().disconnectAll(); - for (Operator op : getOperators()) { - unwire(op, recursive); - } - } - - private void unwire(Operator op, boolean recursive) throws PortException { - op.getOutputPorts().disconnectAll(); - if (recursive) { - if (op instanceof OperatorChain) { - for (ExecutionUnit subprocess : ((OperatorChain) op).getSubprocesses()) { - subprocess.unwire(recursive); - } - } - } - } - - @SuppressWarnings("deprecation") - private void autoWire(CompatibilityLevel level, InputPorts inputPorts, LinkedList readyOutputs) - throws PortException { - boolean success = false; - do { - Set complete = new HashSet(); - for (InputPort in : inputPorts.getAllPorts()) { - success = false; - if (!in.isConnected() && !complete.contains(in) - && in.getPorts().getOwner().getOperator().shouldAutoConnect(in)) { - Iterator outIterator; - // TODO: Simon: Does the same in both cases. Check again. - if (in.simulatesStack()) { - outIterator = readyOutputs.descendingIterator(); - } else { - outIterator = readyOutputs.descendingIterator(); - } - while (outIterator.hasNext()) { - OutputPort outCandidate = outIterator.next(); - // TODO: Remove shouldAutoConnect() in later versions - Operator owner = outCandidate.getPorts().getOwner().getOperator(); - if (owner.shouldAutoConnect(outCandidate)) { - if (outCandidate.getMetaData() != null) { - if (in.isInputCompatible(outCandidate.getMetaData(), level)) { - readyOutputs.remove(outCandidate); - outCandidate.connectTo(in); - // we cannot continue with the remaining input ports - // since connecting may have triggered the creation of new input - // ports - // which would result in undefined behavior and a - // ConcurrentModificationException - success = true; - break; - } - } - } - } - // no port found. - complete.add(in); - if (success) { - break; - } - } - } - } while (success); - } - - /** - * Transforms the meta data of the enclosing operator. Required in {@link #autoWire()} after - * each Operator that has been wired. - */ - private void transformMDNeighbourhood() { - getEnclosingOperator().transformMetaData(); - } - - /** - * Connects the ports automatically in a first-fit approach. Operators are connected in their - * ordering within the {@link #operators} list. Every input of every operator is connected to - * the first compatible output of an operator "left" of this operator. This corresponds to the - * way, IOObjects were consumed in the pre-5.0 version. Disabled operators are skipped. - * - *
- * - * @param level - * If level is {@link CompatibilityLevel#VERSION_5}, an input is considered - * compatible only if it satisfies all meta data constraints. For - * {@link CompatibilityLevel#PRE_VERSION_5} we only consider the classes. - * - * @param keepConnections - * if true, don't unwire old connections before rewiring. - */ - public void autoWire(CompatibilityLevel level, boolean keepConnections, boolean recursive) throws PortException { - if (!keepConnections) { - unwire(recursive); - } - // store all outputs. Scan them to find matching inputs. - LinkedList readyOutputs = new LinkedList(); - addReadyOutputs(readyOutputs, getInnerSources()); - List enabled = new LinkedList(); - for (Operator op : getOperators()) { - if (op.isEnabled()) { - enabled.add(op); - } - } - autoWire(level, enabled, readyOutputs, recursive, true); - } - - /** - * @param wireNew - * If true, OutputPorts of operators will be added to readyOutputs once they are - * wired. - */ - private void autoWire(CompatibilityLevel level, List operators, LinkedList readyOutputs, - boolean recursive, boolean wireNew) throws PortException { - transformMDNeighbourhood(); - - for (Operator op : operators) { - try { - readyOutputs = op.preAutoWire(readyOutputs); - } catch (OperatorException e) { - getEnclosingOperator().getLogger().log(Level.WARNING, "During auto-wiring: " + e, e); - } - autoWire(level, op.getInputPorts(), readyOutputs); - transformMDNeighbourhood(); - if (recursive) { - if (op instanceof OperatorChain) { - for (ExecutionUnit subprocess : ((OperatorChain) op).getSubprocesses()) { - // we have already removed all connections, so keepConnections=true in - // recursive call - subprocess.autoWire(level, true, recursive); - } - } - } - if (wireNew) { - addReadyOutputs(readyOutputs, op.getOutputPorts()); - } - } - autoWire(level, getInnerSinks(), readyOutputs); - transformMDNeighbourhood(); - } - - /** - * Automatically wires inputs and outputs of a single operator in this execution unit. - * - * @param inputs - * Wire inputs? - * @param outputs - * Wire outputs? - */ - public void autoWireSingle(Operator operator, CompatibilityLevel level, boolean inputs, boolean outputs) { - // auto wire inputs - if (inputs) { - transformMDNeighbourhood(); - // store all outputs. Scan them to find matching inputs. - LinkedList readyOutputs = new LinkedList(); - // add the ports, oldest first. Simulate pre-5.0-like stack by taking - // the last out of this list when consuming input. - addReadyOutputs(readyOutputs, getInnerSources()); - boolean found = false; - for (Operator other : operators) { - if (other == operator) { - found = true; - break; - } else { - addReadyOutputs(readyOutputs, other.getOutputPorts()); - } - } - if (!found) { - throw new IllegalArgumentException( - "Operator " + operator.getName() + " does not belong to this subprocess " + getName() + "."); - } - getEnclosingOperator().getLogger() - .fine("Wiring: " + operator + "." + operator.getInputPorts().getAllPorts() + " to " + readyOutputs); - autoWire(level, operator.getInputPorts(), readyOutputs); - } - - // auto wire outputs - if (outputs) { - LinkedList readyOutputs = new LinkedList(); - addReadyOutputs(readyOutputs, operator.getOutputPorts()); - List successors = new LinkedList(); - boolean foundMe = false; - for (Operator other : getOperators()) { - if (foundMe) { - successors.add(other); - } else if (other == operator) { - foundMe = true; - } - } - autoWire(level, successors, readyOutputs, false, false); - } - } - - private void addReadyOutputs(LinkedList readyOutputs, OutputPorts ports) { - // add the parameters in a stack-like fashion like in pre-5.0 - Iterator i = new LinkedList(ports.getAllPorts()).descendingIterator(); - while (i.hasNext()) { - OutputPort port = i.next(); - if (!port.isConnected() && port.shouldAutoConnect()) { - readyOutputs.addLast(port); - } - } - } - - /** - * Returns a list of all available output ports within this process, including inner sources and - * output ports of enclosed operators. - */ - public Collection getAllOutputPorts() { - Collection outputPorts = new LinkedList(); - outputPorts.addAll(getInnerSources().getAllPorts()); - for (Operator operator : operators) { - outputPorts.addAll(operator.getOutputPorts().getAllPorts()); - } - return outputPorts; - } - - public Operator getOperatorByName(String toOp) { - for (Operator op : operators) { - if (op.getName().equals(toOp)) { - return op; - } - } - return null; - } - - public int getNumberOfOperators() { - return operators.size(); - } - - /** - * Clones operators contained in original, adds them to this execution unit and - * wires them as they were originally. - * - * @param forParallelExecution - * Indicates whether this clone is supposed to be executed in parallel. If yes, the - * clone will not be registered with the parent process and will share its - * {@link Operator#applyCount} with the original. - */ - public void cloneExecutionUnitFrom(ExecutionUnit original, boolean forParallelExecution) { - // Clone operators - Map clonedOperatorsByName = new HashMap(); - for (Operator originalChild : original.operators) { - Operator clonedOperator = originalChild.cloneOperator(originalChild.getName(), forParallelExecution); - addOperator(clonedOperator, !forParallelExecution); - clonedOperatorsByName.put(originalChild.getName(), clonedOperator); - } - - // Restore connections - cloneConnections(original.getInnerSources(), original, clonedOperatorsByName); - for (Operator op : original.operators) { - cloneConnections(op.getOutputPorts(), original, clonedOperatorsByName); - } - - // Unlock - original.getInnerSources().unlockPortExtenders(); - original.getInnerSinks().unlockPortExtenders(); - for (Operator op : this.operators) { - op.getInputPorts().unlockPortExtenders(); - op.getOutputPorts().unlockPortExtenders(); - } - - // copy user data entries - if (original.userData != null) { - for (String key : original.userData.keySet()) { - UserData data = original.userData.get(key); - if (data != null) { - setUserData(key, data.copyUserData(this)); - } - } - } - - // Other: - this.expanded = original.expanded; - } - - private void cloneConnections(OutputPorts originalPorts, ExecutionUnit originalExecutionUnit, - Map clonedOperatorsByName) { - for (OutputPort originalSource : originalPorts.getAllPorts()) { - if (originalSource.isConnected()) { - - OutputPort mySource; - if (originalPorts.getOwner().getOperator() == originalExecutionUnit.getEnclosingOperator()) { - // this is an inner source - mySource = getInnerSources().getPortByName(originalSource.getName()); - if (mySource == null) { - throw new RuntimeException("Error during clone: Corresponding source for " + originalSource - + " not found (no such inner source)."); - } - } else { - // this is an output port - Operator myOperator = clonedOperatorsByName - .get(originalSource.getPorts().getOwner().getOperator().getName()); - if (myOperator == null) { - throw new RuntimeException("Error during clone: Corresponding source for " + originalSource - + " not found (no such operator)."); - } - mySource = myOperator.getOutputPorts().getPortByName(originalSource.getName()); - if (mySource == null) { - throw new RuntimeException("Error during clone: Corresponding source for " + originalSource - + " not found (no such output port)."); - } - } - - InputPort originalDestination = originalSource.getDestination(); - InputPort myDestination; - if (originalDestination.getPorts().getOwner().getOperator() == originalExecutionUnit - .getEnclosingOperator()) { - // this is an inner sink - myDestination = getInnerSinks().getPortByName(originalDestination.getName()); - if (myDestination == null) { - throw new RuntimeException("Error during clone: Corresponding destination for " + originalDestination - + " not found (no such inner sink)."); - } - } else { - // this is an input port - Operator myOperator = clonedOperatorsByName - .get(originalDestination.getPorts().getOwner().getOperator().getName()); - if (myOperator == null) { - throw new RuntimeException("Error during clone: Corresponding destination for " + originalDestination - + " not found (no such operator)."); - } - myDestination = myOperator.getInputPorts().getPortByName(originalDestination.getName()); - if (myDestination == null) { - throw new RuntimeException("Error during clone: Corresponding destination for " + originalDestination - + " not found (no such input port)."); - } - } - mySource.connectTo(myDestination); - } - } - } - + public OperatorChain getEnclosingOperator() { + return enclosingOperator; + } + + private void unwire(boolean recursive) { + getInnerSources().disconnectAll(); + for (Operator op : getOperators()) { + unwire(op, recursive); + } + } + + private void unwire(Operator op, boolean recursive) throws PortException { + op.getOutputPorts().disconnectAll(); + if (recursive && op instanceof OperatorChain) { + for (ExecutionUnit subprocess : ((OperatorChain) op).getSubprocesses()) { + subprocess.unwire(recursive); + } + } + } + + @SuppressWarnings("deprecation") + private void autoWire(CompatibilityLevel level, InputPorts inputPorts, LinkedList readyOutputs) + throws PortException { + boolean success = false; + do { + Set complete = new HashSet<>(); + for (InputPort in : inputPorts.getAllPorts()) { + success = false; + if (!in.isConnected() && !complete.contains(in) + && in.getPorts().getOwner().getOperator().shouldAutoConnect(in)) { + Iterator outIterator; + // TODO: Simon: Does the same in both cases. Check again. + if (in.simulatesStack()) { + outIterator = readyOutputs.descendingIterator(); + } else { + outIterator = readyOutputs.descendingIterator(); + } + while (outIterator.hasNext()) { + OutputPort outCandidate = outIterator.next(); + // TODO: Remove shouldAutoConnect() in later versions + Operator owner = outCandidate.getPorts().getOwner().getOperator(); + if (owner.shouldAutoConnect(outCandidate) && outCandidate.getMetaData() != null && + in.isInputCompatible(outCandidate.getMetaData(), level)) { + readyOutputs.remove(outCandidate); + outCandidate.connectTo(in); + // we cannot continue with the remaining input ports + // since connecting may have triggered the creation of new input + // ports + // which would result in undefined behavior and a + // ConcurrentModificationException + success = true; + break; + } + } + // no port found. + complete.add(in); + if (success) { + break; + } + } + } + } while (success); + } + + /** + * Transforms the meta data of the enclosing operator. Required in {@link #autoWire()} after + * each Operator that has been wired. + */ + private void transformMDNeighbourhood() { + getEnclosingOperator().transformMetaData(); + } + + /** + * Connects the ports automatically in a first-fit approach. Operators are connected in their + * ordering within the {@link #operators} list. Every input of every operator is connected to + * the first compatible output of an operator "left" of this operator. This corresponds to the + * way, IOObjects were consumed in the pre-5.0 version. Disabled operators are skipped. + * + * @param level + * If level is {@link CompatibilityLevel#VERSION_5}, an input is considered + * compatible only if it satisfies all meta data constraints. For + * {@link CompatibilityLevel#PRE_VERSION_5} we only consider the classes. + * @param keepConnections + * if true, don't unwire old connections before rewiring. + */ + public void autoWire(CompatibilityLevel level, boolean keepConnections, boolean recursive) throws PortException { + if (!keepConnections) { + unwire(recursive); + } + // store all outputs. Scan them to find matching inputs. + LinkedList readyOutputs = new LinkedList<>(); + addReadyOutputs(readyOutputs, getInnerSources()); + List enabled = new LinkedList<>(); + for (Operator op : getOperators()) { + if (op.isEnabled()) { + enabled.add(op); + } + } + autoWire(level, enabled, readyOutputs, recursive, true); + } + + /** + * @param wireNew + * If true, OutputPorts of operators will be added to readyOutputs once they are + * wired. + */ + private void autoWire(CompatibilityLevel level, List operators, LinkedList readyOutputs, + boolean recursive, boolean wireNew) throws PortException { + transformMDNeighbourhood(); + + for (Operator op : operators) { + try { + readyOutputs = op.preAutoWire(readyOutputs); + } catch (OperatorException e) { + getEnclosingOperator().getLogger().log(Level.WARNING, "During auto-wiring: " + e, e); + } + autoWire(level, op.getInputPorts(), readyOutputs); + transformMDNeighbourhood(); + if (recursive && op instanceof OperatorChain) { + for (ExecutionUnit subprocess : ((OperatorChain) op).getSubprocesses()) { + // we have already removed all connections, so keepConnections=true in + // recursive call + subprocess.autoWire(level, true, recursive); + } + } + if (wireNew) { + addReadyOutputs(readyOutputs, op.getOutputPorts()); + } + } + autoWire(level, getInnerSinks(), readyOutputs); + transformMDNeighbourhood(); + } + + /** + * Automatically wires inputs and outputs of a single operator in this execution unit. + * + * @param inputs + * Wire inputs? + * @param outputs + * Wire outputs? + */ + public void autoWireSingle(Operator operator, CompatibilityLevel level, boolean inputs, boolean outputs) { + // auto wire inputs + if (inputs) { + transformMDNeighbourhood(); + // store all outputs. Scan them to find matching inputs. + LinkedList readyOutputs = new LinkedList<>(); + // add the ports, oldest first. Simulate pre-5.0-like stack by taking + // the last out of this list when consuming input. + addReadyOutputs(readyOutputs, getInnerSources()); + boolean found = false; + for (Operator other : operators) { + if (other == operator) { + found = true; + break; + } else { + addReadyOutputs(readyOutputs, other.getOutputPorts()); + } + } + if (!found) { + throw new IllegalArgumentException( + "Operator " + operator.getName() + " does not belong to this subprocess " + getName() + "."); + } + getEnclosingOperator().getLogger() + .fine("Wiring: " + operator + "." + operator.getInputPorts().getAllPorts() + " to " + readyOutputs); + autoWire(level, operator.getInputPorts(), readyOutputs); + } + + // auto wire outputs + if (outputs) { + LinkedList readyOutputs = new LinkedList<>(); + addReadyOutputs(readyOutputs, operator.getOutputPorts()); + List successors = new LinkedList<>(); + boolean foundMe = false; + for (Operator other : getOperators()) { + if (foundMe) { + successors.add(other); + } else if (other == operator) { + foundMe = true; + } + } + autoWire(level, successors, readyOutputs, false, false); + } + } + + private void addReadyOutputs(LinkedList readyOutputs, OutputPorts ports) { + // add the parameters in a stack-like fashion like in pre-5.0 + Iterator i = new LinkedList(ports.getAllPorts()).descendingIterator(); + while (i.hasNext()) { + OutputPort port = i.next(); + if (!port.isConnected() && port.shouldAutoConnect()) { + readyOutputs.addLast(port); + } + } + } + + /** + * Returns a list of all available output ports within this process, including inner sources and + * output ports of enclosed operators. + */ + public Collection getAllOutputPorts() { + Collection outputPorts = new LinkedList<>(); + outputPorts.addAll(getInnerSources().getAllPorts()); + for (Operator operator : operators) { + outputPorts.addAll(operator.getOutputPorts().getAllPorts()); + } + return outputPorts; + } + + public Operator getOperatorByName(String toOp) { + for (Operator op : operators) { + if (op.getName().equals(toOp)) { + return op; + } + } + return null; + } + + public int getNumberOfOperators() { + return operators.size(); + } + + /** + * Clones operators contained in original, adds them to this execution unit and + * wires them as they were originally. + * + * @param forParallelExecution + * Indicates whether this clone is supposed to be executed in parallel. If yes, the + * clone will not be registered with the parent process and will share its + * {@link Operator#applyCount} with the original. + */ + public void cloneExecutionUnitFrom(ExecutionUnit original, boolean forParallelExecution) { + // Clone operators + Map clonedOperatorsByName = new HashMap<>(); + for (Operator originalChild : original.operators) { + Operator clonedOperator = originalChild.cloneOperator(originalChild.getName(), forParallelExecution); + addOperator(clonedOperator, !forParallelExecution); + clonedOperatorsByName.put(originalChild.getName(), clonedOperator); + } + + // Restore connections + cloneConnections(original.getInnerSources(), original, clonedOperatorsByName); + for (Operator op : original.operators) { + cloneConnections(op.getOutputPorts(), original, clonedOperatorsByName); + } + + // Unlock + original.getInnerSources().unlockPortExtenders(); + original.getInnerSinks().unlockPortExtenders(); + for (Operator op : this.operators) { + op.getInputPorts().unlockPortExtenders(); + op.getOutputPorts().unlockPortExtenders(); + } + + // copy user data entries + if (original.userData != null) { + for (String key : original.userData.keySet()) { + UserData data = original.userData.get(key); + if (data != null) { + setUserData(key, data.copyUserData(this)); + } + } + } + + // Other: + this.expanded = original.expanded; + } + + private void cloneConnections(OutputPorts originalPorts, ExecutionUnit originalExecutionUnit, + Map clonedOperatorsByName) { + for (OutputPort originalSource : originalPorts.getAllPorts()) { + if (originalSource.isConnected()) { + + OutputPort mySource; + if (originalPorts.getOwner().getOperator() == originalExecutionUnit.getEnclosingOperator()) { + // this is an inner source + mySource = getInnerSources().getPortByName(originalSource.getName()); + if (mySource == null) { + throw new RuntimeException("Error during clone: Corresponding source for " + originalSource + + " not found (no such inner source)."); + } + } else { + // this is an output port + Operator myOperator = clonedOperatorsByName + .get(originalSource.getPorts().getOwner().getOperator().getName()); + if (myOperator == null) { + throw new RuntimeException("Error during clone: Corresponding source for " + originalSource + + " not found (no such operator)."); + } + mySource = myOperator.getOutputPorts().getPortByName(originalSource.getName()); + if (mySource == null) { + throw new RuntimeException("Error during clone: Corresponding source for " + originalSource + + " not found (no such output port)."); + } + } + + InputPort originalDestination = originalSource.getDestination(); + InputPort myDestination; + if (originalDestination.getPorts().getOwner().getOperator() == originalExecutionUnit + .getEnclosingOperator()) { + // this is an inner sink + myDestination = getInnerSinks().getPortByName(originalDestination.getName()); + if (myDestination == null) { + throw new RuntimeException("Error during clone: Corresponding destination for " + originalDestination + + " not found (no such inner sink)."); + } + } else { + // this is an input port + Operator myOperator = clonedOperatorsByName + .get(originalDestination.getPorts().getOwner().getOperator().getName()); + if (myOperator == null) { + throw new RuntimeException("Error during clone: Corresponding destination for " + originalDestination + + " not found (no such operator)."); + } + myDestination = myOperator.getInputPorts().getPortByName(originalDestination.getName()); + if (myDestination == null) { + throw new RuntimeException("Error during clone: Corresponding destination for " + originalDestination + + " not found (no such input port)."); + } + } + mySource.connectTo(myDestination); + } + } + } + /** Returns all nested operators. */ - public Collection getChildOperators() { - List children = new LinkedList(); - for (Operator operator : operators) { - children.add(operator); - } - return children; - } - + public Collection getChildOperators() { + List children = new LinkedList<>(); + for (Operator operator : operators) { + children.add(operator); + } + return children; + } + /** Recursively returns all nested operators. */ - public List getAllInnerOperators() { - List children = new LinkedList(); - for (Operator operator : operators) { - children.add(operator); - if (operator instanceof OperatorChain) { - children.addAll(((OperatorChain) operator).getAllInnerOperators()); - } - } - return children; - } - - protected List createProcessTreeList(int indent, String selfPrefix, String childPrefix, Operator markOperator, - String mark) { - List treeList = new LinkedList<>(); - treeList.add(Tools.indent(indent) + " subprocess '" + getName() + "'"); - Iterator i = operators.iterator(); - while (i.hasNext()) { - treeList.addAll(i.next().createProcessTreeList(indent, childPrefix + "+- ", - childPrefix + (i.hasNext() ? "| " : " "), markOperator, mark)); - } - return treeList; - } - + public List getAllInnerOperators() { + List children = new LinkedList<>(); + for (Operator operator : operators) { + children.add(operator); + if (operator instanceof OperatorChain) { + children.addAll(((OperatorChain) operator).getAllInnerOperators()); + } + } + return children; + } + + protected List createProcessTreeList(int indent, String selfPrefix, String childPrefix, + Operator markOperator, String mark) { + List treeList = new LinkedList<>(); + treeList.add(Tools.indent(indent) + " subprocess '" + getName() + "'"); + Iterator i = operators.iterator(); + while (i.hasNext()) { + treeList.addAll(i.next().createProcessTreeList(indent, childPrefix + "+- ", + childPrefix + (i.hasNext() ? "| " : " "), markOperator, mark)); + } + return treeList; + } + /** Executes the inner operators. */ - public void execute() throws OperatorException { - UnitExecutor executor = UnitExecutionFactory.getInstance().getExecutor(this); - // check only the callstack of nested operators, otherwise execution units of - // unsigned extensions might not be able to execute trusted operators - try { - AccessController.doPrivileged(new PrivilegedExceptionAction() { - - @Override - public Void run() throws OperatorException { - - executor.execute(ExecutionUnit.this); - return null; - } - }); - } catch (PrivilegedActionException e) { - // e.getException() can either be an instance of OperatorException, - // or an unsafe Exception, - // as only checked exceptions will be wrapped in a - // PrivilegedActionException. - if (e.getException() instanceof OperatorException) { - throw (OperatorException) e.getException(); - } else { - // Wrap unsafe Exceptions - throw new OperatorException(e.getException().getMessage(), e.getException()); - } - } - } - + public void execute() throws OperatorException { + UnitExecutor executor = UnitExecutionFactory.getInstance().getExecutor(this); + // check only the callstack of nested operators, otherwise execution units of + // unsigned extensions might not be able to execute trusted operators + try { + AccessController.doPrivileged(new PrivilegedExceptionAction() { + + @Override + public Void run() throws OperatorException { + + executor.execute(ExecutionUnit.this); + return null; + } + }); + } catch (PrivilegedActionException e) { + // e.getException() can either be an instance of OperatorException, + // or an unsafe Exception, + // as only checked exceptions will be wrapped in a + // PrivilegedActionException. + if (e.getException() instanceof OperatorException) { + throw (OperatorException) e.getException(); + } else { + // Wrap unsafe Exceptions + throw new OperatorException(e.getException().getMessage(), e.getException()); + } + } + } + /** Frees memory used by inner sinks. */ - public void freeMemory() { - getInnerSources().freeMemory(); - getInnerSinks().freeMemory(); - } - - private boolean expanded = true; - + public void freeMemory() { + getInnerSources().freeMemory(); + getInnerSinks().freeMemory(); + } + + private boolean expanded = true; + /** Sets the expansion mode which indicates if this operator is drawn expanded or not. */ - public void setExpanded(boolean expanded) { - this.expanded = expanded; - } - + public void setExpanded(boolean expanded) { + this.expanded = expanded; + } + /** Returns true if this operator should be painted expanded. */ - public boolean isExpanded() { - return expanded; - } - - public void processStarts() throws OperatorException { - for (Operator operator : operators) { - operator.processStarts(); - } - updateExecutionOrder(); - } - - public void processFinished() throws OperatorException { - for (Operator operator : operators) { - operator.processFinished(); - } - } - - /** - * Moves the operators from this process to another process, keeping all connections intact. - * TODO: Test more rigorously. Do we register/unregister everything correctly? - * - * @return the number of ports the connections of which could not be restored - */ - public int stealOperatorsFrom(ExecutionUnit otherUnit) { - int failedReconnects = 0; - - // remember source and sink connections so we can reconnect them later. - Map sourceMap = new HashMap(); - Map sinkMap = new HashMap(); - for (OutputPort source : otherUnit.getInnerSources().getAllPorts()) { - if (source.isConnected()) { - sourceMap.put(source.getName(), source.getDestination()); - } - } - otherUnit.getInnerSources().disconnectAll(); - for (InputPort sink : otherUnit.getInnerSinks().getAllPorts()) { - if (sink.isConnected()) { - sinkMap.put(sink.getName(), sink.getSource()); - } - } - otherUnit.getInnerSinks().disconnectAll(); - - // Move operators - Iterator i = otherUnit.operators.iterator(); - while (i.hasNext()) { - Operator operator = i.next(); - i.remove(); - otherUnit.unregister(operator); - Process otherProcess = operator.getProcess(); - if (otherProcess != null) { - operator.unregisterOperator(otherProcess); - } - this.operators.add(operator); - operator.setEnclosingProcess(null); - registerOperator(operator, true); - } - - // Rewire sources and sinks - for (Map.Entry entry : sourceMap.entrySet()) { - OutputPort mySource = getInnerSources().getPortByName(entry.getKey()); - if (mySource != null) { - mySource.connectTo(entry.getValue()); - } else { - failedReconnects++; - } - } - getInnerSources().unlockPortExtenders(); - - for (Map.Entry entry : sinkMap.entrySet()) { - InputPort mySink = getInnerSinks().getPortByName(entry.getKey()); - if (mySink != null) { - entry.getValue().connectTo(mySink); - } else { - failedReconnects++; - } - } - getInnerSinks().unlockPortExtenders(); - - fireUpdate(this); - return failedReconnects; - } - - /** - * Moves an operator to the given index. (If the old index is smaller than the new one, the new - * one will automatically be reduced by one.) - */ - public void moveToIndex(Operator op, int newIndex) { - int oldIndex = operators.indexOf(op); - Process process = getEnclosingOperator().getProcess(); - if (oldIndex != -1) { - operators.remove(op); - if (process != null) { - int oldIndexAmongEnabled = getEnabledOperators().indexOf(op); - process.fireOperatorRemoved(op, oldIndex, oldIndexAmongEnabled); - } - if (oldIndex < newIndex) { - newIndex--; - } - operators.add(newIndex, op); - if (process != null) { - process.fireOperatorAdded(op); - } - fireUpdate(); - updateExecutionOrder(); - } - } - - /** - * Re-arranges the execution order such that the specified operators immediately follow - * insertAfter. - */ - public void bringToFront(Collection movedOperators, Operator insertAfter) { - this.operators.removeAll(movedOperators); - int index = this.operators.indexOf(insertAfter) + 1; - for (Operator op : movedOperators) { - this.operators.add(index++, op); - } - updateExecutionOrder(); - fireUpdate(); - } -} + public boolean isExpanded() { + return expanded; + } + + public void processStarts() throws OperatorException { + for (Operator operator : operators) { + operator.processStarts(); + } + updateExecutionOrder(); + } + + public void processFinished() throws OperatorException { + for (Operator operator : operators) { + operator.processFinished(); + } + } + + /** + * Moves the operators from this process to another process, keeping all connections intact. + * TODO: Test more rigorously. Do we register/unregister everything correctly? + * + * @return the number of ports the connections of which could not be restored + */ + public int stealOperatorsFrom(ExecutionUnit otherUnit) { + int failedReconnects = 0; + + // remember source and sink connections so we can reconnect them later. + Map sourceMap = new HashMap<>(); + Map sinkMap = new HashMap<>(); + for (OutputPort source : otherUnit.getInnerSources().getAllPorts()) { + if (source.isConnected()) { + sourceMap.put(source.getName(), source.getDestination()); + } + } + otherUnit.getInnerSources().disconnectAll(); + for (InputPort sink : otherUnit.getInnerSinks().getAllPorts()) { + if (sink.isConnected()) { + sinkMap.put(sink.getName(), sink.getSource()); + } + } + otherUnit.getInnerSinks().disconnectAll(); + + // Move operators + Iterator i = otherUnit.operators.iterator(); + while (i.hasNext()) { + Operator operator = i.next(); + i.remove(); + otherUnit.unregister(operator); + Process otherProcess = operator.getProcess(); + if (otherProcess != null) { + operator.unregisterOperator(otherProcess); + } + this.operators.add(operator); + operator.setEnclosingProcess(null); + registerOperator(operator, true); + } + + // Rewire sources and sinks + for (Map.Entry entry : sourceMap.entrySet()) { + OutputPort mySource = getInnerSources().getPortByName(entry.getKey()); + if (mySource != null) { + mySource.connectTo(entry.getValue()); + } else { + failedReconnects++; + } + } + getInnerSources().unlockPortExtenders(); + + for (Map.Entry entry : sinkMap.entrySet()) { + InputPort mySink = getInnerSinks().getPortByName(entry.getKey()); + if (mySink != null) { + entry.getValue().connectTo(mySink); + } else { + failedReconnects++; + } + } + getInnerSinks().unlockPortExtenders(); + + fireUpdate(this); + return failedReconnects; + } + + /** + * Moves an operator to the given index. (If the old index is smaller than the new one, the new + * one will automatically be reduced by one.) + */ + public void moveToIndex(Operator op, int newIndex) { + int oldIndex = operators.indexOf(op); + Process process = getEnclosingOperator().getProcess(); + if (oldIndex != -1) { + operators.remove(op); + if (process != null) { + int oldIndexAmongEnabled = getEnabledOperators().indexOf(op); + process.fireOperatorRemoved(op, oldIndex, oldIndexAmongEnabled); + } + if (oldIndex < newIndex) { + newIndex--; + } + operators.add(newIndex, op); + if (process != null) { + process.fireOperatorAdded(op); + } + fireUpdate(); + updateExecutionOrder(); + } + } + + /** + * Re-arranges the execution order such that the specified operators immediately follow + * insertAfter. + */ + public void bringToFront(Collection movedOperators, Operator insertAfter) { + this.operators.removeAll(movedOperators); + int index = this.operators.indexOf(insertAfter) + 1; + for (Operator op : movedOperators) { + this.operators.add(index++, op); + } + updateExecutionOrder(); + fireUpdate(); + } +} diff --git a/src/main/java/com/rapidminer/operator/FileEchoOperator.java b/src/main/java/com/rapidminer/operator/FileEchoOperator.java index a2aeac786..93e9980cd 100644 --- a/src/main/java/com/rapidminer/operator/FileEchoOperator.java +++ b/src/main/java/com/rapidminer/operator/FileEchoOperator.java @@ -96,6 +96,7 @@ public List getParameterTypes() { type = new ParameterTypeText(PARAMETER_TEXT, "The text which should be written into the file.", TextType.PLAIN, false); type.setExpert(false); + type.setPrimary(true); types.add(type); type = new ParameterTypeCategory(PARAMETER_MODE, "The text insertion mode, replace any existing file content, or append the text to the end of the file.", diff --git a/src/main/java/com/rapidminer/operator/InvalidRepositoryEntryError.java b/src/main/java/com/rapidminer/operator/InvalidRepositoryEntryError.java new file mode 100644 index 000000000..7cae0f91b --- /dev/null +++ b/src/main/java/com/rapidminer/operator/InvalidRepositoryEntryError.java @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.operator; + +import java.util.Collections; +import java.util.List; + +import com.rapidminer.operator.ports.PortOwner; +import com.rapidminer.operator.ports.quickfix.QuickFix; + + +/** + * Error that is used when a repository entry is either of the wrong type or does not exist. + * + * @author Marco Boeck + * @since 8.2 + */ +public class InvalidRepositoryEntryError extends SimpleProcessSetupError { + + private final String parameterKey; + + + public InvalidRepositoryEntryError(Severity severity, PortOwner owner, String parameterKey, String i18nKey, Object... i18nArgs) { + super(severity, owner, Collections.emptyList(), false, i18nKey, i18nArgs); + + if (parameterKey == null) { + throw new IllegalArgumentException("parameterKey must not be null!"); + } + this.parameterKey = parameterKey; + } + + public InvalidRepositoryEntryError(Severity severity, PortOwner owner, String parameterKey, List fixes, String i18nKey, + Object... i18nArgs) { + super(severity, owner, fixes, false, i18nKey, i18nArgs); + + if (parameterKey == null) { + throw new IllegalArgumentException("parameterKey must not be null!"); + } + this.parameterKey = parameterKey; + } + + public InvalidRepositoryEntryError(Severity severity, PortOwner portOwner, String parameterKey, List fixes, + boolean absoluteKey, String i18nKey, Object... i18nArgs) { + super(severity, portOwner, fixes, absoluteKey, i18nKey, i18nArgs); + + if (parameterKey == null) { + throw new IllegalArgumentException("parameterKey must not be null!"); + } + this.parameterKey = parameterKey; + } + + /** + * Returns the parameter key that caused this error. + * + * @return the parameter key that caused this, never {@code null} + */ + public String getParameterKey() { + return parameterKey; + } +} diff --git a/src/main/java/com/rapidminer/operator/MacroConstructionOperator.java b/src/main/java/com/rapidminer/operator/MacroConstructionOperator.java index 72c027508..f4477f2f1 100644 --- a/src/main/java/com/rapidminer/operator/MacroConstructionOperator.java +++ b/src/main/java/com/rapidminer/operator/MacroConstructionOperator.java @@ -262,6 +262,7 @@ public List getParameterTypes() { "functions_expressions", "The expressions which define the new macros.", getInputPorts() .getPortByIndex(0), false)); type.setExpert(false); + type.setPrimary(true); types.add(type); return types; diff --git a/src/main/java/com/rapidminer/operator/MacroDefinitionOperator.java b/src/main/java/com/rapidminer/operator/MacroDefinitionOperator.java index 7c55090cf..c7111b7aa 100644 --- a/src/main/java/com/rapidminer/operator/MacroDefinitionOperator.java +++ b/src/main/java/com/rapidminer/operator/MacroDefinitionOperator.java @@ -18,6 +18,9 @@ */ package com.rapidminer.operator; +import java.util.Iterator; +import java.util.List; + import com.rapidminer.operator.meta.FeatureIterator; import com.rapidminer.operator.ports.DummyPortPairExtender; import com.rapidminer.operator.ports.PortPairExtender; @@ -25,9 +28,6 @@ import com.rapidminer.parameter.ParameterTypeList; import com.rapidminer.parameter.ParameterTypeString; -import java.util.Iterator; -import java.util.List; - /** *

@@ -98,9 +98,11 @@ public void doWork() throws OperatorException { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeList(PARAMETER_MACROS, "The list of macros defined by the user.", + ParameterType type = new ParameterTypeList(PARAMETER_MACROS, "The list of macros defined by the user.", new ParameterTypeString("macro_name", "The macro name."), new ParameterTypeString(PARAMETER_VALUES, - "The value of this macro.", false), false)); + "The value of this macro.", false), false); + type.setPrimary(true); + types.add(type); return types; } } diff --git a/src/main/java/com/rapidminer/operator/Operator.java b/src/main/java/com/rapidminer/operator/Operator.java index dff03c0a7..3a05dd5e8 100644 --- a/src/main/java/com/rapidminer/operator/Operator.java +++ b/src/main/java/com/rapidminer/operator/Operator.java @@ -32,6 +32,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicInteger; @@ -56,6 +57,7 @@ import com.rapidminer.operator.ProcessSetupError.Severity; import com.rapidminer.operator.annotation.ResourceConsumer; import com.rapidminer.operator.annotation.ResourceConsumptionEstimator; +import com.rapidminer.operator.nio.model.AbstractDataResultSetReader; import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.InputPorts; import com.rapidminer.operator.ports.OutputPort; @@ -73,12 +75,14 @@ import com.rapidminer.operator.ports.quickfix.ParameterSettingQuickFix; import com.rapidminer.operator.ports.quickfix.QuickFix; import com.rapidminer.operator.ports.quickfix.RelativizeRepositoryLocationQuickfix; +import com.rapidminer.operator.preprocessing.filter.AbstractDateDataProcessing; import com.rapidminer.parameter.ParameterHandler; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeAttribute; import com.rapidminer.parameter.ParameterTypeBoolean; import com.rapidminer.parameter.ParameterTypeCategory; import com.rapidminer.parameter.ParameterTypeDate; +import com.rapidminer.parameter.ParameterTypeDateFormat; import com.rapidminer.parameter.ParameterTypeInnerOperator; import com.rapidminer.parameter.ParameterTypeList; import com.rapidminer.parameter.ParameterTypeRepositoryLocation; @@ -900,6 +904,23 @@ Collections. emptyList(), "accessing_repository_by_name", } } + } else if(type instanceof ParameterTypeDateFormat) { + Locale locale = Locale.getDefault(); + try { + int localeIndex; + localeIndex = getParameterAsInt(AbstractDataResultSetReader.PARAMETER_LOCALE); + if (localeIndex >= 0 && localeIndex < AbstractDateDataProcessing.availableLocales.size()) { + locale = AbstractDateDataProcessing.availableLocales.get(localeIndex); + } + } catch (UndefinedParameterError e) { + // ignore and use default locale + } + try { + ParameterTypeDateFormat.createCheckedDateFormat(this, locale, true); + } catch (UserError userError) { + // will not happen because of isSetup + } + } else if (!optional && type instanceof ParameterTypeDate) { String value = getParameters().getParameterOrNull(type.getKey()); if (value != null && !ParameterTypeDate.isValidDate(value)) { @@ -1007,10 +1028,22 @@ public final void execute() throws OperatorException { // Convert unchecked exception to checked exception (unchecked exception might be // thrown from places where no checked exceptions are possible, e.g. thread pools). throw new ProcessStoppedException(this); + } catch (RuntimeException e) { + if (!(e instanceof OperatorRuntimeException)) { + throw e; + } + OperatorException operatorException = ((OperatorRuntimeException) e).toOperatorException(); + if (!(operatorException instanceof UserError)) { + throw operatorException; + } + UserError userError = (UserError) operatorException; + if (userError.getOperator() == null) { + userError.setOperator(this); + } + throw userError; } catch (UserError e) { // TODO: ensuring that operator is removed if it abnormally terminates but is not - // removed if - // child operator terminates abnormally + // removed if child operator terminates abnormally if (e.getOperator() == null) { e.setOperator(this); } @@ -1298,12 +1331,28 @@ public Parameters getParameters() { // if not loaded already: do now parameters = new Parameters(getParameterTypes()); parameters.addObserver(delegatingParameterObserver, false); - + makeDirtyOnUpdate(parameters); } return parameters; } + /** + * Returns the the first primary {@link ParameterType} of this operator that is not hidden + * if it exists. May return {@code null}. + * + * @return the primary parameter or {@code null} + * @since 8.2 + */ + public ParameterType getPrimaryParameter() { + for (ParameterType param : getParameters().getParameterTypes()) { + if (param.isPrimary() && !param.isHidden()) { + return param; + } + } + return null; + } + @Override public ParameterHandler getParameterHandler() { return this; diff --git a/src/main/java/com/rapidminer/operator/OperatorRuntimeException.java b/src/main/java/com/rapidminer/operator/OperatorRuntimeException.java new file mode 100644 index 000000000..a8dff321a --- /dev/null +++ b/src/main/java/com/rapidminer/operator/OperatorRuntimeException.java @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ + +package com.rapidminer.operator; + +/** + * Marker interface for {@link RuntimeException RuntimeExceptions} that should be handled as + * {@link OperatorException OperatorExceptions}; see {@link #toOperatorException()} for details. + * + * @author Jan Czogalla + * @since 8.2 + */ +public interface OperatorRuntimeException { + + /** + * Creates an {@link OperatorException} from this {@link RuntimeException}. Implementing classes can return more + * specific subclasses if necessary (e.g. {@link com.rapidminer.operator.UserError UserError}). + * + * @return an operator exception + */ + OperatorException toOperatorException(); +} diff --git a/src/main/java/com/rapidminer/operator/ScriptingOperator.java b/src/main/java/com/rapidminer/operator/ScriptingOperator.java index cd273dac0..87e5a1d60 100644 --- a/src/main/java/com/rapidminer/operator/ScriptingOperator.java +++ b/src/main/java/com/rapidminer/operator/ScriptingOperator.java @@ -302,6 +302,7 @@ public List getParameterTypes() { List types = super.getParameterTypes(); ParameterType type = new ParameterTypeText(PARAMETER_SCRIPT, "The script to execute.", TextType.GROOVY, false); type.setExpert(false); + type.setPrimary(true); type.setDefaultValue("/* \n" + " * You can use both Java and Groovy syntax in this script.\n" + " * \n * Note that you have access to the following two predefined variables:\n" + " * 1) input (an array of all input data)\n" diff --git a/src/main/java/com/rapidminer/operator/WrapperOperatorRuntimeException.java b/src/main/java/com/rapidminer/operator/WrapperOperatorRuntimeException.java new file mode 100644 index 000000000..6289edcfe --- /dev/null +++ b/src/main/java/com/rapidminer/operator/WrapperOperatorRuntimeException.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.operator; + +import java.util.Objects; + +/** + * Simple wrapper {@link RuntimeException} for {@link OperatorException OperatorExceptions}. + * Implements {@link OperatorRuntimeException} for improved error handling. + * The given {@link OperatorException} will be the cause with no parent stack trace and provides the error message. + * + * @author Jan Czogalla + * @since 8.2 + */ +public class WrapperOperatorRuntimeException extends RuntimeException implements OperatorRuntimeException { + + private final OperatorException operatorException; + + public WrapperOperatorRuntimeException(OperatorException operatorException) { + super(operatorException); + this.operatorException = Objects.requireNonNull(operatorException); + // ignore stack trace of this throwable, just use cause + this.setStackTrace(new StackTraceElement[0]); + } + + @Override + public String getMessage() { + return operatorException.getMessage(); + } + + @Override + public OperatorException toOperatorException() { + return operatorException; + } +} diff --git a/src/main/java/com/rapidminer/operator/clustering/ClusterToPrediction.java b/src/main/java/com/rapidminer/operator/clustering/ClusterToPrediction.java index 13b6f9ec9..d49d20628 100644 --- a/src/main/java/com/rapidminer/operator/clustering/ClusterToPrediction.java +++ b/src/main/java/com/rapidminer/operator/clustering/ClusterToPrediction.java @@ -18,10 +18,14 @@ */ package com.rapidminer.operator.clustering; +import java.util.HashMap; +import java.util.Vector; + import com.rapidminer.example.Attribute; import com.rapidminer.example.Attributes; import com.rapidminer.example.Example; import com.rapidminer.example.ExampleSet; +import com.rapidminer.example.Tools; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; @@ -36,9 +40,8 @@ import com.rapidminer.operator.ports.metadata.ExampleSetPrecondition; import com.rapidminer.operator.ports.metadata.MetaData; import com.rapidminer.operator.ports.metadata.SimpleMetaDataError; - -import java.util.HashMap; -import java.util.Vector; +import com.rapidminer.operator.ports.quickfix.AttributeToNominalQuickFixProvider; +import com.rapidminer.tools.Ontology; /** @@ -75,10 +78,14 @@ public MetaData modifyMetaData(MetaData metaData) { "special_unknown", "label")); return emd; case YES: - AttributeMetaData predictionMD = AttributeMetaData.createPredictionMetaData(emd - .getLabelMetaData()); + AttributeMetaData label = emd.getLabelMetaData(); + AttributeMetaData predictionMD = AttributeMetaData.createPredictionMetaData(label); emd.addAttribute(predictionMD); AttributeMetaData.createConfidenceAttributeMetaData(emd); + if (!label.isNominal()) { + exampleSetInput.addError( + new SimpleMetaDataError(Severity.ERROR, exampleSetInput, AttributeToNominalQuickFixProvider.labelToNominal(exampleSetInput, label), "special_attribute_has_wrong_type", label.getName(), Attributes.LABEL_NAME, Ontology.VALUE_TYPE_NAMES[Ontology.NOMINAL])); + } return emd; default: return emd; @@ -95,6 +102,7 @@ public MetaData modifyMetaData(MetaData metaData) { public void doWork() throws OperatorException { ExampleSet exampleSet = exampleSetInput.getData(ExampleSet.class); ClusterModel model = clusterModelInput.getData(ClusterModel.class); + Tools.hasNominalLabels(exampleSet); // generate the predicted attribute Attribute labelAttribute = exampleSet.getAttributes().getLabel(); @@ -297,6 +305,9 @@ public void doWork() throws OperatorException { String clusterNumber = example.getValueAsString(clusterAttribute).replaceAll("[^\\d]+", ""); try { int number = Integer.parseInt(clusterNumber); + if (number < 0 || number >= model.getNumberOfClusters()) { + throw new UserError(this, 145, clusterAttribute.getName()); + } predictionToCluster.put(example.getValueAsString(example.getAttributes().getPredictedLabel()), number); } catch (NumberFormatException e) { diff --git a/src/main/java/com/rapidminer/operator/clustering/clusterer/KMeans.java b/src/main/java/com/rapidminer/operator/clustering/clusterer/KMeans.java index 9fdfbb6e5..8f9f180bb 100644 --- a/src/main/java/com/rapidminer/operator/clustering/clusterer/KMeans.java +++ b/src/main/java/com/rapidminer/operator/clustering/clusterer/KMeans.java @@ -179,7 +179,7 @@ protected ClusterModel generateInternalClusterModel(ExampleSet exampleSet) throw distanceSum += distance * distance; i++; } - if (distanceSum < minimalIntraClusterDistance) { + if (distanceSum < minimalIntraClusterDistance || bestModel == null) { bestModel = model; minimalIntraClusterDistance = distanceSum; bestAssignments = centroidAssignments; diff --git a/src/main/java/com/rapidminer/operator/clustering/clusterer/SVClustering.java b/src/main/java/com/rapidminer/operator/clustering/clusterer/SVClustering.java index 3d24b23b2..a65cf972a 100644 --- a/src/main/java/com/rapidminer/operator/clustering/clusterer/SVClustering.java +++ b/src/main/java/com/rapidminer/operator/clustering/clusterer/SVClustering.java @@ -221,8 +221,13 @@ protected ClusterModel generateInternalClusterModel(ExampleSet exampleSet) throw } else { targetAttribute = exampleSet.getAttributes().getCluster(); } - NominalMapping mapping = targetAttribute.getMapping(); - mapping.setMapping("noise", mapping.getIndex("cluster_" + NOISE)); + try { + Tools.replaceValue(targetAttribute, "cluster_" + NOISE, "noise"); + } catch (RuntimeException e){ + // ignore, because there might be no noise cluster + // this will not interfere with attribute type checking, since RMAbstractClusterer#addClusterAttribute + // guarantees this attribute to be nominal. + } } return model; } diff --git a/src/main/java/com/rapidminer/operator/clustering/clusterer/soft/EMClusterer.java b/src/main/java/com/rapidminer/operator/clustering/clusterer/soft/EMClusterer.java index bb8a96cd8..066ce9441 100644 --- a/src/main/java/com/rapidminer/operator/clustering/clusterer/soft/EMClusterer.java +++ b/src/main/java/com/rapidminer/operator/clustering/clusterer/soft/EMClusterer.java @@ -136,8 +136,9 @@ protected Collection getAdditionalAttributes() { public ClusterModel createClusterModel(ExampleSet exampleSet) throws OperatorException { FlatFuzzyClusterModel bestModel = null; - int restoreMaxRuns = getParameterAsInt(PARAMETER_MAX_RUNS); - boolean restoreCorrelated = getParameterAsBoolean(PARAMETER_CORRELATED); + int initialMaxRuns = getParameterAsInt(PARAMETER_MAX_RUNS); + int maxRuns = initialMaxRuns; + boolean isCorrelated = getParameterAsBoolean(PARAMETER_CORRELATED); boolean addAsLabel = addsLabelAttribute(); boolean removeUnlabeled = getParameterAsBoolean(RMAbstractClusterer.PARAMETER_REMOVE_UNLABELED); @@ -147,10 +148,13 @@ public ClusterModel createClusterModel(ExampleSet exampleSet) throws OperatorExc // init operator progress int maxOptiRuns = getParameterAsInt(PARAMETER_MAX_OPTIMIZATION_STEPS); getProgress().setTotal(100); + int progressOffset = 0; int initSpecialSize = exampleSet.getAttributes().specialSize(); double[][] exampleInClusterProbability = new double[exampleSet.size()][k]; + boolean showProbs = getParameterAsBoolean(PARAMETER_SHOW_PROBABILITIES); + double max = Double.NEGATIVE_INFINITY; int exceptionCounter = 0; @@ -159,25 +163,23 @@ public ClusterModel createClusterModel(ExampleSet exampleSet) throws OperatorExc RandomGenerator randomGenerator = RandomGenerator.getGlobalRandomGenerator(); // the iterations - for (int iter = 0; iter < restoreMaxRuns; iter++) { + for (int iter = 0; iter < maxRuns; iter++) { FlatFuzzyClusterModel result = new FlatFuzzyClusterModel(exampleSet, k, addAsLabel, removeUnlabeled); FlatFuzzyClusterModel oldResult = null; // initialize the model try { - init(exampleSet, result, k, initSpecialSize, exampleInClusterProbability); + init(exampleSet, result, k, initSpecialSize, exampleInClusterProbability, showProbs, isCorrelated); } catch (OperatorCreationException e1) { e1.printStackTrace(); } boolean stableState = false; - double logLikelyHood_old = Double.POSITIVE_INFINITY; - double logLikelyHood = 0; + double logLikelihoodOld = Double.POSITIVE_INFINITY; + double logLikelihood = 0; // the optimization-steps - int optiStep = 0; int[] clusterAssignments = new int[exampleSet.size()]; try { - for (optiStep = 0; optiStep < maxOptiRuns && !stableState; optiStep++) { - getProgress().setCompleted( - (int) (100.0 * iter / restoreMaxRuns + 100.0 / restoreMaxRuns * optiStep / maxOptiRuns)); + for (int optiStep = 0; optiStep < maxOptiRuns && !stableState; optiStep++) { + getProgress().setCompleted(progressOffset + 100 * (iter * maxOptiRuns + optiStep) / maxRuns / maxOptiRuns); stableState = true; oldResult = result; result = new FlatFuzzyClusterModel(exampleSet, k, addAsLabel, removeUnlabeled); @@ -198,52 +200,57 @@ public ClusterModel createClusterModel(ExampleSet exampleSet) throws OperatorExc } result.setClusterAssignments(clusterAssignments, exampleSet); // Recalculate the values: cluster probabilities, means and standard deviations - maximization(exampleSet, k, exampleInClusterProbability, result); + maximization(exampleSet, k, exampleInClusterProbability, result, isCorrelated); // test if the quality of the soft-clustering performs the user-defined quality - logLikelyHood = computeLogLikelyhood(k, exampleInClusterProbability, result); - double difference = logLikelyHood_old - logLikelyHood; - if (!(Math.abs(difference) < quality)) { + logLikelihood = computeLogLikelihood(k, exampleInClusterProbability, result); + double difference = logLikelihoodOld - logLikelihood; + if (Math.abs(difference) >= quality) { stableState = false; } - logLikelyHood_old = logLikelyHood; + logLikelihoodOld = logLikelihood; } } catch (OperatorException e) { throw e; } catch (Exception e) { exceptionCounter++; // If there occurs an exception, don't stop at the first time and if there are some - // useable models don't discard them. - if (exceptionCounter > restoreMaxRuns) { + // usable models don't discard them. + if (exceptionCounter > initialMaxRuns) { // if there are not enough models, start again without the option correlated - if (iter - (exceptionCounter - 1) < Math.round(restoreMaxRuns * 0.49)) { - getLogger().info( - "Can't compute the inverse of the covariance matrix. Maybe the Matrix is singular. Changing option \"correlated_attributes\" to false."); - setParameter(PARAMETER_CORRELATED, "" + false); - setParameter(PARAMETER_MAX_RUNS, "" + restoreMaxRuns); - bestModel = (FlatFuzzyClusterModel) createClusterModel(exampleSet); + if (iter - (exceptionCounter - 1) < Math.round(maxRuns * 0.49) && isCorrelated) { + getLogger().info("Can't compute the inverse of the covariance matrix. Maybe the Matrix is singular. Changing option \"correlated_attributes\" to false."); + // reset maxRuns and iter, start again with no correlation + exceptionCounter = 0; + isCorrelated = false; + maxRuns = initialMaxRuns; + iter = -1; + getProgress().setTotal(200); + getProgress().setCompleted(100); + progressOffset = 100; + continue; } break; - } else { - setParameter(PARAMETER_MAX_RUNS, "" + (getParameterAsInt(PARAMETER_MAX_RUNS) + 1)); - continue; } + // allow one more run + maxRuns++; + continue; } // check if the model of the current iteration is better than the models computed before - if (Math.abs(logLikelyHood) > max) { - max = Math.abs(logLikelyHood); + if (Math.abs(logLikelihood) > max) { + max = Math.abs(logLikelihood); bestModel = result; bestAssignments = clusterAssignments; - if (showProbs() == true) { + if (showProbs) { setProbabilitiesInTable(exampleSet, exampleInClusterProbability); bestModel.setExampleInClusterProbability(exampleInClusterProbability); } } - getProgress().setCompleted((int) (100.0 * (iter + 1) / restoreMaxRuns)); + getProgress().setCompleted(progressOffset + (int) (100.0 * (iter + 1) / maxRuns)); } - // restore original values - setParameter(PARAMETER_MAX_RUNS, "" + restoreMaxRuns); - setParameter(PARAMETER_CORRELATED, "" + restoreCorrelated); + if (bestModel == null) { + throw new UserError(this, "no_cluster_model_calculated"); + } if (addsClusterAttribute()) { addClusterAssignments(exampleSet, bestAssignments); @@ -260,7 +267,7 @@ public ClusterModel createClusterModel(ExampleSet exampleSet) throws OperatorExc * Main init method. */ private void init(ExampleSet exampleSet, FlatFuzzyClusterModel result, int k, int initSpecialSize, - double[][] exampleInClusterProbability) throws OperatorException, OperatorCreationException { + double[][] exampleInClusterProbability, boolean showProbs, boolean correlated) throws OperatorException, OperatorCreationException { // init means, standard deviations (or covariance matrix) and cluster probabilities // according to specified distribution @@ -303,7 +310,7 @@ private void init(ExampleSet exampleSet, FlatFuzzyClusterModel result, int k, in } // compute means (normalized), stdDev...) computeValuesWithClusterMemberships(exampleSet, k, exampleInClusterProbability, result); - if (isCorrelated()) { + if (correlated) { initCovarianceMatrix(exampleSet, exampleInClusterProbability, result, k); } break; @@ -343,19 +350,19 @@ private void init(ExampleSet exampleSet, FlatFuzzyClusterModel result, int k, in } // compute means (normalized), stdDev...) computeValuesWithClusterMemberships(exampleSet, k, exampleInClusterProbability, result); - if (isCorrelated()) { + if (correlated) { initCovarianceMatrix(exampleSet, exampleInClusterProbability, result, k); } break; case AVERAGE_PARAMETERS: default: Random random = RandomGenerator.getRandomGenerator(this); - initAverageParameters(exampleSet, k, exampleInClusterProbability, result, random); + initAverageParameters(exampleSet, k, exampleInClusterProbability, result, random, correlated); break; } // show probabilities in example table? - if (showProbs()) { + if (showProbs) { if (exampleSet.getAttributes().specialSize() == initSpecialSize) { for (int i = 0; i < k; i++) { String name = "cluster_" + i + "_probability"; @@ -428,7 +435,7 @@ private void initCovarianceMatrix(ExampleSet exampleSet, double[][] exampleInClu * this values over the exampleSet. */ private void initAverageParameters(ExampleSet exampleSet, int k, double[][] exampleInClusterProbability, - FlatFuzzyClusterModel result, Random random) { + FlatFuzzyClusterModel result, Random random, boolean correlated) { // various initializations double[] max = new double[exampleSet.getAttributes().size()]; double[] min = new double[max.length]; @@ -457,7 +464,7 @@ private void initAverageParameters(ExampleSet exampleSet, int k, double[][] exam int j = 0; for (i = 0; i < k; i++) { - double[] clusterMean = new double[exampleSet.getAttributes().size()]; + double[] clusterMean; double clusterStDeviation; double clusterProbability; if (i < k / 2) { @@ -484,7 +491,7 @@ private void initAverageParameters(ExampleSet exampleSet, int k, double[][] exam result.setClusterProbability(i, clusterProbability); } - if (isCorrelated()) { + if (correlated) { initCovarianceMatrix(exampleSet, exampleInClusterProbability, result, k); } @@ -612,8 +619,7 @@ protected void expectationCorrelated(ExampleSet exampleSet, int k, double[][] ex * AND - cluster standard deviation [sigma_i] OR - cluster covariance matrix [Sigma_i] with the * probabilities of each example to each cluster [P(Cluster_i|example)] */ - protected void maximization(ExampleSet exampleSet, int k, double[][] exampleInClusterProbability, - FlatFuzzyClusterModel result) { + protected void maximization(ExampleSet exampleSet, int k, double[][] exampleInClusterProbability, FlatFuzzyClusterModel result, boolean correlated) { for (int i = 0; i < k; i++) { double probabilitySum = 0; int j = 0; @@ -627,7 +633,7 @@ protected void maximization(ExampleSet exampleSet, int k, double[][] exampleInCl result.setClusterMean(i, VectorMath.vectorDivision(clusterMean, probabilitySum)); result.setClusterProbability(i, probabilitySum / exampleSet.size()); - if (!isCorrelated()) { + if (!correlated) { j = 0; double clusterStDeviation = 0; for (Example example : exampleSet) { @@ -639,7 +645,7 @@ protected void maximization(ExampleSet exampleSet, int k, double[][] exampleInCl result.setClusterStandardDeviation(i, clusterStDeviation / probabilitySum); } } - if (isCorrelated()) { + if (correlated) { computeCovarianceMatrix(exampleSet, exampleInClusterProbability, result, k); } } @@ -677,9 +683,9 @@ private void computeCovarianceMatrix(ExampleSet exampleSet, double[][] exampleIn } /* - * Computes the loglikelyhood. + * Computes the loglikelihood. */ - protected double computeLogLikelyhood(int k, double[][] exampleInClusterProbability, FlatFuzzyClusterModel resultModel) { + protected double computeLogLikelihood(int k, double[][] exampleInClusterProbability, FlatFuzzyClusterModel resultModel) { double result = 0; double temp = 0; for (int n = 0; n < exampleInClusterProbability.length; n++) { @@ -691,32 +697,11 @@ protected double computeLogLikelyhood(int k, double[][] exampleInClusterProbabil return result; } - /* - * Show cluster probabilities in table? - */ - private boolean showProbs() { - if (getParameterAsBoolean(PARAMETER_SHOW_PROBABILITIES) == true) { - return true; - } - return false; - } - - /* - * Are there correlated attributes in the example set? - */ - private boolean isCorrelated() { - if (!getParameterAsBoolean(PARAMETER_CORRELATED)) { - return false; - } - return true; - } - /* * Sets the cluster probabilities in the table, according to the actual values in * exampleClusterProbs. */ - private void setProbabilitiesInTable(ExampleSet exampleSet, double[][] exampleInClusterProbability) - throws OperatorException { + private void setProbabilitiesInTable(ExampleSet exampleSet, double[][] exampleInClusterProbability) throws OperatorException { int k = getParameterAsInt(PARAMETER_K); for (int i = 0; i < k; i++) { String name = "cluster_" + i + "_probability"; @@ -756,9 +741,7 @@ protected ClusterModel generateInternalClusterModel(ExampleSet exampleSet) throw throw new UserError(this, 142, k); } - ClusterModel model = createClusterModel(exampleSet); - - return model; + return createClusterModel(exampleSet); } @@ -785,7 +768,7 @@ public List getParameterTypes() { "The maximal number of iterations performed for one run of this operator.", 1, Integer.MAX_VALUE, 100, false)); types.add(new ParameterTypeDouble(PARAMETER_QUALITY, - "The quality that must be fullfilled before the algorithm stops. (The rising of the loglikelyhood that must be undercut)", + "The quality that must be fullfilled before the algorithm stops. (The rising of the loglikelihood that must be undercut)", 1.0E-15, 1.0E-1, 1.0E-10)); types.addAll(RandomGenerator.getRandomGeneratorParameters(this)); diff --git a/src/main/java/com/rapidminer/operator/error/AttributeNotFoundError.java b/src/main/java/com/rapidminer/operator/error/AttributeNotFoundError.java index e05fc5dfb..43b49fd03 100644 --- a/src/main/java/com/rapidminer/operator/error/AttributeNotFoundError.java +++ b/src/main/java/com/rapidminer/operator/error/AttributeNotFoundError.java @@ -30,7 +30,7 @@ * @since 6.5.0 * */ -public class AttributeNotFoundError extends UserError { +public class AttributeNotFoundError extends ParameterError { /** if the attribute could neither be found in regular nor in special attributes */ public static final int ATTRIBUTE_NOT_FOUND = 160; @@ -40,8 +40,7 @@ public class AttributeNotFoundError extends UserError { private static final long serialVersionUID = 4107157631726397970L; - private String key; - private String attributeName; + private final String attributeName; /** * Throw if the parameter of an operator specifies an attribute which cannot be found in the @@ -72,20 +71,12 @@ public AttributeNotFoundError(Operator operator, String key, String attributeNam * the name of the attribute */ public AttributeNotFoundError(Operator operator, int code, String key, String attributeName) { - super(operator, code, attributeName); + super(operator, code, key, new Object[]{attributeName}); if (attributeName == null) { this.attributeName = ""; } else { this.attributeName = attributeName; } - this.key = key; - } - - /** - * @return the key of the parameter which caused the error. Can be {@code null} - */ - public String getKey() { - return key; } /** diff --git a/src/main/java/com/rapidminer/operator/error/ParameterError.java b/src/main/java/com/rapidminer/operator/error/ParameterError.java index 6f984a7be..87b319c59 100644 --- a/src/main/java/com/rapidminer/operator/error/ParameterError.java +++ b/src/main/java/com/rapidminer/operator/error/ParameterError.java @@ -55,8 +55,7 @@ public ParameterError(Operator operator, String code, String parameterkey, Objec } /** - * @return the key of the parameter which caused the error. Can be {@code null} in very rare - * cases + * @return the key of the parameter which caused the error. Can be {@code null} in very rare cases or subclasses */ public String getKey() { return key; diff --git a/src/main/java/com/rapidminer/operator/features/construction/AttributeAggregationOperator.java b/src/main/java/com/rapidminer/operator/features/construction/AttributeAggregationOperator.java index c518529a0..70c5fdfa7 100644 --- a/src/main/java/com/rapidminer/operator/features/construction/AttributeAggregationOperator.java +++ b/src/main/java/com/rapidminer/operator/features/construction/AttributeAggregationOperator.java @@ -45,6 +45,7 @@ import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.parameter.conditions.AboveOperatorVersionCondition; import com.rapidminer.tools.Ontology; +import com.rapidminer.tools.ProcessTools; import com.rapidminer.tools.math.function.aggregation.AbstractAggregationFunction; import com.rapidminer.tools.math.function.aggregation.AggregationFunction; @@ -210,7 +211,7 @@ public OperatorVersion[] getIncompatibleVersionChanges() { public List getParameterTypes() { List types = super.getParameterTypes(); types.add(new ParameterTypeString(PARAMETER_ATTRIBUTE_NAME, "Name of the resulting attributes.", false)); - types.addAll(new AttributeSubsetSelector(this, getExampleSetInputPort()).getParameterTypes()); + types.addAll(ProcessTools.setSubsetSelectorPrimaryParameter(new AttributeSubsetSelector(this, getExampleSetInputPort()).getParameterTypes(), true)); ParameterType type = new ParameterTypeCategory(PARAMETER_AGGREGATION_FUNCTION, "Function for aggregating the attribute values.", AbstractAggregationFunction.KNOWN_AGGREGATION_FUNCTION_NAMES, AbstractAggregationFunction.SUM); diff --git a/src/main/java/com/rapidminer/operator/features/construction/AttributeConstruction.java b/src/main/java/com/rapidminer/operator/features/construction/AttributeConstruction.java index b519ce5ff..7ec5eedf4 100644 --- a/src/main/java/com/rapidminer/operator/features/construction/AttributeConstruction.java +++ b/src/main/java/com/rapidminer/operator/features/construction/AttributeConstruction.java @@ -332,6 +332,7 @@ public List getParameterTypes() { new ParameterTypeExpression("function_expressions", "Function and arguments to use for generation.", getInputPort())); type.setExpert(false); + type.setPrimary(true); types.add(type); types.add(new ParameterTypeBoolean(PARAMETER_KEEP_ALL, diff --git a/src/main/java/com/rapidminer/operator/features/construction/GaussFeatureConstructionOperator.java b/src/main/java/com/rapidminer/operator/features/construction/GaussFeatureConstructionOperator.java index e8738cedc..0b39e142d 100644 --- a/src/main/java/com/rapidminer/operator/features/construction/GaussFeatureConstructionOperator.java +++ b/src/main/java/com/rapidminer/operator/features/construction/GaussFeatureConstructionOperator.java @@ -18,6 +18,9 @@ */ package com.rapidminer.operator.features.construction; +import java.util.LinkedList; +import java.util.List; + import com.rapidminer.example.Attribute; import com.rapidminer.example.Example; import com.rapidminer.example.ExampleSet; @@ -34,11 +37,9 @@ import com.rapidminer.parameter.ParameterTypeDouble; import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.tools.Ontology; +import com.rapidminer.tools.ProcessTools; import com.rapidminer.tools.math.container.Range; -import java.util.LinkedList; -import java.util.List; - /** * Creates a gaussian function based on a given attribute and a specified mean and standard @@ -113,7 +114,7 @@ private AttributeSubsetSelector getSubsetSelector() { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.addAll(getSubsetSelector().getParameterTypes()); + types.addAll(ProcessTools.setSubsetSelectorPrimaryParameter(getSubsetSelector().getParameterTypes(), true)); types.add(new ParameterTypeDouble(PARAMETER_MEAN, "The mean value for the gaussian function.", Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, 0.0d)); diff --git a/src/main/java/com/rapidminer/operator/features/weighting/NameBasedWeighting.java b/src/main/java/com/rapidminer/operator/features/weighting/NameBasedWeighting.java index fcd4faac4..bb5c7e610 100644 --- a/src/main/java/com/rapidminer/operator/features/weighting/NameBasedWeighting.java +++ b/src/main/java/com/rapidminer/operator/features/weighting/NameBasedWeighting.java @@ -135,6 +135,7 @@ public LinkedList getPreviewList() { }, new ParameterTypeDouble("weight", "The new weight for all this attributes.", Double.NEGATIVE_INFINITY, Double.MAX_VALUE, false)); type.setExpert(false); + type.setPrimary(true); types.add(type); types.add(new ParameterTypeBoolean( PARAMETER_DISTRIBUTE_WEIGHTS, diff --git a/src/main/java/com/rapidminer/operator/filesystem/CreateDirectoryOperator.java b/src/main/java/com/rapidminer/operator/filesystem/CreateDirectoryOperator.java index 9476f546f..8e1064a90 100644 --- a/src/main/java/com/rapidminer/operator/filesystem/CreateDirectoryOperator.java +++ b/src/main/java/com/rapidminer/operator/filesystem/CreateDirectoryOperator.java @@ -18,6 +18,9 @@ */ package com.rapidminer.operator.filesystem; +import java.io.File; +import java.util.List; + import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; @@ -28,9 +31,6 @@ import com.rapidminer.parameter.ParameterTypeDirectory; import com.rapidminer.parameter.ParameterTypeString; -import java.io.File; -import java.util.List; - /** * @@ -60,7 +60,9 @@ public CreateDirectoryOperator(OperatorDescription description) { public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeDirectory(PARAMETER_LOCATION, "The parent directory of the new folder.", false)); + ParameterType type = new ParameterTypeDirectory(PARAMETER_LOCATION, "The parent directory of the new folder.", false); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeString(PARAMETER_NAME, "The name of the new directory.", false, false)); return types; diff --git a/src/main/java/com/rapidminer/operator/filesystem/DeleteFileOperator.java b/src/main/java/com/rapidminer/operator/filesystem/DeleteFileOperator.java index c080d5e97..822efd911 100644 --- a/src/main/java/com/rapidminer/operator/filesystem/DeleteFileOperator.java +++ b/src/main/java/com/rapidminer/operator/filesystem/DeleteFileOperator.java @@ -59,7 +59,9 @@ public DeleteFileOperator(OperatorDescription description) { public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeFile(PARAMETER_FILE, "The file to delete.", "*", false, false)); + ParameterType type = new ParameterTypeFile(PARAMETER_FILE, "The file to delete.", "*", false, false); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeBoolean(PARAMETER_NO_FILE_ERROR, "Determines whether an exception should be generated if the file is not found.", false, false)); diff --git a/src/main/java/com/rapidminer/operator/filesystem/RenameFileOperator.java b/src/main/java/com/rapidminer/operator/filesystem/RenameFileOperator.java index 6140716ef..61a0368c7 100644 --- a/src/main/java/com/rapidminer/operator/filesystem/RenameFileOperator.java +++ b/src/main/java/com/rapidminer/operator/filesystem/RenameFileOperator.java @@ -18,6 +18,9 @@ */ package com.rapidminer.operator.filesystem; +import java.io.File; +import java.util.List; + import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; @@ -28,9 +31,6 @@ import com.rapidminer.parameter.ParameterTypeFile; import com.rapidminer.parameter.ParameterTypeString; -import java.io.File; -import java.util.List; - /** * @@ -57,7 +57,9 @@ public RenameFileOperator(OperatorDescription description) { public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeFile(PARAMETER_FILE, "The file to rename.", "*", false, false)); + ParameterType type = new ParameterTypeFile(PARAMETER_FILE, "The file to rename.", "*", false, false); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeString(PARAMETER_NEW_NAME, "The new filename.", false)); return types; diff --git a/src/main/java/com/rapidminer/operator/generator/UserSpecificationDataGenerator.java b/src/main/java/com/rapidminer/operator/generator/UserSpecificationDataGenerator.java index e31144c18..9142333c2 100644 --- a/src/main/java/com/rapidminer/operator/generator/UserSpecificationDataGenerator.java +++ b/src/main/java/com/rapidminer/operator/generator/UserSpecificationDataGenerator.java @@ -223,13 +223,15 @@ private void setRole(ExampleSet exampleSet, String name, String newRole) throws @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeList(PARAMETER_VALUES, + ParameterType type = new ParameterTypeList(PARAMETER_VALUES, "This parameter defines the attributes and their values in the single example returned.", new ParameterTypeString(PARAMETER_ATTRIBUTE_NAME, "This is the name of the generated attribute.", false), new ParameterTypeExpression(PARAMETER_ATTRIBUTE_VALUE, "An expression that is parsed to derive the value of this attribute.", new OperatorVersionCallable(this)), - false)); + false); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeList(PARAMETER_ROLES, "This parameter defines additional attribute role combinations.", new ParameterTypeString(PARAMETER_NAME, "The name of the attribute whose role should be changed.", false, diff --git a/src/main/java/com/rapidminer/operator/io/AbstractDataReader.java b/src/main/java/com/rapidminer/operator/io/AbstractDataReader.java index 6aa8abfa3..6ef413258 100644 --- a/src/main/java/com/rapidminer/operator/io/AbstractDataReader.java +++ b/src/main/java/com/rapidminer/operator/io/AbstractDataReader.java @@ -139,7 +139,7 @@ public abstract class AbstractDataReader extends AbstractExampleSource { */ public static final String PARAMETER_COLUM_ROLE = "attribute_role"; - public static final ArrayList ROLE_NAMES = new ArrayList(); + public static final List ROLE_NAMES = new ArrayList<>(); { ROLE_NAMES.clear(); @@ -155,7 +155,7 @@ public abstract class AbstractDataReader extends AbstractExampleSource { /** * a list of errors which might occurred during the importing prozess. */ - private List importErrors = new LinkedList(); + private List importErrors = new LinkedList<>(); protected abstract DataSet getDataSet() throws OperatorException, IOException; @@ -207,7 +207,7 @@ public abstract class AbstractDataReader extends AbstractExampleSource { * @see AbstractDataReader#hasParseError(int, int) * @see AbstractDataReader#hasParseErrorInColumn(int) */ - TreeMap> errorCells = new TreeMap>(); + TreeMap> errorCells = new TreeMap<>(); /** * The columns of the created {@link ExampleSet}. @@ -215,7 +215,7 @@ public abstract class AbstractDataReader extends AbstractExampleSource { * @see AbstractDataReader#createExampleSet() * @see AbstractDataReader#guessValueTypes() */ - private List attributeColumns = new ArrayList(); + private List attributeColumns = new ArrayList<>(); public void clearAllReaderSettings() { clearReaderSettings(); @@ -233,13 +233,6 @@ public void deleteAttributeMetaDataParamters() { setAttributeNamesDefinedByUser(false); } - // public void clearOperatorMetaData() { - // // just meta data information for the gui - // metaDataFixed = false; - // guessedValueTypes = false; - // rowCountFromGuessing = 0; - // } - public AbstractDataReader(OperatorDescription description) { super(description); } @@ -273,9 +266,6 @@ public List getActiveAttributeColumns() { * @return */ public List getAllAttributeColumns() { - // List list = new LinkedList(); - // list.addAll(attributeColumns); - // return list; return Collections.unmodifiableList(attributeColumns); } @@ -567,7 +557,7 @@ private void adjustAttributeColumnsNumbers(int newNumberOfColumns) { } // too long if (getAllAttributeColumns().size() > newNumberOfColumns) { - List list = new ArrayList(); + List list = new ArrayList<>(); for (int i = 0; i < newNumberOfColumns; i++) { list.add(getAttributeColumn(i)); } @@ -674,7 +664,7 @@ public List getPreviewAsList(ProgressListener progress, boolean trimAt public List getErrorPreviewAsList(ProgressListener progress) throws OperatorException { List preview = getPreviewAsList(progress, true, false, -1); - List errorPreview = new LinkedList(); + List errorPreview = new LinkedList<>(); Iterator it = preview.iterator(); int rowNum = 0; @@ -723,7 +713,7 @@ public List getPreviewAsList(ProgressListener progress, boolean enable throw new UserError(this, e, 403, e.getMessage()); } - List preview = new LinkedList(); + List preview = new LinkedList<>(); // counting starts at one because the user sees it. int currentRow = 1; @@ -787,10 +777,8 @@ public List getPreviewAsList(ProgressListener progress, boolean enable // information if (values[i + 1] != null) { if (column.valueSet.size() >= 2) { - if (!column.valueSet.contains(values[i + 1])) { - if (column.isActivated) { - foundParseError(i, rowCountFromGuessing); - } + if (!column.valueSet.contains(values[i + 1]) && column.isActivated) { + foundParseError(i, rowCountFromGuessing); } } else { column.valueSet.add((String) values[i + 1]); @@ -834,8 +822,8 @@ public List getPreviewAsList(ProgressListener progress, boolean enable private void foundParseError(int column, int row) { TreeSet treeSet = errorCells.get(column); if (treeSet == null) { - treeSet = new TreeSet(); - errorCells.put(column, new TreeSet()); + treeSet = new TreeSet<>(); + errorCells.put(column, new TreeSet<>()); } treeSet.add(row); } @@ -882,7 +870,7 @@ protected boolean isMetaDataCacheable() { * @return a unique column name */ protected String getNewGenericColumnName(int column) { - HashSet usedNames = new HashSet(); + HashSet usedNames = new HashSet<>(); for (AttributeColumn col : getAllAttributeColumns()) { usedNames.add(col.getName()); } @@ -905,7 +893,7 @@ protected String getNewGenericColumnName(int column) { * @return */ private String[] getGenericColumnNames(String[] proposedNames, String[] oldColumnNames) { - HashSet usedNames = new HashSet(); + HashSet usedNames = new HashSet<>(); for (AttributeColumn col : getAllAttributeColumns()) { usedNames.add(col.getName()); } @@ -1004,10 +992,8 @@ public void guessValueTypes(ProgressListener progress) throws OperatorException column.maxValue = number.doubleValue(); } // try integer - if (column.canParseInteger) { - if (!Tools.isEqual(Math.round(number.doubleValue()), number.doubleValue())) { - column.canParseInteger = false; - } + if (column.canParseInteger && !Tools.isEqual(Math.round(number.doubleValue()), number.doubleValue())) { + column.canParseInteger = false; } // set the value values[i] = number; @@ -1033,22 +1019,16 @@ public void guessValueTypes(ProgressListener progress) throws OperatorException if (column.lastDate != null) { lastDateCalendar.setTime(column.lastDate); currDateCalendar.setTime(date); - if (!column.shouldBeDate) { - if (lastDateCalendar.get(Calendar.DAY_OF_MONTH) != currDateCalendar - .get(Calendar.DAY_OF_MONTH) - || lastDateCalendar.get(Calendar.MONTH) != currDateCalendar.get(Calendar.MONTH) - || lastDateCalendar.get(Calendar.YEAR) != currDateCalendar.get(Calendar.YEAR)) { - column.shouldBeDate = true; - } + if (!column.shouldBeDate && (lastDateCalendar.get(Calendar.DAY_OF_MONTH) != currDateCalendar.get(Calendar.DAY_OF_MONTH) + || lastDateCalendar.get(Calendar.MONTH) != currDateCalendar.get(Calendar.MONTH) + || lastDateCalendar.get(Calendar.YEAR) != currDateCalendar.get(Calendar.YEAR))) { + column.shouldBeDate = true; } - if (!column.shouldBeTime) { - if (lastDateCalendar.get(Calendar.HOUR_OF_DAY) != currDateCalendar.get(Calendar.HOUR_OF_DAY) - || lastDateCalendar.get(Calendar.MINUTE) != currDateCalendar.get(Calendar.MINUTE) - || lastDateCalendar.get(Calendar.SECOND) != currDateCalendar.get(Calendar.SECOND) - || lastDateCalendar.get(Calendar.MILLISECOND) != currDateCalendar - .get(Calendar.MILLISECOND)) { - column.shouldBeTime = true; - } + if (!column.shouldBeTime && (lastDateCalendar.get(Calendar.HOUR_OF_DAY) != currDateCalendar.get(Calendar.HOUR_OF_DAY) + || lastDateCalendar.get(Calendar.MINUTE) != currDateCalendar.get(Calendar.MINUTE) + || lastDateCalendar.get(Calendar.SECOND) != currDateCalendar.get(Calendar.SECOND) + || lastDateCalendar.get(Calendar.MILLISECOND) != currDateCalendar.get(Calendar.MILLISECOND))) { + column.shouldBeTime = true; } } column.lastDate = date; @@ -1147,7 +1127,7 @@ public ExampleSet createExampleSet() throws OperatorException { * @throws OperatorException */ private ExampleSet createExampleSet(int limitOfReadLines) throws OperatorException { - List activeAttributes = new ArrayList(); + List activeAttributes = new ArrayList<>(); // load the attribute names/value types/ roles/... which are defined by // the user @@ -1174,7 +1154,7 @@ private ExampleSet createExampleSet(int limitOfReadLines) throws OperatorExcepti } // list of double arrays which holds the read values fpr each line - List dataRows = new ArrayList(); + List dataRows = new ArrayList<>(); int lineCount = 0; // debugging purpose while (set.next() && (limitOfReadLines == -1 || limitOfReadLines > lineCount)) { @@ -1247,7 +1227,7 @@ private ExampleSet createExampleSet(int limitOfReadLines) throws OperatorExcepti */ @Override public List getParameterTypes() { - List types = new LinkedList(); + List types = new LinkedList<>(); types.addAll(super.getParameterTypes()); types.add(new ParameterTypeBoolean(PARAMETER_ERROR_TOLERANT, @@ -1320,8 +1300,10 @@ protected CacheResetParameterObserver(String parameterKey) { @Override public void update(Observable observable, String arg) { String newFilename = getParameters().getParameterOrNull(parameterKey); - if (newFilename == null && oldFilename != null || newFilename != null && oldFilename == null - || newFilename != null && oldFilename != null && !newFilename.equals(oldFilename)) { + if (oldFilename == newFilename) { + return; + } + if (oldFilename == null || newFilename == null || !newFilename.equals(oldFilename)) { clearAllReaderSettings(); this.oldFilename = newFilename; } @@ -1546,7 +1528,7 @@ public String toString() { * The valueSet of this attribute, in case it is (bi)nominal. Only for the operator MetaData * purposes. */ - protected Set valueSet = new LinkedHashSet(); + protected Set valueSet = new LinkedHashSet<>(); /** * The number of missing values which were read during the guessing. Only for the operator @@ -1896,7 +1878,7 @@ protected abstract class DataSet { * @param columnIndex * @return */ - public abstract Date getDate(int columnIndex); + public abstract Date getDate(int columnIndex) throws OperatorException; /** * Closes the data source. May tear down a database connection or close a file which is re` diff --git a/src/main/java/com/rapidminer/operator/io/AbstractReader.java b/src/main/java/com/rapidminer/operator/io/AbstractReader.java index f07aebb09..911b606dc 100644 --- a/src/main/java/com/rapidminer/operator/io/AbstractReader.java +++ b/src/main/java/com/rapidminer/operator/io/AbstractReader.java @@ -18,6 +18,15 @@ */ package com.rapidminer.operator.io; +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import com.rapidminer.Process; import com.rapidminer.operator.Annotations; import com.rapidminer.operator.IOObject; @@ -26,26 +35,18 @@ import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; import com.rapidminer.operator.ProcessSetupError.Severity; +import com.rapidminer.operator.UserError; import com.rapidminer.operator.ports.OutputPort; -import com.rapidminer.operator.ports.metadata.MDTransformationRule; import com.rapidminer.operator.ports.metadata.MetaData; import com.rapidminer.operator.ports.metadata.MetaDataError; +import com.rapidminer.operator.ports.metadata.ProcessNotInRepositoryMetaDataError; import com.rapidminer.operator.ports.metadata.SimpleMetaDataError; +import com.rapidminer.operator.ports.quickfix.SaveProcessQuickFix; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.UndefinedParameterError; -import com.rapidminer.tools.Observable; -import com.rapidminer.tools.Observer; import com.rapidminer.tools.OperatorService; import com.rapidminer.tools.io.Encoding; -import java.io.File; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** * Superclass of all operators that have no input and generate a single output. This class is mainly @@ -65,34 +66,47 @@ public abstract class AbstractReader extends Operator { public AbstractReader(OperatorDescription description, Class generatedClass) { super(description); this.generatedClass = generatedClass; - getTransformer().addRule(new MDTransformationRule() { + getTransformer().addRule(() -> { + if (cacheDirty || !isMetaDataCacheable()) { + try { + // TODO add extra thread for meta data generation? + cachedMetaData = AbstractReader.this.getGeneratedMetaData(); + cachedError = null; + } catch (UserError e) { + cachedMetaData = new MetaData(AbstractReader.this.generatedClass); + String msg = e.getMessage(); + if ((msg == null) || (msg.length() == 0)) { + msg = e.toString(); + } - @Override - public void transformMD() { - if (cacheDirty || !isMetaDataCacheable()) { - try { - // TODO add extra thread for meta data generation? - cachedMetaData = AbstractReader.this.getGeneratedMetaData(); - cachedError = null; - } catch (OperatorException e) { - cachedMetaData = new MetaData(AbstractReader.this.generatedClass); - String msg = e.getMessage(); - if ((msg == null) || (msg.length() == 0)) { - msg = e.toString(); - } - // will be added below + // will be added below + if (e.getCode() == 317 && getProcess() != null) { + cachedError = new ProcessNotInRepositoryMetaDataError(Severity.WARNING, outputPort, + Collections.singletonList(new SaveProcessQuickFix(getProcess())), + "save_process", msg); + } else { cachedError = new SimpleMetaDataError(Severity.WARNING, outputPort, - "cannot_create_exampleset_metadata", new Object[] { msg }); + "cannot_create_exampleset_metadata", msg); } - if (cachedMetaData != null) { - cachedMetaData.addToHistory(outputPort); + } catch (OperatorException e) { + cachedMetaData = new MetaData(AbstractReader.this.generatedClass); + String msg = e.getMessage(); + if ((msg == null) || (msg.length() == 0)) { + msg = e.toString(); } - cacheDirty = false; + + // will be added below + cachedError = new SimpleMetaDataError(Severity.WARNING, outputPort, + "cannot_create_exampleset_metadata", msg); } - outputPort.deliverMD(cachedMetaData); - if (cachedError != null) { - outputPort.addError(cachedError); + if (cachedMetaData != null) { + cachedMetaData.addToHistory(outputPort); } + cacheDirty = false; + } + outputPort.deliverMD(cachedMetaData); + if (cachedError != null) { + outputPort.addError(cachedError); } }); observeParameters(); @@ -101,13 +115,7 @@ public void transformMD() { private void observeParameters() { // we add this as the first observer. otherwise, this change is not seen // by the resulting meta data transformation - getParameters().addObserverAsFirst(new Observer() { - - @Override - public void update(Observable observable, String arg) { - cacheDirty = true; - } - }, false); + getParameters().addObserverAsFirst((observable, arg) -> cacheDirty = true, false); } public MetaData getGeneratedMetaData() throws OperatorException { @@ -162,7 +170,7 @@ public ReaderDescription(String fileExtension, Class } } - private static final Map READER_DESCRIPTIONS = new HashMap(); + private static final Map READER_DESCRIPTIONS = new HashMap<>(); /** Registers an operator that can read files with a given extension. */ protected static void registerReaderDescription(ReaderDescription rd) { diff --git a/src/main/java/com/rapidminer/operator/io/AbstractStreamWriter.java b/src/main/java/com/rapidminer/operator/io/AbstractStreamWriter.java index 15dd53937..28b4859f3 100644 --- a/src/main/java/com/rapidminer/operator/io/AbstractStreamWriter.java +++ b/src/main/java/com/rapidminer/operator/io/AbstractStreamWriter.java @@ -30,10 +30,8 @@ import com.rapidminer.operator.nio.file.FileObject; import com.rapidminer.operator.nio.file.FileOutputPortHandler; import com.rapidminer.operator.ports.OutputPort; -import com.rapidminer.operator.ports.Port; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeFile; -import com.rapidminer.parameter.PortProvider; /** @@ -100,13 +98,7 @@ public ExampleSet write(ExampleSet exampleSet) throws OperatorException { * depends on whether or not {@link #fileOutputPort} is connected. */ protected ParameterType makeFileParameterType() { - return FileOutputPortHandler.makeFileParameterType(this, getFileParameterName(), new PortProvider() { - - @Override - public Port getPort() { - return fileOutputPort; - } - }, getFileExtensions()); + return FileOutputPortHandler.makeFileParameterType(this, getFileParameterName(), () -> fileOutputPort, getFileExtensions()); } /** diff --git a/src/main/java/com/rapidminer/operator/io/BytewiseExampleSource.java b/src/main/java/com/rapidminer/operator/io/BytewiseExampleSource.java index 5a8d1112f..7546d4f66 100644 --- a/src/main/java/com/rapidminer/operator/io/BytewiseExampleSource.java +++ b/src/main/java/com/rapidminer/operator/io/BytewiseExampleSource.java @@ -283,6 +283,7 @@ public Port getPort() { } }); type.setExpert(false); + type.setPrimary(true); types.add(type); DataManagementParameterHelper.addParameterTypes(types, this); return types; diff --git a/src/main/java/com/rapidminer/operator/io/CSVExampleSetWriter.java b/src/main/java/com/rapidminer/operator/io/CSVExampleSetWriter.java index f771f2b55..678a24c68 100644 --- a/src/main/java/com/rapidminer/operator/io/CSVExampleSetWriter.java +++ b/src/main/java/com/rapidminer/operator/io/CSVExampleSetWriter.java @@ -320,8 +320,9 @@ protected boolean shouldAppend() { @Override public List getParameterTypes() { List types = new LinkedList(); - types.add(makeFileParameterType()); - // types.add(new ParameterTypeFile(PARAMETER_CSV_FILE, + ParameterType type = makeFileParameterType(); + type.setPrimary(true); + types.add(type); // "The CSV file which should be written.", "csv", false)); types.add(new ParameterTypeString(PARAMETER_COLUMN_SEPARATOR, "The column separator.", ";", false)); types.add(new ParameterTypeBoolean(PARAMETER_WRITE_ATTRIBUTE_NAMES, @@ -331,7 +332,7 @@ public List getParameterTypes() { types.add(new ParameterTypeBoolean(PARAMETER_FORMAT_DATE, "Indicates if date attributes are written as a formated string or as milliseconds past since January 1, 1970, 00:00:00 GMT", true, true)); - ParameterType type = new ParameterTypeBoolean(PARAMETER_APPEND_FILE, + type = new ParameterTypeBoolean(PARAMETER_APPEND_FILE, "Indicates if new content should be appended to the file or if the pre-existing file content should be overwritten.", false, false); type.registerDependencyCondition(new PortConnectedCondition(this, new PortProvider() { diff --git a/src/main/java/com/rapidminer/operator/io/ExcelExampleSetWriter.java b/src/main/java/com/rapidminer/operator/io/ExcelExampleSetWriter.java index 9e72ea2ea..c5a12d8e6 100644 --- a/src/main/java/com/rapidminer/operator/io/ExcelExampleSetWriter.java +++ b/src/main/java/com/rapidminer/operator/io/ExcelExampleSetWriter.java @@ -50,7 +50,6 @@ import com.rapidminer.parameter.ParameterTypeDateFormat; import com.rapidminer.parameter.ParameterTypeString; import com.rapidminer.parameter.conditions.EqualTypeCondition; -import com.rapidminer.tools.DateParser; import com.rapidminer.tools.I18N; import com.rapidminer.tools.Ontology; import com.rapidminer.tools.io.Encoding; @@ -92,11 +91,15 @@ public class ExcelExampleSetWriter extends AbstractStreamWriter { public static final int FILE_FORMAT_XLSX_INDEX = 1; public static final String PARAMETER_FILE_FORMAT = "file_format"; - public static final String PARAMETER_DATE_FORMAT = "date_format"; + /** + * @deprecated since 8.2; use {@link ParameterTypeDateFormat#PARAMETER_DATE_FORMAT} instead. + */ + @Deprecated + public static final String PARAMETER_DATE_FORMAT = ParameterTypeDateFormat.PARAMETER_DATE_FORMAT; public static final String PARAMETER_NUMBER_FORMAT = "number_format"; public static final String PARAMETER_SHEET_NAME = "sheet_name"; - public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public static final String DEFAULT_DATE_FORMAT = ParameterTypeDateFormat.DATE_TIME_FORMAT_YYYY_MM_DD_HH_MM_SS; public static final String DEFAULT_NUMBER_FORMAT = "#.0"; /** @@ -206,7 +209,7 @@ private static void writeDataSheet(WritableSheet s, ExampleSet exampleSet, Opera WritableFont wf2 = new WritableFont(WritableFont.ARIAL, 10, WritableFont.NO_BOLD); WritableCellFormat cf2 = new WritableCellFormat(wf2); - DateFormat df = new DateFormat(DateParser.DEFAULT_DATE_TIME_FORMAT); + DateFormat df = new DateFormat(ParameterTypeDateFormat.DATE_TIME_FORMAT_YYYY_MM_DD_HH_MM_SS); WritableCellFormat dfCell = new WritableCellFormat(df); int rowCounter = 1; @@ -250,15 +253,17 @@ private static String replaceForbiddenChars(String originalValue) { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(makeFileParameterType()); + ParameterType type = makeFileParameterType(); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeCategory(PARAMETER_FILE_FORMAT, "Defines the file format the excel file should be saved as.", FILE_FORMAT_CATEGORIES, FILE_FORMAT_XLSX_INDEX, false)); List encodingTypes = Encoding.getParameterTypes(this); - for (ParameterType type : encodingTypes) { - type.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_FILE_FORMAT, FILE_FORMAT_CATEGORIES, + for (ParameterType encodingType : encodingTypes) { + encodingType.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_FILE_FORMAT, FILE_FORMAT_CATEGORIES, false, new int[] { FILE_FORMAT_XLS_INDEX })); } types.addAll(encodingTypes); @@ -269,13 +274,14 @@ public List getParameterTypes() { RAPID_MINER_DATA); sheetName.setExpert(false); xlsxTypes.add(sheetName); - xlsxTypes.add(new ParameterTypeDateFormat(PARAMETER_DATE_FORMAT, - "The parse format of the date values. Default: for example \"yyyy-MM-dd HH:mm:ss\".", DEFAULT_DATE_FORMAT, - true)); + ParameterType dateType = new ParameterTypeDateFormat(); + dateType.setDefaultValue(DEFAULT_DATE_FORMAT); + dateType.setExpert(true); + xlsxTypes.add(dateType); xlsxTypes.add(new ParameterTypeString(PARAMETER_NUMBER_FORMAT, "Specifies the number format for date entries. Default: \"#.0\"", DEFAULT_NUMBER_FORMAT, true)); - for (ParameterType type : xlsxTypes) { - type.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_FILE_FORMAT, FILE_FORMAT_CATEGORIES, + for (ParameterType xlsxType : xlsxTypes) { + xlsxType.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_FILE_FORMAT, FILE_FORMAT_CATEGORIES, false, new int[] { FILE_FORMAT_XLSX_INDEX })); } types.addAll(xlsxTypes); @@ -299,7 +305,10 @@ protected void writeStream(ExampleSet exampleSet, OutputStream outputStream) thr if (getParameterAsString(PARAMETER_FILE_FORMAT).equals(FILE_FORMAT_XLSX)) { - String dateFormat = isParameterSet(PARAMETER_DATE_FORMAT) ? getParameterAsString(PARAMETER_DATE_FORMAT) : null; + // check if date format is valid + ParameterTypeDateFormat.createCheckedDateFormat(this, null); + + String dateFormat = isParameterSet(ParameterTypeDateFormat.PARAMETER_DATE_FORMAT) ? getParameterAsString(ParameterTypeDateFormat.PARAMETER_DATE_FORMAT) : null; String numberFormat = isParameterSet(PARAMETER_NUMBER_FORMAT) ? getParameterAsString(PARAMETER_NUMBER_FORMAT) : null; String sheetName = getParameterAsString(PARAMETER_SHEET_NAME); diff --git a/src/main/java/com/rapidminer/operator/io/RepositorySource.java b/src/main/java/com/rapidminer/operator/io/RepositorySource.java index 40d7272b8..4b3e15fe0 100644 --- a/src/main/java/com/rapidminer/operator/io/RepositorySource.java +++ b/src/main/java/com/rapidminer/operator/io/RepositorySource.java @@ -18,8 +18,13 @@ */ package com.rapidminer.operator.io; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; + import com.rapidminer.operator.Annotations; import com.rapidminer.operator.IOObject; +import com.rapidminer.operator.InvalidRepositoryEntryError; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; import com.rapidminer.operator.ProcessSetupError.Severity; @@ -28,17 +33,17 @@ import com.rapidminer.operator.ports.metadata.AttributeMetaData; import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; import com.rapidminer.operator.ports.metadata.MetaData; +import com.rapidminer.operator.ports.quickfix.ParameterSettingQuickFix; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeRepositoryLocation; import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.repository.Entry; import com.rapidminer.repository.IOObjectEntry; +import com.rapidminer.repository.RepositoryEntryNotFoundException; +import com.rapidminer.repository.RepositoryEntryWrongTypeException; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; -import java.util.List; -import java.util.logging.Level; - /** * @@ -57,6 +62,18 @@ public MetaData getGeneratedMetaData() throws OperatorException { IOObjectEntry entry; try { entry = getRepositoryEntry(); + } catch (RepositoryEntryNotFoundException e) { + addError(new InvalidRepositoryEntryError(Severity.WARNING, getPortOwner(), PARAMETER_REPOSITORY_ENTRY, + Collections.singletonList(new ParameterSettingQuickFix(getPortOwner().getOperator(), PARAMETER_REPOSITORY_ENTRY)), + "repository_location_does_not_exist", getParameterAsRepositoryLocation(PARAMETER_REPOSITORY_ENTRY), + e.getMessage())); + return super.getGeneratedMetaData(); + } catch (RepositoryEntryWrongTypeException e) { + addError(new InvalidRepositoryEntryError(Severity.WARNING, getPortOwner(), PARAMETER_REPOSITORY_ENTRY, + Collections.singletonList(new ParameterSettingQuickFix(getPortOwner().getOperator(), PARAMETER_REPOSITORY_ENTRY)), + "repository_location_wrong_type", getParameterAsRepositoryLocation(PARAMETER_REPOSITORY_ENTRY), + e.getMessage())); + return super.getGeneratedMetaData(); } catch (RepositoryException e) { addError(new SimpleProcessSetupError(Severity.WARNING, getPortOwner(), "repository_access_error", getParameterAsRepositoryLocation(PARAMETER_REPOSITORY_ENTRY), e.getMessage())); @@ -92,11 +109,11 @@ private IOObjectEntry getRepositoryEntry() throws RepositoryException, UserError RepositoryLocation location = getParameterAsRepositoryLocation(PARAMETER_REPOSITORY_ENTRY); Entry entry = location.locateEntry(); if (entry == null) { - throw new RepositoryException("Entry '" + location + "' does not exist."); + throw new RepositoryEntryNotFoundException("Entry '" + location + "' does not exist."); } else if (entry instanceof IOObjectEntry) { return (IOObjectEntry) entry; } else { - throw new RepositoryException("Entry '" + location + "' is not a data entry, but " + entry.getType()); + throw new RepositoryEntryWrongTypeException("Entry '" + location + "' is not a data entry, but " + entry.getType()); } } @@ -117,6 +134,7 @@ public List getParameterTypes() { ParameterTypeRepositoryLocation type = new ParameterTypeRepositoryLocation(PARAMETER_REPOSITORY_ENTRY, "Repository entry.", false); type.setExpert(false); + type.setPrimary(true); types.add(type); return types; } diff --git a/src/main/java/com/rapidminer/operator/io/RepositoryStorer.java b/src/main/java/com/rapidminer/operator/io/RepositoryStorer.java index b1ac6e8dc..6000edb2f 100644 --- a/src/main/java/com/rapidminer/operator/io/RepositoryStorer.java +++ b/src/main/java/com/rapidminer/operator/io/RepositoryStorer.java @@ -18,6 +18,8 @@ */ package com.rapidminer.operator.io; +import java.util.List; + import com.rapidminer.operator.IOObject; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; @@ -28,8 +30,6 @@ import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.repository.RepositoryManager; -import java.util.List; - /** * This operator stores IOObjects at a location in a repository. @@ -61,6 +61,7 @@ public List getParameterTypes() { ParameterTypeRepositoryLocation type = new ParameterTypeRepositoryLocation(PARAMETER_REPOSITORY_ENTRY, "Repository entry.", true, false, false, false, true, true); type.setExpert(false); + type.setPrimary(true); types.add(type); return types; } diff --git a/src/main/java/com/rapidminer/operator/io/ResultWriter.java b/src/main/java/com/rapidminer/operator/io/ResultWriter.java index 495bff04f..b972f6ca8 100644 --- a/src/main/java/com/rapidminer/operator/io/ResultWriter.java +++ b/src/main/java/com/rapidminer/operator/io/ResultWriter.java @@ -116,6 +116,7 @@ public List getParameterTypes() { "Appends the descriptions of the input objects to this file. If empty, use the general file defined in the process root operator.", "res", true); type.setExpert(false); + type.setPrimary(true); types.add(type); types.addAll(Encoding.getParameterTypes(this)); diff --git a/src/main/java/com/rapidminer/operator/io/SpecialFormatExampleSetWriter.java b/src/main/java/com/rapidminer/operator/io/SpecialFormatExampleSetWriter.java index 56839470a..d4c6fec82 100644 --- a/src/main/java/com/rapidminer/operator/io/SpecialFormatExampleSetWriter.java +++ b/src/main/java/com/rapidminer/operator/io/SpecialFormatExampleSetWriter.java @@ -18,6 +18,19 @@ */ package com.rapidminer.operator.io; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Level; +import java.util.zip.GZIPOutputStream; + import com.rapidminer.example.Example; import com.rapidminer.example.ExampleFormatter; import com.rapidminer.example.ExampleSet; @@ -34,19 +47,6 @@ import com.rapidminer.parameter.ParameterTypeString; import com.rapidminer.tools.io.Encoding; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.nio.charset.Charset; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.logging.Level; -import java.util.zip.GZIPOutputStream; - /** * Writes an example set to a file using a special, user defined format. @@ -196,8 +196,10 @@ private void writeSpecialFormat(ExampleSet exampleSet, File dataFile, int fracti @Override public List getParameterTypes() { - List types = new LinkedList(); - types.add(new ParameterTypeFile(PARAMETER_EXAMPLE_SET_FILE, "File to save the example set to.", "dat", false)); + List types = new LinkedList<>(); + ParameterType type = new ParameterTypeFile(PARAMETER_EXAMPLE_SET_FILE, "File to save the example set to.", "dat", false); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeString(PARAMETER_SPECIAL_FORMAT, "Format string to use for output.", false)); types.add(new ParameterTypeInt(PARAMETER_FRACTION_DIGITS, "The number of fraction digits in the output file (-1: all possible digits).", -1, Integer.MAX_VALUE, -1)); diff --git a/src/main/java/com/rapidminer/operator/learner/associations/FrequentItemSet.java b/src/main/java/com/rapidminer/operator/learner/associations/FrequentItemSet.java index bdec9d590..ae1e2b57e 100644 --- a/src/main/java/com/rapidminer/operator/learner/associations/FrequentItemSet.java +++ b/src/main/java/com/rapidminer/operator/learner/associations/FrequentItemSet.java @@ -18,14 +18,14 @@ */ package com.rapidminer.operator.learner.associations; -import com.rapidminer.tools.Tools; - import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import com.rapidminer.tools.Tools; + /** * A frequent item set contains a set of frequent {@link Item}s. @@ -41,7 +41,7 @@ public class FrequentItemSet implements Comparable, Cloneable, private int frequency; public FrequentItemSet() { - this.items = new ArrayList(1); + this.items = new ArrayList<>(1); } public FrequentItemSet(ArrayList items, int frequency) { @@ -50,6 +50,14 @@ public FrequentItemSet(ArrayList items, int frequency) { this.frequency = frequency; } + /** + * Clone constructor. + */ + private FrequentItemSet(FrequentItemSet other) { + this.items = new ArrayList<>(other.items); + this.frequency = other.frequency; + } + public void addItem(Item item, int frequency) { items.add(item); Collections.sort(this.items); @@ -103,10 +111,7 @@ public int compareTo(FrequentItemSet o) { */ @Override public boolean equals(Object o) { - if (o instanceof FrequentItemSet) { - return (this.compareTo((FrequentItemSet) o) == 0); - } - return false; + return o instanceof FrequentItemSet && (this.compareTo((FrequentItemSet) o) == 0); } @Override @@ -118,7 +123,7 @@ public int hashCode() { * This method returns a representation of the items */ public String getItemsAsString() { - StringBuffer buffer = new StringBuffer(); + StringBuilder buffer = new StringBuilder(); Iterator iterator = items.iterator(); while (iterator.hasNext()) { buffer.append(iterator.next().toString()); @@ -134,7 +139,7 @@ public String getItemsAsString() { */ @Override public String toString() { - StringBuffer buffer = new StringBuffer(); + StringBuilder buffer = new StringBuilder(); Iterator iterator = items.iterator(); while (iterator.hasNext()) { buffer.append(iterator.next().toString()); @@ -149,6 +154,6 @@ public String toString() { @Override public Object clone() { - return new FrequentItemSet(new ArrayList(items), frequency); + return new FrequentItemSet(this); } } diff --git a/src/main/java/com/rapidminer/operator/learner/associations/fpgrowth/FPGrowth.java b/src/main/java/com/rapidminer/operator/learner/associations/fpgrowth/FPGrowth.java index 8658c241e..e3f7ef21f 100644 --- a/src/main/java/com/rapidminer/operator/learner/associations/fpgrowth/FPGrowth.java +++ b/src/main/java/com/rapidminer/operator/learner/associations/fpgrowth/FPGrowth.java @@ -91,7 +91,9 @@ *

* * @author Sebastian Land, Ingo Mierswa, Marius Helf + * @deprecated since 8.2, replaced by the BeltFPGrowth in the Concurrency extension */ +@Deprecated public class FPGrowth extends Operator { /** diff --git a/src/main/java/com/rapidminer/operator/learner/functions/neuralnet/ImprovedNeuralNetLearner.java b/src/main/java/com/rapidminer/operator/learner/functions/neuralnet/ImprovedNeuralNetLearner.java index 1c1695d83..f14c433b4 100644 --- a/src/main/java/com/rapidminer/operator/learner/functions/neuralnet/ImprovedNeuralNetLearner.java +++ b/src/main/java/com/rapidminer/operator/learner/functions/neuralnet/ImprovedNeuralNetLearner.java @@ -168,6 +168,7 @@ public List getParameterTypes() { "The size of the hidden layers. A size of < 0 leads to a layer size of (number_of_attributes + number of classes) / 2 + 1.", -1, Integer.MAX_VALUE, -1)); type.setExpert(false); + type.setPrimary(true); types.add(type); type = new ParameterTypeInt(PARAMETER_TRAINING_CYCLES, diff --git a/src/main/java/com/rapidminer/operator/learner/meta/CostBasedThresholdLearner.java b/src/main/java/com/rapidminer/operator/learner/meta/CostBasedThresholdLearner.java index da0f76435..6d5e44a44 100644 --- a/src/main/java/com/rapidminer/operator/learner/meta/CostBasedThresholdLearner.java +++ b/src/main/java/com/rapidminer/operator/learner/meta/CostBasedThresholdLearner.java @@ -210,6 +210,7 @@ public List getParameterTypes() { new ParameterTypeString("class_name", "The name of the class."), new ParameterTypeDouble("weight", "The weight for this class.", 0.0d, Double.POSITIVE_INFINITY, 1.0d)); type.setExpert(false); + type.setPrimary(true); types.add(type); types.add(new ParameterTypeBoolean( diff --git a/src/main/java/com/rapidminer/operator/learner/meta/HierarchicalMultiClassLearner.java b/src/main/java/com/rapidminer/operator/learner/meta/HierarchicalMultiClassLearner.java index 594ae553a..6082dfa88 100644 --- a/src/main/java/com/rapidminer/operator/learner/meta/HierarchicalMultiClassLearner.java +++ b/src/main/java/com/rapidminer/operator/learner/meta/HierarchicalMultiClassLearner.java @@ -18,6 +18,13 @@ */ package com.rapidminer.operator.learner.meta; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + import com.rapidminer.example.Attribute; import com.rapidminer.example.Example; import com.rapidminer.example.ExampleSet; @@ -36,13 +43,6 @@ import com.rapidminer.parameter.ParameterTypeString; import com.rapidminer.tools.RandomGenerator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; - /** * This is a meta learner for classifying multiple classes using a hierarchical approach. For a @@ -274,9 +274,11 @@ public boolean supportsCapability(OperatorCapability capability) { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeList(PARAMETER_HIERARCHY, "The hierarchy...", new ParameterTypeString( + ParameterType type = new ParameterTypeList(PARAMETER_HIERARCHY, "The hierarchy...", new ParameterTypeString( PARAMETER_PARENT_CLASS, "The parent class.", false), new ParameterTypeString(PARAMETER_CHILD_CLASS, - "The child class.", false))); + "The child class.", false)); + type.setPrimary(true); + types.add(type); types.addAll(RandomGenerator.getRandomGeneratorParameters(this)); return types; } diff --git a/src/main/java/com/rapidminer/operator/learner/meta/MetaCost.java b/src/main/java/com/rapidminer/operator/learner/meta/MetaCost.java index df7b88001..cd2bd648e 100644 --- a/src/main/java/com/rapidminer/operator/learner/meta/MetaCost.java +++ b/src/main/java/com/rapidminer/operator/learner/meta/MetaCost.java @@ -124,8 +124,10 @@ public boolean supportsCapability(OperatorCapability capability) { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeMatrix(PARAMETER_COST_MATRIX, "The cost matrix in Matlab single line format", - "Cost Matrix", "Predicted Class", "True Class", true, false)); + ParameterType type = new ParameterTypeMatrix(PARAMETER_COST_MATRIX, "The cost matrix in Matlab single line format", + "Cost Matrix", "Predicted Class", "True Class", true, false); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeDouble(PARAMETER_USE_SUBSET_FOR_TRAINING, "Fraction of examples used for training. Must be greater than 0 and should be lower than 1.", 0, 1, 1.0)); types.add(new ParameterTypeInt(PARAMETER_ITERATIONS, "The number of iterations (base models).", 1, Integer.MAX_VALUE, diff --git a/src/main/java/com/rapidminer/operator/meta/ParameterCloner.java b/src/main/java/com/rapidminer/operator/meta/ParameterCloner.java index d1a41f655..98bfa5082 100644 --- a/src/main/java/com/rapidminer/operator/meta/ParameterCloner.java +++ b/src/main/java/com/rapidminer/operator/meta/ParameterCloner.java @@ -184,11 +184,13 @@ public int checkProperties() { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeList(PARAMETER_NAME_MAP, + ParameterType type = new ParameterTypeList(PARAMETER_NAME_MAP, "A list mapping operator parameters from the set to other operator parameters in the process setup.", new ParameterTypeString("source", "The source parameter, specified as 'operator'.'parameter'. This value is copied to the target parameter."), - new ParameterTypeString("target", "The target parameter, specified as 'operator'.'parameter'."), false)); + new ParameterTypeString("target", "The target parameter, specified as 'operator'.'parameter'."), false); + type.setPrimary(true); + types.add(type); return types; } } diff --git a/src/main/java/com/rapidminer/operator/meta/ParameterSetter.java b/src/main/java/com/rapidminer/operator/meta/ParameterSetter.java index 76bcb0d92..1fc9b37da 100644 --- a/src/main/java/com/rapidminer/operator/meta/ParameterSetter.java +++ b/src/main/java/com/rapidminer/operator/meta/ParameterSetter.java @@ -111,6 +111,7 @@ public List getParameterTypes() { new ParameterTypeString("operator_name", "The name of an operator in the process setup, which parameters should be set in according to the parameter set.")); type.setExpert(false); + type.setPrimary(true); types.add(type); return types; } diff --git a/src/main/java/com/rapidminer/operator/meta/RepositoryIterator.java b/src/main/java/com/rapidminer/operator/meta/RepositoryIterator.java index d519037a6..1855cc416 100644 --- a/src/main/java/com/rapidminer/operator/meta/RepositoryIterator.java +++ b/src/main/java/com/rapidminer/operator/meta/RepositoryIterator.java @@ -171,6 +171,7 @@ public List getParameterTypes() { ParameterTypeRepositoryLocation folder = new ParameterTypeRepositoryLocation(PARAMETER_DIRECTORY, "Folder in the repository to iterate over", false, true, false); folder.setExpert(false); + folder.setPrimary(true); types.add(folder); types.addAll(super.getParameterTypes()); diff --git a/src/main/java/com/rapidminer/operator/meta/ZippedFileIterator.java b/src/main/java/com/rapidminer/operator/meta/ZippedFileIterator.java index e5d4e5239..f42f50e7d 100644 --- a/src/main/java/com/rapidminer/operator/meta/ZippedFileIterator.java +++ b/src/main/java/com/rapidminer/operator/meta/ZippedFileIterator.java @@ -160,7 +160,7 @@ public Port getPort() { return fileInputPort; } }, true, false)); - + type.setPrimary(true); types.add(type); type = new ParameterTypeString(PARAMETER_INTERNAL_DIRECTORY, diff --git a/src/main/java/com/rapidminer/operator/nio/CSVExampleSource.java b/src/main/java/com/rapidminer/operator/nio/CSVExampleSource.java index 4fb9beb08..886933370 100644 --- a/src/main/java/com/rapidminer/operator/nio/CSVExampleSource.java +++ b/src/main/java/com/rapidminer/operator/nio/CSVExampleSource.java @@ -32,9 +32,9 @@ import com.rapidminer.parameter.ParameterTypeBoolean; import com.rapidminer.parameter.ParameterTypeChar; import com.rapidminer.parameter.ParameterTypeConfiguration; +import com.rapidminer.parameter.ParameterTypeDateFormat; import com.rapidminer.parameter.ParameterTypeString; import com.rapidminer.parameter.conditions.BooleanParameterCondition; -import com.rapidminer.tools.DateParser; import com.rapidminer.tools.LineParser; import com.rapidminer.tools.StrictDecimalFormat; @@ -129,7 +129,9 @@ public List getParameterTypes() { // Numberformats types.addAll(StrictDecimalFormat.getParameterTypes(this, true)); - types.addAll(DateParser.getParameterTypes(this)); + type = new ParameterTypeDateFormat(); + type.setDefaultValue(ParameterTypeDateFormat.DATE_FORMAT_YYYY_MM_DD); + types.add(type); types.addAll(super.getParameterTypes()); return types; diff --git a/src/main/java/com/rapidminer/operator/nio/ExcelFormatExampleSource.java b/src/main/java/com/rapidminer/operator/nio/ExcelFormatExampleSource.java index 35835903e..69ab366d9 100644 --- a/src/main/java/com/rapidminer/operator/nio/ExcelFormatExampleSource.java +++ b/src/main/java/com/rapidminer/operator/nio/ExcelFormatExampleSource.java @@ -42,12 +42,10 @@ import com.rapidminer.operator.nio.file.FileInputPortHandler; import com.rapidminer.operator.nio.file.FileObject; import com.rapidminer.operator.ports.InputPort; -import com.rapidminer.operator.ports.Port; import com.rapidminer.operator.ports.metadata.MetaData; import com.rapidminer.operator.ports.metadata.SimplePrecondition; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeInt; -import com.rapidminer.parameter.PortProvider; import com.rapidminer.tools.Ontology; import com.rapidminer.tools.Tools; import com.rapidminer.tools.io.Encoding; @@ -271,23 +269,10 @@ public ExampleSet createExampleSet() throws OperatorException { public List getParameterTypes() { List types = super.getParameterTypes(); - /* - * ParameterType fileParam = new ParameterTypeFile(PARAMETER_EXCEL_FILE, - * "Name of the excel file to read the data from.", "xls", false); - * fileParam.registerDependencyCondition(new InputPortNotConnectedCondition(this, new - * PortProvider() { - * - * @Override public Port getPort() { return fileInputPort; } }, true)); - * types.add(fileParam); - */ - types.add(FileInputPortHandler.makeFileParameterType(this, PARAMETER_EXCEL_FILE, - "Name of the file to read the data from.", "xls", new PortProvider() { - - @Override - public Port getPort() { - return fileInputPort; - } - })); + ParameterType type = FileInputPortHandler.makeFileParameterType(this, PARAMETER_EXCEL_FILE, + "Name of the file to read the data from.", "xls", () -> fileInputPort); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeInt(PARAMETER_SHEET_NUMBER, "The number of the sheet which should be imported.", 1, Integer.MAX_VALUE, 1, false)); diff --git a/src/main/java/com/rapidminer/operator/nio/MetaDataDeclarationWizardStep.java b/src/main/java/com/rapidminer/operator/nio/MetaDataDeclarationWizardStep.java index b0f8e393c..704968246 100644 --- a/src/main/java/com/rapidminer/operator/nio/MetaDataDeclarationWizardStep.java +++ b/src/main/java/com/rapidminer/operator/nio/MetaDataDeclarationWizardStep.java @@ -303,7 +303,10 @@ public void run() { if (nameIndex != -1 && dataPreview != null) { for (int i = 0; i < dataPreview.getColumnCount(); i++) { ColumnMetaData columnMetaData = state.getTranslationConfiguration().getColumnMetaData(i); - final String foundName = String.valueOf(dataPreview.getValueAt(nameIndex, i)); + String foundName = String.valueOf(dataPreview.getValueAt(nameIndex, i)); + if (state.getOperator() == null || state.getOperator().shouldTrimAttributeNames()) { + foundName = foundName == null ? null : foundName.trim(); + } if (foundName != null && !foundName.isEmpty()) { columnMetaData.setUserDefinedAttributeName(foundName); } diff --git a/src/main/java/com/rapidminer/operator/nio/file/LoadFileOperator.java b/src/main/java/com/rapidminer/operator/nio/file/LoadFileOperator.java index 500e7b0da..3f119d8f6 100644 --- a/src/main/java/com/rapidminer/operator/nio/file/LoadFileOperator.java +++ b/src/main/java/com/rapidminer/operator/nio/file/LoadFileOperator.java @@ -18,6 +18,15 @@ */ package com.rapidminer.operator.nio.file; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.LinkedList; +import java.util.List; + import com.rapidminer.operator.Annotations; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorDescription; @@ -42,15 +51,6 @@ import com.rapidminer.tools.Tools; import com.rapidminer.tools.WebServiceTools; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; -import java.util.LinkedList; -import java.util.List; - /** * @author Nils Woehler, Marius Helf @@ -222,6 +222,7 @@ public List getParameterTypes() { ParameterTypeFile parameterTypeFile = new ParameterTypeFile(PARAMETER_FILENAME, "File to open", null, true, false); parameterTypeFile.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_SOURCE_TYPE, SOURCE_TYPES, true, SOURCE_TYPE_FILE)); + parameterTypeFile.setPrimary(true); parameterTypes.add(parameterTypeFile); ParameterTypeString parameterTypeUrl = new ParameterTypeString(PARAMETER_URL, "URL to open", true, false); diff --git a/src/main/java/com/rapidminer/operator/nio/file/WriteFileOperator.java b/src/main/java/com/rapidminer/operator/nio/file/WriteFileOperator.java index 2b58d64a0..62b18a155 100644 --- a/src/main/java/com/rapidminer/operator/nio/file/WriteFileOperator.java +++ b/src/main/java/com/rapidminer/operator/nio/file/WriteFileOperator.java @@ -138,6 +138,7 @@ public List getParameterTypes() { false); parameterTypeFile.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_DESTINATION_TYPE, DESTINATION_TYPES, true, DESTINATION_TYPE_FILE)); + parameterTypeFile.setPrimary(true); parameterTypes.add(parameterTypeFile); ParameterTypeRepositoryLocation parameterTypeRepositoryLocation = new ParameterTypeRepositoryLocation( diff --git a/src/main/java/com/rapidminer/operator/nio/model/AbstractDataResultSetReader.java b/src/main/java/com/rapidminer/operator/nio/model/AbstractDataResultSetReader.java index 20f7981c6..08bc925b2 100644 --- a/src/main/java/com/rapidminer/operator/nio/model/AbstractDataResultSetReader.java +++ b/src/main/java/com/rapidminer/operator/nio/model/AbstractDataResultSetReader.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import com.rapidminer.example.Attributes; import com.rapidminer.example.ExampleSet; @@ -38,7 +39,6 @@ import com.rapidminer.operator.nio.file.FileInputPortHandler; import com.rapidminer.operator.nio.file.FileObject; import com.rapidminer.operator.ports.InputPort; -import com.rapidminer.operator.ports.Port; import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; import com.rapidminer.operator.ports.metadata.MetaData; import com.rapidminer.operator.ports.metadata.SimplePrecondition; @@ -53,7 +53,6 @@ import com.rapidminer.parameter.ParameterTypeString; import com.rapidminer.parameter.ParameterTypeStringCategory; import com.rapidminer.parameter.ParameterTypeTupel; -import com.rapidminer.parameter.PortProvider; import com.rapidminer.parameter.conditions.BooleanParameterCondition; import com.rapidminer.parameter.conditions.ParameterCondition; import com.rapidminer.tools.Ontology; @@ -89,7 +88,11 @@ public abstract class AbstractDataResultSetReader extends AbstractExampleSource public static final String PARAMETER_COLUMN_ROLE = "attribute_role"; public static final String PARAMETER_READ_AS_POLYNOMINAL = "read_all_values_as_polynominal"; - public static final String PARAMETER_DATE_FORMAT = "date_format"; + /** + * @deprecated since 8.2; use {@link ParameterTypeDateFormat#PARAMETER_DATE_FORMAT} instead. + */ + @Deprecated + public static final String PARAMETER_DATE_FORMAT = ParameterTypeDateFormat.PARAMETER_DATE_FORMAT; public static final String PARAMETER_TIME_ZONE = "time_zone"; public static final String PARAMETER_LOCALE = "locale"; @@ -122,6 +125,14 @@ public InputPort getFileInputPort() { @Override public ExampleSet createExampleSet() throws OperatorException { + // check if date format is valid + int localeIndex = getParameterAsInt(PARAMETER_LOCALE); + Locale selectedLocale = Locale.US; + if (localeIndex >= 0 && localeIndex < AbstractDateDataProcessing.availableLocales.size()) { + selectedLocale = AbstractDateDataProcessing.availableLocales.get(localeIndex); + } + ParameterTypeDateFormat.createCheckedDateFormat(this, selectedLocale, false); + // loading data result set final ExampleSet exampleSet; try (DataResultSetFactory dataResultSetFactory = getDataResultSetFactory(); @@ -249,18 +260,12 @@ protected String[] getFileExtensions() { */ protected ParameterType makeFileParameterType() { return FileInputPortHandler.makeFileParameterType(this, getFileParameterName(), - "Name of the file to read the data from.", new PortProvider() { - - @Override - public Port getPort() { - return fileInputPort; - } - }, true, getFileExtensions()); + "Name of the file to read the data from.", () -> fileInputPort, true, getFileExtensions()); } @Override public List getParameterTypes() { - List types = new LinkedList(); + List types = new LinkedList<>(); if (isSupportingFirstRowAsNames()) { types.add(new ParameterTypeBoolean( @@ -269,7 +274,7 @@ public List getParameterTypes() { true, false)); } - List annotations = new LinkedList(); + List annotations = new LinkedList<>(); annotations.add(ANNOTATION_NAME); annotations.addAll(Arrays.asList(Annotations.ALL_KEYS_ATTRIBUTE)); ParameterType type = new ParameterTypeList(PARAMETER_ANNOTATIONS, "Maps row numbers to annotation names.", // @@ -281,8 +286,7 @@ public List getParameterTypes() { } types.add(type); - type = new ParameterTypeDateFormat(PARAMETER_DATE_FORMAT, - "The parse format of the date values, for example \"yyyy/MM/dd\".", false); + type = new ParameterTypeDateFormat(); type.setExpert(false); types.add(type); @@ -325,11 +329,20 @@ public List getParameterTypes() { return types; } + /** + * @return whether attribute names should be trimmed when parsing or not. + * @since 8.1.1 + */ + public boolean shouldTrimAttributeNames() { + return getCompatibilityLevel().isAbove(DataResultSetTranslator.BEFORE_ATTRIBUTE_TRIMMING); + } + @Override public OperatorVersion[] getIncompatibleVersionChanges() { OperatorVersion[] changes = super.getIncompatibleVersionChanges(); - changes = Arrays.copyOf(changes, changes.length + 1); - changes[changes.length - 1] = DataResultSetTranslator.VERSION_6_0_3; + changes = Arrays.copyOf(changes, changes.length + 2); + changes[changes.length - 2] = DataResultSetTranslator.VERSION_6_0_3; + changes[changes.length - 1] = DataResultSetTranslator.BEFORE_ATTRIBUTE_TRIMMING; return changes; } diff --git a/src/main/java/com/rapidminer/operator/nio/model/ColumnMetaData.java b/src/main/java/com/rapidminer/operator/nio/model/ColumnMetaData.java index e0a21d2c5..bae31ea40 100644 --- a/src/main/java/com/rapidminer/operator/nio/model/ColumnMetaData.java +++ b/src/main/java/com/rapidminer/operator/nio/model/ColumnMetaData.java @@ -41,19 +41,40 @@ public class ColumnMetaData extends Observable { private int attributeValueType; private String role; private boolean selected; + private boolean trimAttributeNames; + /** Creates an empty {@link ColumnMetaData} instance with no attribute trimming. */ public ColumnMetaData() { + this(false); + } + + /** + * Creates an empty {@link ColumnMetaData} instance with the given attribute trimming. + * @since 8.1.1 + */ + public ColumnMetaData(boolean trimAttributeNames) { + this.trimAttributeNames = trimAttributeNames; + } + /** Creates a {@link ColumnMetaData} instance with the specified parameters and no attribute trimming. */ + public ColumnMetaData(String originalAttributeName, String userDefinedAttributeName, int attributeValueType, String role, boolean selected) { + this(originalAttributeName, userDefinedAttributeName, attributeValueType, role, selected, false); } - public ColumnMetaData(String originalAttributeName, String userDefinedAttributeName, int attributeValueType, - String role, boolean selected) { - super(); + /** + * Creates a {@link ColumnMetaData} instance with the specified parameters and the given attribute trimming. + * @since 8.1.1 + */ + public ColumnMetaData(String originalAttributeName, String userDefinedAttributeName, int attributeValueType, String role, boolean selected, boolean trimAttributeNames) { this.originalAttributeName = originalAttributeName; + if (trimAttributeNames) { + userDefinedAttributeName = userDefinedAttributeName == null ? null : userDefinedAttributeName.trim(); + } this.userDefinedAttributeName = userDefinedAttributeName; this.attributeValueType = attributeValueType; this.role = role; this.selected = selected; + this.trimAttributeNames = trimAttributeNames; } /** Used to inform the validator about the ColumnMetaData object **/ @@ -80,6 +101,9 @@ public String getUserDefinedAttributeName() { } public void setUserDefinedAttributeName(String userDefinedAttributeName) { + if (trimAttributeNames) { + userDefinedAttributeName = userDefinedAttributeName == null ? null : userDefinedAttributeName.trim(); + } if (equals(this.userDefinedAttributeName, userDefinedAttributeName)) { return; } diff --git a/src/main/java/com/rapidminer/operator/nio/model/DataResultSetTranslationConfiguration.java b/src/main/java/com/rapidminer/operator/nio/model/DataResultSetTranslationConfiguration.java index 761ba64b6..302a04213 100644 --- a/src/main/java/com/rapidminer/operator/nio/model/DataResultSetTranslationConfiguration.java +++ b/src/main/java/com/rapidminer/operator/nio/model/DataResultSetTranslationConfiguration.java @@ -20,15 +20,14 @@ import static com.rapidminer.operator.nio.model.AbstractDataResultSetReader.ANNOTATION_NAME; import static com.rapidminer.operator.nio.model.AbstractDataResultSetReader.PARAMETER_ANNOTATIONS; -import static com.rapidminer.operator.nio.model.AbstractDataResultSetReader.PARAMETER_DATE_FORMAT; import static com.rapidminer.operator.nio.model.AbstractDataResultSetReader.PARAMETER_FIRST_ROW_AS_NAMES; -import static com.rapidminer.operator.nio.model.AbstractDataResultSetReader.PARAMETER_READ_AS_POLYNOMINAL; import static com.rapidminer.operator.nio.model.AbstractDataResultSetReader.PARAMETER_LOCALE; import static com.rapidminer.operator.nio.model.AbstractDataResultSetReader.PARAMETER_META_DATA; +import static com.rapidminer.operator.nio.model.AbstractDataResultSetReader.PARAMETER_READ_AS_POLYNOMINAL; +import static com.rapidminer.parameter.ParameterTypeDateFormat.PARAMETER_DATE_FORMAT; import java.text.DateFormat; import java.text.NumberFormat; -import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; @@ -45,10 +44,13 @@ import com.rapidminer.example.table.DataRowFactory; import com.rapidminer.operator.Annotations; import com.rapidminer.operator.OperatorException; +import com.rapidminer.operator.OperatorRuntimeException; +import com.rapidminer.operator.UserError; import com.rapidminer.operator.generator.ExampleSetGenerator; import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; import com.rapidminer.operator.ports.metadata.MDInteger; import com.rapidminer.operator.preprocessing.filter.AbstractDateDataProcessing; +import com.rapidminer.parameter.ParameterTypeDateFormat; import com.rapidminer.parameter.ParameterTypeList; import com.rapidminer.parameter.ParameterTypeTupel; import com.rapidminer.parameter.UndefinedParameterError; @@ -78,6 +80,8 @@ public class DataResultSetTranslationConfiguration { private int dataManagementType = DataRowFactory.TYPE_DOUBLE_ARRAY; + private boolean trimAttributeNames = true; + /** * This constructor can be used to generate an empty configuration just depending on the given * resultSet @@ -87,6 +91,9 @@ public class DataResultSetTranslationConfiguration { */ public DataResultSetTranslationConfiguration(AbstractDataResultSetReader readerOperator) { this(readerOperator, null); + if (readerOperator != null) { + trimAttributeNames = readerOperator.shouldTrimAttributeNames(); + } } /** @@ -182,7 +189,7 @@ public void reconfigure(DataResultSet dataResultSet) { int[] attributeValueTypes = dataResultSet.getValueTypes(); for (int i = 0; i < numberOfColumns; i++) { columnMetaData[i] = new ColumnMetaData(originalColumnNames[i], originalColumnNames[i], - attributeValueTypes[i], Attributes.ATTRIBUTE_NAME, true); + attributeValueTypes[i], Attributes.ATTRIBUTE_NAME, true, trimAttributeNames); } } } @@ -310,23 +317,27 @@ public void resetValueTypes() { } } - public DateFormat getDateFormat() { + public DateFormat getDateFormat() throws OperatorException { if (dateFormat == null) { - this.dateFormat = new ThreadLocal() { - - @Override - protected DateFormat initialValue() { - if (datePattern == null || datePattern.trim().isEmpty()) { - // clone because getDateInstance uses an internal pool which can return the - // same instance for multiple threads - return (DateFormat) DateFormat.getDateTimeInstance().clone(); - } else { - return new SimpleDateFormat(getDatePattern(), locale); + this.dateFormat = ThreadLocal.withInitial(() -> { + if (datePattern == null || datePattern.trim().isEmpty()) { + // clone because getDateInstance uses an internal pool which can return the + // same instance for multiple threads + return (DateFormat) DateFormat.getDateTimeInstance().clone(); + } else { + try { + return ParameterTypeDateFormat.createCheckedDateFormat(getDatePattern(), locale); + } catch (UserError userError) { + throw new DateFormatRuntimeException(userError); } } - }; + }); + } + try { + return this.dateFormat.get(); + } catch (DateFormatRuntimeException dfre) { + throw dfre.toOperatorException(); } - return this.dateFormat.get(); } public String getDatePattern() { @@ -395,10 +406,11 @@ public static ColumnMetaData[] readColumnMetaData(AbstractDataResultSetReader re } // initialize with values from settings ColumnMetaData[] columnMetaData = new ColumnMetaData[maxUsedColumnIndex + 1]; + boolean trimAttributeNames = readerOperator.shouldTrimAttributeNames(); for (String[] metaDataDefinition : metaDataSettings) { int currentColumn = Integer.parseInt(metaDataDefinition[0]); String[] metaDataDefintionValues = ParameterTypeTupel.transformString2Tupel(metaDataDefinition[1]); - columnMetaData[currentColumn] = new ColumnMetaData(); + columnMetaData[currentColumn] = new ColumnMetaData(trimAttributeNames); final ColumnMetaData cmd = columnMetaData[currentColumn]; cmd.setSelected(Boolean.parseBoolean(metaDataDefintionValues[1])); if (cmd.isSelected()) { // otherwise details don't matter @@ -419,9 +431,29 @@ public static ColumnMetaData[] readColumnMetaData(AbstractDataResultSetReader re // is at least not null) for (int i = 0; i < columnMetaData.length; i++) { if (columnMetaData[i] == null) { - columnMetaData[i] = new ColumnMetaData(); + columnMetaData[i] = new ColumnMetaData(trimAttributeNames); } } return columnMetaData; } + + /** + * Simple wrapper class for a {@link UserError}. + * + * @author Jan Czogalla + * @since 8.2 + */ + private static class DateFormatRuntimeException extends IllegalArgumentException implements OperatorRuntimeException { + + private final UserError userError; + + private DateFormatRuntimeException(UserError userError) { + this.userError = userError; + } + + @Override + public OperatorException toOperatorException() { + return userError; + } + } } diff --git a/src/main/java/com/rapidminer/operator/nio/model/DataResultSetTranslator.java b/src/main/java/com/rapidminer/operator/nio/model/DataResultSetTranslator.java index 7853400d3..55c7d1281 100644 --- a/src/main/java/com/rapidminer/operator/nio/model/DataResultSetTranslator.java +++ b/src/main/java/com/rapidminer/operator/nio/model/DataResultSetTranslator.java @@ -104,6 +104,11 @@ private boolean register(String value) { */ public static final OperatorVersion VERSION_6_0_3 = new OperatorVersion(6, 0, 3); + /** + * From this version, attribute names will be trimmed on read/import + */ + public static final OperatorVersion BEFORE_ATTRIBUTE_TRIMMING = new OperatorVersion(8, 1, 0); + private Operator operator; public DataResultSetTranslator(Operator operator) { @@ -378,7 +383,11 @@ private double getStringIndex(Attribute attribute, DataResultSet dataResultSet, private String getString(DataResultSet dataResultSet, int row, int column, boolean isFaultTolerant) throws UserError { try { - return dataResultSet.getString(column); + String string = dataResultSet.getString(column); + if (operator.getCompatibilityLevel().isAbove(BEFORE_ATTRIBUTE_TRIMMING)) { + string = string == null ? null : string.trim(); + } + return string; } catch (com.rapidminer.operator.nio.model.ParseException e) { addOrThrow(isFaultTolerant, e.getError(), row); return null; diff --git a/src/main/java/com/rapidminer/operator/nio/model/Excel2007ResultSet.java b/src/main/java/com/rapidminer/operator/nio/model/Excel2007ResultSet.java index 92093c34d..3eeda44bc 100644 --- a/src/main/java/com/rapidminer/operator/nio/model/Excel2007ResultSet.java +++ b/src/main/java/com/rapidminer/operator/nio/model/Excel2007ResultSet.java @@ -42,6 +42,7 @@ import com.rapidminer.operator.OperatorException; import com.rapidminer.operator.UserError; import com.rapidminer.operator.nio.model.xlsx.XlsxResultSet; +import com.rapidminer.parameter.ParameterTypeDateFormat; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.ProgressListener; @@ -273,9 +274,8 @@ public String[] getColumnNames() { public boolean isMissing(int columnIndex) { Cell cell = getCurrentCell(columnIndex); try { - boolean missing = cell == null || cell.getCellType() == Cell.CELL_TYPE_BLANK + return cell == null || cell.getCellType() == Cell.CELL_TYPE_BLANK || cell.getCellType() == Cell.CELL_TYPE_ERROR || "".equals(cell.getStringCellValue().trim()); - return missing; } catch (IllegalStateException e) { return false; } @@ -323,18 +323,20 @@ public Date getDate(int columnIndex) throws ParseException { return null; } if (cell.getCellType() == Cell.CELL_TYPE_NUMERIC) { - Date dateCellValue = cell.getDateCellValue(); - return dateCellValue; + return cell.getDateCellValue(); } else { try { String valueString = cell.getStringCellValue(); try { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat); + SimpleDateFormat simpleDateFormat = ParameterTypeDateFormat.createCheckedDateFormat(dateFormat, null); simpleDateFormat.setTimeZone(TimeZone.getTimeZone(this.timeZone)); return simpleDateFormat.parse(valueString); } catch (java.text.ParseException e) { throw new ParseException(new ParsingError(currentRow, columnIndex, ParsingError.ErrorCode.UNPARSEABLE_DATE, valueString)); + } catch (UserError userError) { + throw new ParseException(new ParsingError(currentRow, columnIndex, + ParsingError.ErrorCode.UNPARSEABLE_DATE, userError.getMessage())); } } catch (IllegalStateException e) { throw new ParseException(new ParsingError(currentRow, columnIndex, ParsingError.ErrorCode.UNPARSEABLE_DATE, @@ -350,8 +352,7 @@ public String getString(int columnIndex) { return ""; } if (cell.getCellType() == Cell.CELL_TYPE_NUMERIC) { - String value = String.valueOf(cell.getNumericCellValue()); - return value; + return String.valueOf(cell.getNumericCellValue()); } else if (cell.getCellType() == Cell.CELL_TYPE_FORMULA) { String value; if (cell.getCachedFormulaResultType() == Cell.CELL_TYPE_NUMERIC) { @@ -362,8 +363,7 @@ public String getString(int columnIndex) { return value; } else { try { - String value = cell.getStringCellValue(); - return value; + return cell.getStringCellValue(); } catch (IllegalStateException e) { return ""; } diff --git a/src/main/java/com/rapidminer/operator/nio/model/ExcelResultSet.java b/src/main/java/com/rapidminer/operator/nio/model/ExcelResultSet.java index fa7d1f7bb..533707cc7 100644 --- a/src/main/java/com/rapidminer/operator/nio/model/ExcelResultSet.java +++ b/src/main/java/com/rapidminer/operator/nio/model/ExcelResultSet.java @@ -21,7 +21,6 @@ import java.io.File; import java.io.IOException; import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.LinkedList; @@ -33,6 +32,7 @@ import com.rapidminer.operator.OperatorException; import com.rapidminer.operator.ProcessStoppedException; import com.rapidminer.operator.UserError; +import com.rapidminer.parameter.ParameterTypeDateFormat; import com.rapidminer.tools.ProgressListener; import com.rapidminer.tools.Tools; @@ -184,32 +184,21 @@ public ExcelResultSet(Operator callingOperator, final ExcelResultSetConfiguratio final String timezone = configuration.getTimezone(); if (dateFormatProvider != null) { if (timezone != null) { - this.dateFormatProvider = new DateFormatProvider() { - - @Override - public DateFormat geDateFormat() { - DateFormat format = dateFormatProvider.geDateFormat(); - format.setTimeZone(TimeZone.getTimeZone(timezone)); - return format; - } - + this.dateFormatProvider = () -> { + DateFormat format = dateFormatProvider.geDateFormat(); + format.setTimeZone(TimeZone.getTimeZone(timezone)); + return format; }; } else { this.dateFormatProvider = dateFormatProvider; } } else { String datePattern = configuration.getDatePattern(); - final DateFormat dateFormat = datePattern == null ? new SimpleDateFormat() : new SimpleDateFormat(datePattern); + final DateFormat dateFormat = ParameterTypeDateFormat.createCheckedDateFormat(callingOperator, (datePattern == null ? "" : datePattern)); if (timezone != null) { dateFormat.setTimeZone(TimeZone.getTimeZone(timezone)); } - this.dateFormatProvider = new DateFormatProvider() { - - @Override - public DateFormat geDateFormat() { - return dateFormat; - } - }; + this.dateFormatProvider = () -> dateFormat; } if (callingOperator != null) { @@ -335,8 +324,13 @@ public Date getDate(int columnIndex) throws ParseException { return date; } else { String valueString = cell.getContents(); + DateFormat dateFormat = dateFormatProvider.geDateFormat(); + if (dateFormat == null) { + throw new ParseException( + new ParsingError(currentRow, columnIndex, ParsingError.ErrorCode.UNPARSEABLE_DATE, "illegal date format pattern")); + } try { - return dateFormatProvider.geDateFormat().parse(valueString); + return dateFormat.parse(valueString); } catch (java.text.ParseException e) { throw new ParseException( new ParsingError(currentRow, columnIndex, ParsingError.ErrorCode.UNPARSEABLE_DATE, valueString)); diff --git a/src/main/java/com/rapidminer/operator/nio/model/ExcelResultSetConfiguration.java b/src/main/java/com/rapidminer/operator/nio/model/ExcelResultSetConfiguration.java index f7e67fe93..c9295da43 100644 --- a/src/main/java/com/rapidminer/operator/nio/model/ExcelResultSetConfiguration.java +++ b/src/main/java/com/rapidminer/operator/nio/model/ExcelResultSetConfiguration.java @@ -44,6 +44,7 @@ import com.rapidminer.operator.nio.model.xlsx.XlsxWorkbookParser; import com.rapidminer.operator.nio.model.xlsx.XlsxWorkbookParser.XlsxWorkbook; import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; +import com.rapidminer.parameter.ParameterTypeDateFormat; import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.tools.ProgressListener; import com.rapidminer.tools.Tools; @@ -119,8 +120,8 @@ public ExcelResultSetConfiguration(ExcelExampleSource excelExampleSource) throws } } - if (excelExampleSource.isParameterSet(AbstractDataResultSetReader.PARAMETER_DATE_FORMAT)) { - datePattern = excelExampleSource.getParameterAsString(AbstractDataResultSetReader.PARAMETER_DATE_FORMAT); + if (excelExampleSource.isParameterSet(ParameterTypeDateFormat.PARAMETER_DATE_FORMAT)) { + datePattern = excelExampleSource.getParameterAsString(ParameterTypeDateFormat.PARAMETER_DATE_FORMAT); } if (excelExampleSource.isParameterSet(AbstractDataResultSetReader.PARAMETER_TIME_ZONE)) { diff --git a/src/main/java/com/rapidminer/operator/nio/model/xlsx/XlsxResultSet.java b/src/main/java/com/rapidminer/operator/nio/model/xlsx/XlsxResultSet.java index c19bd86c6..72d6d5cad 100644 --- a/src/main/java/com/rapidminer/operator/nio/model/xlsx/XlsxResultSet.java +++ b/src/main/java/com/rapidminer/operator/nio/model/xlsx/XlsxResultSet.java @@ -25,7 +25,6 @@ import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.text.DecimalFormat; -import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; import java.util.LinkedList; @@ -49,12 +48,13 @@ import com.rapidminer.operator.nio.model.DataResultSet; import com.rapidminer.operator.nio.model.DateFormatProvider; import com.rapidminer.operator.nio.model.ExcelResultSetConfiguration; +import com.rapidminer.operator.nio.model.ExcelSheetSelection; import com.rapidminer.operator.nio.model.ParseException; import com.rapidminer.operator.nio.model.ParsingError; -import com.rapidminer.operator.nio.model.ExcelSheetSelection; import com.rapidminer.operator.nio.model.xlsx.XlsxUtilities.XlsxCell; import com.rapidminer.operator.nio.model.xlsx.XlsxWorkbookParser.XlsxWorkbook; import com.rapidminer.operator.nio.model.xlsx.XlsxWorkbookRelationParser.XlsxWorkbookRel; +import com.rapidminer.parameter.ParameterTypeDateFormat; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.ProgressListener; @@ -73,7 +73,7 @@ public class XlsxResultSet implements DataResultSet { /** * Defines whether the Excel file is read by the operator or by the Wizard. */ - public static enum XlsxReadMode { + public enum XlsxReadMode { WIZARD_WORKPANE, WIZARD_PREVIEW, OPERATOR, /** * Specifies that the {@link XlsxResultSet} was created to display preview content in the @@ -263,32 +263,21 @@ public XlsxResultSet(Operator callingOperator, final ExcelResultSetConfiguration final String timezone = configuration.getTimezone(); if (provider != null) { if (timezone != null) { - this.dateFormatProvider = new DateFormatProvider() { - - @Override - public DateFormat geDateFormat() { - DateFormat format = provider.geDateFormat(); - format.setTimeZone(TimeZone.getTimeZone(timezone)); - return null; - } - + this.dateFormatProvider = () -> { + DateFormat format = provider.geDateFormat(); + format.setTimeZone(TimeZone.getTimeZone(timezone)); + return format; }; } else { this.dateFormatProvider = provider; } } else { String datePattern = configuration.getDatePattern(); - final DateFormat dateFormat = new SimpleDateFormat(datePattern == null ? "" : datePattern); + final DateFormat dateFormat = ParameterTypeDateFormat.createCheckedDateFormat(callingOperator, (datePattern == null ? "" : datePattern)); if (timezone != null) { dateFormat.setTimeZone(TimeZone.getTimeZone(timezone)); } - this.dateFormatProvider = new DateFormatProvider() { - - @Override - public DateFormat geDateFormat() { - return dateFormat; - } - }; + this.dateFormatProvider = () -> dateFormat; } if (callingOperator != null) { @@ -354,11 +343,7 @@ public int getCurrentRow() { public boolean isMissing(int columnIndex) { String value = getValue(columnIndex); XlsxCellType cellType = getCellType(columnIndex); - if (value == null || value.trim().isEmpty() || XlsxCellType.ERROR.equals(cellType)) { - return true; - } else { - return false; - } + return value == null || value.trim().isEmpty() || XlsxCellType.ERROR.equals(cellType); } @Override diff --git a/src/main/java/com/rapidminer/operator/nio/model/xlsx/XlsxUtilities.java b/src/main/java/com/rapidminer/operator/nio/model/xlsx/XlsxUtilities.java index 849712f1c..8baf4dfd3 100644 --- a/src/main/java/com/rapidminer/operator/nio/model/xlsx/XlsxUtilities.java +++ b/src/main/java/com/rapidminer/operator/nio/model/xlsx/XlsxUtilities.java @@ -26,6 +26,7 @@ import org.xml.sax.helpers.AttributesImpl; import com.rapidminer.operator.nio.ImportWizardUtils; +import com.rapidminer.tools.Tools; /** @@ -283,7 +284,7 @@ public static int convertToColumnIndex(String columnName) { /** * Convert given Excel column index to column name, eg. '0=A', '26=AA' * - * @param columnIndex + * @param index * the {@code 0} based column index * @return the column name */ @@ -291,14 +292,7 @@ public static String convertToColumnName(int index) { if (index < 0) { throw new IllegalArgumentException("Indices below 0 are not allowed"); } - StringBuilder sb = new StringBuilder(); - // increase by 1 as algorithm expects a 1 as starting point - index++; - while (index-- > 0) { - sb.append((char) ('A' + index % 26)); - index /= 26; - } - return sb.reverse().toString(); + return Tools.getExcelColumnName(index); } /** diff --git a/src/main/java/com/rapidminer/operator/performance/PerformanceCriterion.java b/src/main/java/com/rapidminer/operator/performance/PerformanceCriterion.java index 48c680116..2c48cab25 100644 --- a/src/main/java/com/rapidminer/operator/performance/PerformanceCriterion.java +++ b/src/main/java/com/rapidminer/operator/performance/PerformanceCriterion.java @@ -18,6 +18,10 @@ */ package com.rapidminer.operator.performance; +import java.util.Objects; + +import com.rapidminer.operator.UserError; +import com.rapidminer.operator.WrapperOperatorRuntimeException; import com.rapidminer.tools.math.Averagable; @@ -100,11 +104,15 @@ public double getMaxFitness() { */ @Override public int compareTo(PerformanceCriterion o) { - if (!this.getClass().equals(o.getClass())) { - throw new RuntimeException("Mismatched criterion class:" + this.getClass() + ", " + o.getClass()); - } - if (!o.getName().equals(this.getName())) { - throw new RuntimeException("Mismatched criterion type:" + this.getName() + ", " + o.getName()); + Class aClass = this.getClass(); + Class oClass = o.getClass(); + boolean classesDiffer = aClass != oClass; + String aType = this.getName(); + String oType = o.getName(); + if (classesDiffer || !Objects.equals(aType, oType)) { + throw new WrapperOperatorRuntimeException(new UserError(null, + "performance_criterion_" + (classesDiffer ? "class" : "type") + "_mismatch", + classesDiffer ? aClass.getName() : aType, classesDiffer ? oClass.getName() : oType)); } return Double.compare(this.getFitness(), o.getFitness()); } diff --git a/src/main/java/com/rapidminer/operator/performance/cost/CostEvaluator.java b/src/main/java/com/rapidminer/operator/performance/cost/CostEvaluator.java index dde142325..dc6d8973b 100644 --- a/src/main/java/com/rapidminer/operator/performance/cost/CostEvaluator.java +++ b/src/main/java/com/rapidminer/operator/performance/cost/CostEvaluator.java @@ -18,6 +18,10 @@ */ package com.rapidminer.operator.performance.cost; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import com.rapidminer.example.Attribute; import com.rapidminer.example.Attributes; import com.rapidminer.example.Example; @@ -40,10 +44,6 @@ import com.rapidminer.parameter.ParameterTypeString; import com.rapidminer.tools.Ontology; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** * This operator provides the ability to evaluate classification costs. Therefore a cost matrix @@ -149,9 +149,11 @@ public List getParameterTypes() { "Indicates if the example set should be kept.", false); type.setHidden(true); types.add(type); - types.add(new ParameterTypeMatrix(PARAMETER_COST_MATRIX, + type = new ParameterTypeMatrix(PARAMETER_COST_MATRIX, "The matrix of missclassification costs. Columns and Rows in order of internal mapping.", "Cost Matrix", - "Predicted Class", "True Class", true, false)); + "Predicted Class", "True Class", true, false); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeEnumeration( PARAMETER_CLASS_DEFINITION, "With this parameter it is possible to define the order of classes used in the cost matrix. First class in this list is First class in the matrix.", diff --git a/src/main/java/com/rapidminer/operator/performance/cost/RankingCriterion.java b/src/main/java/com/rapidminer/operator/performance/cost/RankingCriterion.java index ecaaac68a..45c71f57e 100644 --- a/src/main/java/com/rapidminer/operator/performance/cost/RankingCriterion.java +++ b/src/main/java/com/rapidminer/operator/performance/cost/RankingCriterion.java @@ -88,7 +88,7 @@ public void countExample(Example example) { while (intervallIndex < rankIntervallStarts.length - 1 && rankIntervallStarts[intervallIndex + 1] <= rank) { intervallIndex++; } - if (rank >= rankIntervallStarts[0]) { + if (rankIntervallStarts.length > 0 && rank >= rankIntervallStarts[0]) { // otherwise not defined costs: Assume 0 costs += rankIntervallCost[intervallIndex]; } diff --git a/src/main/java/com/rapidminer/operator/performance/cost/RankingEvaluator.java b/src/main/java/com/rapidminer/operator/performance/cost/RankingEvaluator.java index 576d7e7f5..4a8d0a616 100644 --- a/src/main/java/com/rapidminer/operator/performance/cost/RankingEvaluator.java +++ b/src/main/java/com/rapidminer/operator/performance/cost/RankingEvaluator.java @@ -18,6 +18,10 @@ */ package com.rapidminer.operator.performance.cost; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; + import com.rapidminer.example.Attributes; import com.rapidminer.example.Example; import com.rapidminer.example.ExampleSet; @@ -25,6 +29,8 @@ import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; +import com.rapidminer.operator.ProcessSetupError; +import com.rapidminer.operator.SimpleProcessSetupError; import com.rapidminer.operator.ValueDouble; import com.rapidminer.operator.performance.MeasuredPerformance; import com.rapidminer.operator.performance.PerformanceCriterion; @@ -32,14 +38,16 @@ import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.OutputPort; import com.rapidminer.operator.ports.metadata.ExampleSetPrecondition; +import com.rapidminer.operator.ports.quickfix.ParameterSettingQuickFix; +import com.rapidminer.operator.ports.quickfix.QuickFix; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeDouble; import com.rapidminer.parameter.ParameterTypeInt; import com.rapidminer.parameter.ParameterTypeList; +import com.rapidminer.parameter.UndefinedParameterError; +import com.rapidminer.tools.LogService; import com.rapidminer.tools.Ontology; -import java.util.List; - /** * This operator provides the ability to evaluate classification costs. Therefore a cost matrix @@ -115,18 +123,39 @@ public void doWork() throws OperatorException { performanceOutput.deliver(performance); } + @Override + public int checkProperties() { + int errorCount = super.checkProperties(); + if (isEnabled()) { + try { + List rankings = getParameterList(PARAMETER_RANKING_COSTS); + if (rankings.isEmpty()) { + List quickFixes = Collections.singletonList(new ParameterSettingQuickFix(this, PARAMETER_RANKING_COSTS)); + addError(new SimpleProcessSetupError(ProcessSetupError.Severity.ERROR, getPortOwner(), quickFixes, + "parameter_list_undefined", PARAMETER_RANKING_COSTS.replace('_', ' '))); + errorCount++; + } + } catch (UndefinedParameterError pe) { + LogService.getRoot().log(Level.WARNING, "com.rapidminer.operator.performance.cost.RankingEvaluator.parameter_undefined", PARAMETER_RANKING_COSTS); + } + } + return errorCount; + } + @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeList( + ParameterType type = new ParameterTypeList( PARAMETER_RANKING_COSTS, "This parameter defines the costs when the real call isn't the one with the highest confidence.", new ParameterTypeInt( PARAMETER_RANK_START, "This is the first rank of the interval between this and the nearest greater defined rank. Each of these ranks get assigned this value. Rank counting starts with 0.", 0, Integer.MAX_VALUE), new ParameterTypeDouble(PARAMETER_RANK_COST, - "This is the cost of all ranks within this range.", Double.NEGATIVE_INFINITY, - Double.POSITIVE_INFINITY), false)); + "This is the cost of all ranks within this range.", Double.NEGATIVE_INFINITY, + Double.POSITIVE_INFINITY), false); + type.setPrimary(true); + types.add(type); return types; } } diff --git a/src/main/java/com/rapidminer/operator/ports/metadata/InputMissingMetaDataError.java b/src/main/java/com/rapidminer/operator/ports/metadata/InputMissingMetaDataError.java index fffe86d06..de0cbf431 100644 --- a/src/main/java/com/rapidminer/operator/ports/metadata/InputMissingMetaDataError.java +++ b/src/main/java/com/rapidminer/operator/ports/metadata/InputMissingMetaDataError.java @@ -18,16 +18,15 @@ */ package com.rapidminer.operator.ports.metadata; +import java.util.LinkedList; +import java.util.List; + import com.rapidminer.operator.IOObject; import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.OutputPort; -import com.rapidminer.operator.ports.quickfix.AddCompatibleOperatorQuickFix; import com.rapidminer.operator.ports.quickfix.ConnectToQuickFix; import com.rapidminer.operator.ports.quickfix.QuickFix; -import java.util.LinkedList; -import java.util.List; - /** * Indicates that input for a port was missing or had the wrong type. @@ -64,7 +63,6 @@ public List getQuickFixes() { } } - fixes.add(new AddCompatibleOperatorQuickFix(inputPort, desiredClass)); } return fixes; } diff --git a/src/main/java/com/rapidminer/operator/ports/metadata/ProcessNotInRepositoryMetaDataError.java b/src/main/java/com/rapidminer/operator/ports/metadata/ProcessNotInRepositoryMetaDataError.java new file mode 100644 index 000000000..6311d0345 --- /dev/null +++ b/src/main/java/com/rapidminer/operator/ports/metadata/ProcessNotInRepositoryMetaDataError.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.operator.ports.metadata; + +import java.util.Collections; +import java.util.List; + +import com.rapidminer.operator.ports.Port; +import com.rapidminer.operator.ports.quickfix.QuickFix; + + +/** + * An Error that occurs if the process is not registered to a repository but relative repository paths are used. + * + * @author Marco Boeck + * @since 8.2 + */ +public class ProcessNotInRepositoryMetaDataError extends SimpleMetaDataError { + + /** + * Constructor for an error. Please note, that the i18nKey will be appended to "metadata.error." + * to form the final key. + */ + public ProcessNotInRepositoryMetaDataError(Severity severity, Port port, String i18nKey, Object... i18nArgs) { + super(severity, port, Collections.emptyList(), i18nKey, i18nArgs); + } + + /** + * Constructor for an error. Please note, that the i18nKey will be appended to "metadata.error." + * to form the final key. + */ + public ProcessNotInRepositoryMetaDataError(Severity severity, Port port, List fixes, String i18nKey, Object... args) { + super(severity, port, fixes, i18nKey, args); + } + +} diff --git a/src/main/java/com/rapidminer/operator/ports/quickfix/AttributeToNominalQuickFixProvider.java b/src/main/java/com/rapidminer/operator/ports/quickfix/AttributeToNominalQuickFixProvider.java new file mode 100644 index 000000000..4bc05abb4 --- /dev/null +++ b/src/main/java/com/rapidminer/operator/ports/quickfix/AttributeToNominalQuickFixProvider.java @@ -0,0 +1,112 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.operator.ports.quickfix; + +import java.util.LinkedList; +import java.util.List; + +import com.rapidminer.operator.Operator; +import com.rapidminer.operator.OperatorCreationException; +import com.rapidminer.operator.ports.InputPort; +import com.rapidminer.operator.ports.metadata.AttributeMetaData; +import com.rapidminer.operator.preprocessing.filter.Date2Nominal; +import com.rapidminer.operator.preprocessing.filter.NumericToPolynominal; +import com.rapidminer.operator.preprocessing.filter.attributes.SingleAttributeFilter; +import com.rapidminer.operator.tools.AttributeSubsetSelector; +import com.rapidminer.parameter.ParameterTypeDateFormat; +import com.rapidminer.tools.OperatorService; + + +/** + * Provides DateTime and Numerical to Nominal quick fixes for a single attribute + * + * @author Jonas Wilms-Pfau + * @since 8.2.0 + */ +public final class AttributeToNominalQuickFixProvider { + + /** Rating used in case multiple quick fixes are available */ + private static final int DEFAULT_RATING = 1; + + /** + * Prevent utility class instantiation. + */ + private AttributeToNominalQuickFixProvider(){ + throw new AssertionError("Utility class"); + } + + /** + * Returns a QuickFix for a Numerical or DateTime label to Nominal label + * + * @param exampleSetInput the input port where the quickfix should be applied + * @param labelMD the label that should be converted to nominal + * @return list containing the quickfix if available + */ + public static List labelToNominal(InputPort exampleSetInput, AttributeMetaData labelMD) { + return attributeToNominal(exampleSetInput, labelMD, DEFAULT_RATING, "insert_to_nominal_label"); + } + + /*** + * Returns a QuickFix for Numerical and DateTime Attributes to Nominal + * + * @param exampleSetInput the input port where the quickfix should be applied + * @param attributeMD the attribute that should be converted + * @param rating defines the position in the quickfix list (default 1) + * @param i18nKey the i18n key used for the QuickFix + * @param i18nArgs i18n arguments + * @return list containing the quickfix if available + */ + public static List attributeToNominal(InputPort exampleSetInput, AttributeMetaData attributeMD, int rating, String i18nKey, Object... i18nArgs) { + + LinkedList quickFixes = new LinkedList<>(); + + if (attributeMD.isNumerical()) { + QuickFix numericalQuickFix = new OperatorInsertionQuickFix(i18nKey, + i18nArgs, rating, exampleSetInput) { + + @Override + public Operator createOperator() throws OperatorCreationException { + NumericToPolynominal operator = OperatorService.createOperator(NumericToPolynominal.class); + if (attributeMD.isSpecial()) { + operator.setParameter(AttributeSubsetSelector.PARAMETER_INCLUDE_SPECIAL_ATTRIBUTES, "true"); + } + operator.setParameter(AttributeSubsetSelector.PARAMETER_FILTER_TYPE, AttributeSubsetSelector.CONDITION_NAMES[AttributeSubsetSelector.CONDITION_SINGLE]); + operator.setParameter(SingleAttributeFilter.PARAMETER_ATTRIBUTE, attributeMD.getName()); + return operator; + } + }; + quickFixes.add(numericalQuickFix); + } else if (attributeMD.isDateTime()) { + QuickFix dateTimeQuickFix = new OperatorInsertionQuickFix(i18nKey, + i18nArgs, rating, exampleSetInput) { + + @Override + public Operator createOperator() throws OperatorCreationException { + Date2Nominal operator = OperatorService.createOperator(Date2Nominal.class); + operator.setParameter(Date2Nominal.PARAMETER_ATTRIBUTE_NAME, attributeMD.getName()); + operator.setParameter(ParameterTypeDateFormat.PARAMETER_DATE_FORMAT, ParameterTypeDateFormat.DATE_TIME_FORMAT_ISO8601_UTC_MS); + return operator; + } + }; + quickFixes.add(dateTimeQuickFix); + } + return quickFixes; + } + +} diff --git a/src/main/java/com/rapidminer/operator/ports/quickfix/ConnectLastOperatorToOutputPortsQuickFix.java b/src/main/java/com/rapidminer/operator/ports/quickfix/ConnectLastOperatorToOutputPortsQuickFix.java new file mode 100644 index 000000000..dbcdd33a2 --- /dev/null +++ b/src/main/java/com/rapidminer/operator/ports/quickfix/ConnectLastOperatorToOutputPortsQuickFix.java @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.operator.ports.quickfix; + +import java.util.logging.Level; + +import com.rapidminer.operator.Operator; +import com.rapidminer.operator.ports.IncompatibleMDClassException; +import com.rapidminer.operator.ports.InputPort; +import com.rapidminer.operator.ports.InputPorts; +import com.rapidminer.operator.ports.OutputPort; +import com.rapidminer.operator.ports.PortException; +import com.rapidminer.operator.ports.metadata.MetaData; +import com.rapidminer.tools.LogService; + + +/** + * Connects the last operator in a process to the process result ports. + * + * @author Marco Boeck + * @since 8.2 + */ +public class ConnectLastOperatorToOutputPortsQuickFix extends AbstractQuickFix { + + private final Operator lastOperator; + + public ConnectLastOperatorToOutputPortsQuickFix(Operator lastOperator) { + super(MAX_RATING, false, "connect_to_result_ports"); + this.lastOperator = lastOperator; + } + + @Override + public void apply() { + connectPorts(true); + } + + /** + * Tries to connect the output ports of the last operator. + * + * @param skipPortsWithoutMetaData + * if {@code true}, ports that have no metadata will not be connected + */ + private void connectPorts(final boolean skipPortsWithoutMetaData) { + int index = 0; + InputPorts innerSinks = lastOperator.getProcess().getRootOperator().getSubprocess(0).getInnerSinks(); + for (OutputPort outputPort : lastOperator.getOutputPorts().getAllPorts()) { + if (!outputPort.isConnected() && outputPort.shouldAutoConnect()) { + try { + if (skipPortsWithoutMetaData && outputPort.getMetaData(MetaData.class) == null) { + continue; + } + InputPort inputPort; + // search first free input port + do { + // stop if no more free ports are available + // this will not stop for inner sinks that have a port extender, since those always add new ports + if (index >= innerSinks.getNumberOfPorts()) { + return; + } + inputPort = innerSinks.getPortByIndex(index++); + } while (inputPort.isConnected()); + outputPort.connectTo(inputPort); + } catch (PortException | IncompatibleMDClassException e) { + // cannot happen because the port is not connected yet and we use the top-level class - ignore + LogService.getRoot().log(Level.WARNING, "Error while connecting port", e); + } + } + } + + // nothing was connected, and we skipped ports w/o metadata? Try again once, this time connect ports w/o metadata + if (index == 0 && skipPortsWithoutMetaData) { + connectPorts(false); + } + } +} diff --git a/src/main/java/com/rapidminer/operator/ports/quickfix/ConnectToQuickFix.java b/src/main/java/com/rapidminer/operator/ports/quickfix/ConnectToQuickFix.java index c3cd6e5d9..44d1c0f17 100644 --- a/src/main/java/com/rapidminer/operator/ports/quickfix/ConnectToQuickFix.java +++ b/src/main/java/com/rapidminer/operator/ports/quickfix/ConnectToQuickFix.java @@ -31,8 +31,8 @@ public class ConnectToQuickFix extends AbstractQuickFix { private OutputPort outputPort; public ConnectToQuickFix(InputPort inputPort, OutputPort outputPort) { - super(MAX_RATING, false, inputPort.isConnected() ? "reconnect_to" : "connect_to", new Object[] { outputPort - .getSpec() }); + super(MAX_RATING, false, inputPort.isConnected() ? "reconnect_to" : "connect_to", outputPort + .getSpec()); this.inputPort = inputPort; this.outputPort = outputPort; } diff --git a/src/main/java/com/rapidminer/operator/ports/quickfix/ParameterSettingQuickFix.java b/src/main/java/com/rapidminer/operator/ports/quickfix/ParameterSettingQuickFix.java index 067c46d81..9dc69c792 100644 --- a/src/main/java/com/rapidminer/operator/ports/quickfix/ParameterSettingQuickFix.java +++ b/src/main/java/com/rapidminer/operator/ports/quickfix/ParameterSettingQuickFix.java @@ -18,6 +18,11 @@ */ package com.rapidminer.operator.ports.quickfix; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + import com.rapidminer.gui.properties.AttributesPropertyDialog; import com.rapidminer.gui.properties.ListPropertyDialog; import com.rapidminer.gui.tools.dialogs.SetParameterDialog; @@ -29,11 +34,6 @@ import com.rapidminer.parameter.ParameterTypeList; import com.rapidminer.parameter.UndefinedParameterError; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - /** * @author Sebastian Land @@ -45,13 +45,13 @@ public class ParameterSettingQuickFix extends AbstractQuickFix { private String value; public ParameterSettingQuickFix(Operator operator, String parameterName) { - this(operator, parameterName, null, "set_parameter", new Object[] { parameterName.replace('_', ' ') }); + this(operator, parameterName, null, "set_parameter", parameterName.replace('_', ' ')); ParameterType type = operator.getParameterType(parameterName); if (type instanceof ParameterTypeConfiguration) { seti18nKey("set_parameters_using_wizard"); } else if (type instanceof ParameterTypeList) { - seti18nKey("set_parameter_list", parameterName); + seti18nKey("set_parameter_list", parameterName.replace('_', ' ')); } } @@ -62,15 +62,15 @@ public ParameterSettingQuickFix(Operator operator, String parameterName, String if (type instanceof ParameterTypeConfiguration) { seti18nKey("correct_parameter_settings_with_wizard"); } else if (type instanceof ParameterTypeList) { - seti18nKey("correct_parameter_settings_list", parameterName); - } else { + seti18nKey("correct_parameter_settings_list", parameterName.replace('_', ' ')); } + if (value != null) { if (type instanceof ParameterTypeBoolean) { if (value.equals("true")) { - seti18nKey("correct_parameter_settings_boolean_enable", parameterName); + seti18nKey("correct_parameter_settings_boolean_enable", parameterName.replace('_', ' ')); } else { - seti18nKey("correct_parameter_settings_boolean_disable", parameterName); + seti18nKey("correct_parameter_settings_boolean_disable", parameterName.replace('_', ' ')); } } } @@ -111,7 +111,7 @@ public void apply() { try { list = operator.getParameterList(parameterName); } catch (UndefinedParameterError e) { - list = new LinkedList(); + list = new LinkedList<>(); } ListPropertyDialog dialog = new ListPropertyDialog((ParameterTypeList) type, list, operator); dialog.setVisible(true); @@ -120,7 +120,7 @@ public void apply() { } } else if (type instanceof ParameterTypeAttributes) { AttributesPropertyDialog dialog = new AttributesPropertyDialog((ParameterTypeAttributes) type, - Collections. emptyList()); + Collections.emptyList()); dialog.setVisible(true); if (dialog.isOk()) { boolean first = true; diff --git a/src/main/java/com/rapidminer/operator/ports/quickfix/SaveProcessQuickFix.java b/src/main/java/com/rapidminer/operator/ports/quickfix/SaveProcessQuickFix.java new file mode 100644 index 000000000..479e3f22d --- /dev/null +++ b/src/main/java/com/rapidminer/operator/ports/quickfix/SaveProcessQuickFix.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.operator.ports.quickfix; + +import com.rapidminer.Process; +import com.rapidminer.gui.actions.SaveAsAction; + + +/** + * Quick fix to save a process. + * + * @author Marco Boeck + * @since 8.2 + */ +public class SaveProcessQuickFix extends AbstractQuickFix { + + private Process process; + + /** + * This constructor will build a quickfix that let's the user save the given process. + */ + public SaveProcessQuickFix(Process process) { + this(process, "save_process", (Object[]) null); + } + + /** + * This constructor will build a quickfix that will automatically set the parameter to the given + * value without further user interaction. Use this constructor if you can comprehend the + * correct value. + */ + private SaveProcessQuickFix(Process process, String i18nKey, Object... i18nArgs) { + super(1, true, i18nKey, i18nArgs); + this.process = process; + } + + @Override + public void apply() { + SaveAsAction.saveAs(process, true); + } +} diff --git a/src/main/java/com/rapidminer/operator/preprocessing/AttributeSubsetPreprocessing.java b/src/main/java/com/rapidminer/operator/preprocessing/AttributeSubsetPreprocessing.java index 46d14c50e..a2db07141 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/AttributeSubsetPreprocessing.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/AttributeSubsetPreprocessing.java @@ -18,6 +18,12 @@ */ package com.rapidminer.operator.preprocessing; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + import com.rapidminer.example.Attribute; import com.rapidminer.example.AttributeRole; import com.rapidminer.example.Example; @@ -49,12 +55,7 @@ import com.rapidminer.parameter.ParameterTypeCategory; import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.tools.OperatorResourceConsumptionHandler; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; +import com.rapidminer.tools.ProcessTools; /** @@ -640,7 +641,7 @@ private void mergeSets(ExampleSet resultSet, ExampleSet inputSet, List getParameterTypes() { List types = super.getParameterTypes(); - types.addAll(attributeSelector.getParameterTypes()); + types.addAll(ProcessTools.setSubsetSelectorPrimaryParameter(attributeSelector.getParameterTypes(), true)); ParameterType type = new ParameterTypeCategory(PARAMETER_NAME_CONFLICT_HANDLING, "Decides how to deal with duplicate attribute names.", HANDLE_NAME_CONFLICT_MODES, 0); diff --git a/src/main/java/com/rapidminer/operator/preprocessing/ExampleSetTranspose.java b/src/main/java/com/rapidminer/operator/preprocessing/ExampleSetTranspose.java index 9c0fe4734..1af540751 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/ExampleSetTranspose.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/ExampleSetTranspose.java @@ -1,180 +1,213 @@ -/** - * Copyright (C) 2001-2018 by RapidMiner and the contributors - * - * Complete list of developers available at our web site: - * - * http://rapidminer.com - * - * This program is free software: you can redistribute it and/or modify it under the terms of the - * GNU Affero 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 - * Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License along with this program. - * If not, see http://www.gnu.org/licenses/. -*/ -package com.rapidminer.operator.preprocessing; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import com.rapidminer.example.Attribute; -import com.rapidminer.example.AttributeRole; -import com.rapidminer.example.Attributes; -import com.rapidminer.example.Example; -import com.rapidminer.example.ExampleSet; -import com.rapidminer.example.table.AttributeFactory; -import com.rapidminer.example.utils.ExampleSetBuilder; -import com.rapidminer.example.utils.ExampleSets; -import com.rapidminer.operator.AbstractExampleSetProcessing; -import com.rapidminer.operator.OperatorDescription; -import com.rapidminer.operator.OperatorException; -import com.rapidminer.operator.annotation.ResourceConsumptionEstimator; -import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; -import com.rapidminer.operator.ports.metadata.MetaData; -import com.rapidminer.operator.preprocessing.filter.ChangeAttributeRole; -import com.rapidminer.tools.Ontology; -import com.rapidminer.tools.OperatorResourceConsumptionHandler; - - -/** - *

- * This operator transposes an example set, i.e. the columns with become the new rows and the old - * rows will become the columns. Hence, this operator works very similar to the well know transpose - * operation for matrices. - *

- * - *

- * If an Id attribute is part of the given example set, the ids will become the names of the new - * attributes. The names of the old attributes will be transformed into the id values of a new - * special Id attribute. Since no other "special" examples or data rows exist, all other - * new attributes will be regular after the transformation. You can use the - * {@link ChangeAttributeRole} operator in order to change one of these into a special type - * afterwards. - *

- * - *

- * If all old attribute have the same value type, all new attributes will have this value type. - * Otherwise, the new value types will all be "nominal" if at least one nominal attribute - * was part of the given example set and "real" if the types contained mixed numbers. - *

- * - *

- * This operator produces a copy of the data in the main memory and it therefore not suggested to - * use it on very large data sets. - *

- * - * @author Ingo Mierswa - */ -public class ExampleSetTranspose extends AbstractExampleSetProcessing { - - public ExampleSetTranspose(OperatorDescription description) { - super(description); - } - - @Override - protected MetaData modifyMetaData(ExampleSetMetaData metaData) { - return metaData.transpose(); - } - - @Override - public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { - // init operator progress - int numberOfAllAttributeRoles = 0; - Iterator aIter = exampleSet.getAttributes().allAttributeRoles(); - while (aIter.hasNext()) { - aIter.next(); - numberOfAllAttributeRoles++; - } - getProgress().setTotal(numberOfAllAttributeRoles); - // determine new value types - int valueType = Ontology.REAL; - Iterator a = exampleSet.getAttributes().allAttributeRoles(); - while (a.hasNext()) { - AttributeRole attributeRole = a.next(); - if (!attributeRole.isSpecial() || !attributeRole.getSpecialName().equals(Attributes.ID_NAME)) { - if (attributeRole.getAttribute().isNominal()) { - valueType = Ontology.NOMINAL; - break; - } - } - } - - // create new attributes - List newAttributes = new ArrayList(exampleSet.size()); - Attribute newIdAttribute = AttributeFactory.createAttribute(Attributes.ID_NAME, Ontology.NOMINAL); - newAttributes.add(newIdAttribute); - Attribute oldIdAttribute = exampleSet.getAttributes().getId(); - if (oldIdAttribute != null) { - for (Example e : exampleSet) { - double idValue = e.getValue(oldIdAttribute); - String attributeName = "att_" + idValue; - if (oldIdAttribute.isNominal()) { - if (Double.isNaN(idValue)) { - newAttributes.add(AttributeFactory.createAttribute(valueType)); - } else { - attributeName = oldIdAttribute.getMapping().mapIndex((int) idValue); - newAttributes.add(AttributeFactory.createAttribute(attributeName, valueType)); - } - } else { - newAttributes.add(AttributeFactory.createAttribute(attributeName, valueType)); - } - } - } else { - for (int i = 0; i < exampleSet.size(); i++) { - newAttributes.add(AttributeFactory.createAttribute("att_" + (i + 1), valueType)); - } - } - - // create and fill table - ExampleSetBuilder builder = ExampleSets.from(newAttributes); - a = exampleSet.getAttributes().allAttributeRoles(); - while (a.hasNext()) { - AttributeRole attributeRole = a.next(); - if (!attributeRole.isSpecial() || !attributeRole.getSpecialName().equals(Attributes.ID_NAME)) { - Attribute attribute = attributeRole.getAttribute(); - double[] data = new double[exampleSet.size() + 1]; - data[0] = newIdAttribute.getMapping().mapString(attribute.getName()); - int counter = 1; - for (Example e : exampleSet) { - double currentValue = e.getValue(attribute); - data[counter] = currentValue; - Attribute newAttribute = newAttributes.get(counter); - if (newAttribute.isNominal()) { - if (!Double.isNaN(currentValue)) { - String currentValueString = currentValue + ""; - if (attribute.isNominal()) { - currentValueString = attribute.getMapping().mapIndex((int) currentValue); - } - data[counter] = newAttribute.getMapping().mapString(currentValueString); - } - } - counter++; - } - builder.addRow(data); - } - getProgress().step(); - } - - // create and deliver example set - getProgress().complete(); - ExampleSet result = builder.withRole(newIdAttribute, Attributes.ID_NAME).build(); - result.getAnnotations().addAll(exampleSet.getAnnotations()); - return result; - } - - @Override - public boolean writesIntoExistingData() { - return false; - } - - @Override - public ResourceConsumptionEstimator getResourceConsumptionEstimator() { - return OperatorResourceConsumptionHandler.getResourceConsumptionEstimator(getInputPort(), ExampleSetTranspose.class, - null); - } -} +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.operator.preprocessing; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import com.rapidminer.example.Attribute; +import com.rapidminer.example.AttributeRole; +import com.rapidminer.example.Attributes; +import com.rapidminer.example.Example; +import com.rapidminer.example.ExampleSet; +import com.rapidminer.example.table.AttributeFactory; +import com.rapidminer.example.utils.ExampleSetBuilder; +import com.rapidminer.example.utils.ExampleSets; +import com.rapidminer.operator.AbstractExampleSetProcessing; +import com.rapidminer.operator.OperatorDescription; +import com.rapidminer.operator.OperatorException; +import com.rapidminer.operator.OperatorVersion; +import com.rapidminer.operator.UserError; +import com.rapidminer.operator.annotation.ResourceConsumptionEstimator; +import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; +import com.rapidminer.operator.ports.metadata.MetaData; +import com.rapidminer.operator.preprocessing.filter.ChangeAttributeRole; +import com.rapidminer.tools.Ontology; +import com.rapidminer.tools.OperatorResourceConsumptionHandler; + + +/** + *

+ * This operator transposes an example set, i.e. the columns with become the new rows and the old + * rows will become the columns. Hence, this operator works very similar to the well know transpose + * operation for matrices. + *

+ * + *

+ * If an Id attribute is part of the given example set, the ids will become the names of the new + * attributes. The names of the old attributes will be transformed into the id values of a new + * special Id attribute. Since no other "special" examples or data rows exist, all other + * new attributes will be regular after the transformation. You can use the + * {@link ChangeAttributeRole} operator in order to change one of these into a special type + * afterwards. + *

+ * + *

+ * If all old attribute have the same value type, all new attributes will have this value type. + * Otherwise, the new value types will all be "nominal" if at least one nominal attribute + * was part of the given example set and "real" if the types contained mixed numbers. + *

+ * + *

+ * This operator produces a copy of the data in the main memory and it therefore not suggested to + * use it on very large data sets. + *

+ * + * @author Ingo Mierswa + */ +public class ExampleSetTranspose extends AbstractExampleSetProcessing { + + private static final OperatorVersion BEFORE_FORMAT_NUMERICAL_ID = new OperatorVersion(8, 1, 10); + + public ExampleSetTranspose(OperatorDescription description) { + super(description); + } + + @Override + protected MetaData modifyMetaData(ExampleSetMetaData metaData) { + return metaData.transpose(); + } + + @Override + public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { + // init operator progress + int numberOfAllAttributeRoles = 0; + Iterator aIter = exampleSet.getAttributes().allAttributeRoles(); + while (aIter.hasNext()) { + aIter.next(); + numberOfAllAttributeRoles++; + } + getProgress().setTotal(numberOfAllAttributeRoles); + // determine new value types + int valueType = Ontology.REAL; + Iterator a = exampleSet.getAttributes().allAttributeRoles(); + while (a.hasNext()) { + AttributeRole attributeRole = a.next(); + if (!attributeRole.isSpecial() || !attributeRole.getSpecialName().equals(Attributes.ID_NAME)) { + if (attributeRole.getAttribute().isNominal()) { + valueType = Ontology.NOMINAL; + break; + } + } + } + + // create new attributes + List newAttributes = new ArrayList<>(exampleSet.size() + 1); + Attribute newIdAttribute = AttributeFactory.createAttribute(Attributes.ID_NAME, Ontology.NOMINAL); + newAttributes.add(newIdAttribute); + Attribute oldIdAttribute = exampleSet.getAttributes().getId(); + if (oldIdAttribute != null) { + // check for duplicate names here to reduce computing time + Set newAttributeNames = new LinkedHashSet<>(exampleSet.size()); + boolean oldIdAttributeIsNominal = oldIdAttribute.isNominal(); + boolean dontFormatNumericalIDs = !shouldFormatNumericalIDs() && oldIdAttribute.getValueType() == Ontology.INTEGER; + for (Example e : exampleSet) { + double idValue = e.getValue(oldIdAttribute); + String attributeName = "att_"; + if (Double.isNaN(idValue) || dontFormatNumericalIDs) { + attributeName += idValue; + } else { + int idIntValue = (int) idValue; + if (oldIdAttributeIsNominal) { + attributeName = oldIdAttribute.getMapping().mapIndex(idIntValue); + } else { + attributeName += idIntValue; + } + } + if (!newAttributeNames.add(attributeName)) { + // duplicate attribute name, i.e. duplicate IDs + throw new UserError(this, "transpose_duplicate_id", attributeName); + } + } + int finalValueType = valueType; + newAttributeNames.forEach(n -> newAttributes.add(AttributeFactory.createAttribute(n, finalValueType))); + } else { + for (int i = 0; i < exampleSet.size(); i++) { + newAttributes.add(AttributeFactory.createAttribute("att_" + (i + 1), valueType)); + } + } + + // create and fill table + ExampleSetBuilder builder = ExampleSets.from(newAttributes); + a = exampleSet.getAttributes().allAttributeRoles(); + while (a.hasNext()) { + AttributeRole attributeRole = a.next(); + if (!attributeRole.isSpecial() || !attributeRole.getSpecialName().equals(Attributes.ID_NAME)) { + Attribute attribute = attributeRole.getAttribute(); + double[] data = new double[exampleSet.size() + 1]; + data[0] = newIdAttribute.getMapping().mapString(attribute.getName()); + int counter = 1; + for (Example e : exampleSet) { + double currentValue = e.getValue(attribute); + data[counter] = currentValue; + Attribute newAttribute = newAttributes.get(counter); + if (newAttribute.isNominal()) { + if (!Double.isNaN(currentValue)) { + String currentValueString = currentValue + ""; + if (attribute.isNominal()) { + currentValueString = attribute.getMapping().mapIndex((int) currentValue); + } + data[counter] = newAttribute.getMapping().mapString(currentValueString); + } + } + counter++; + } + builder.addRow(data); + } + getProgress().step(); + } + + // create and deliver example set + getProgress().complete(); + ExampleSet result = builder.withRole(newIdAttribute, Attributes.ID_NAME).build(); + result.getAnnotations().addAll(exampleSet.getAnnotations()); + return result; + } + + @Override + public boolean writesIntoExistingData() { + return false; + } + + @Override + public ResourceConsumptionEstimator getResourceConsumptionEstimator() { + return OperatorResourceConsumptionHandler.getResourceConsumptionEstimator(getInputPort(), ExampleSetTranspose.class, + null); + } + + /** + * @return whether integer IDs should be formatted as integer dependent on the {@link #getCompatibilityLevel() compatibility level}. + * @since 8.2 + */ + private boolean shouldFormatNumericalIDs() { + return getCompatibilityLevel().isAbove(BEFORE_FORMAT_NUMERICAL_ID); + } + + @Override + public OperatorVersion[] getIncompatibleVersionChanges() { + OperatorVersion[] incompatibleVersionChanges = super.getIncompatibleVersionChanges(); + incompatibleVersionChanges = Arrays.copyOf(incompatibleVersionChanges, incompatibleVersionChanges.length + 1); + incompatibleVersionChanges[incompatibleVersionChanges.length - 1] = BEFORE_FORMAT_NUMERICAL_ID; + return incompatibleVersionChanges; + } +} diff --git a/src/main/java/com/rapidminer/operator/preprocessing/GuessValueTypes.java b/src/main/java/com/rapidminer/operator/preprocessing/GuessValueTypes.java index 4093ee85d..2e7b40bf6 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/GuessValueTypes.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/GuessValueTypes.java @@ -39,6 +39,7 @@ import com.rapidminer.parameter.ParameterTypeString; import com.rapidminer.tools.Ontology; import com.rapidminer.tools.OperatorResourceConsumptionHandler; +import com.rapidminer.tools.ProcessTools; import com.rapidminer.tools.Tools; @@ -225,7 +226,7 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.addAll(attributeSelector.getParameterTypes()); + types.addAll(ProcessTools.setSubsetSelectorPrimaryParameter(attributeSelector.getParameterTypes(), true)); types.add(new ParameterTypeString(PARAMETER_DECIMAL_POINT_CHARACTER, "Character that is used as decimal point.", ".", false)); types.add(new ParameterTypeString(PARAMETER_NUMBER_GROUPING_CHARACTER, diff --git a/src/main/java/com/rapidminer/operator/preprocessing/PreprocessingOperator.java b/src/main/java/com/rapidminer/operator/preprocessing/PreprocessingOperator.java index c1aee99eb..01c0005a8 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/PreprocessingOperator.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/PreprocessingOperator.java @@ -36,6 +36,7 @@ import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeBoolean; import com.rapidminer.parameter.UndefinedParameterError; +import com.rapidminer.tools.ProcessTools; import com.rapidminer.tools.container.Pair; @@ -48,10 +49,12 @@ */ public abstract class PreprocessingOperator extends AbstractDataProcessing { - private final OutputPort modelOutput = getOutputPorts().createPort("preprocessing model"); - - protected final AttributeSubsetSelector attributeSelector = new AttributeSubsetSelector(this, getExampleSetInputPort(), - getFilterValueTypes()); + /** + * the name of the preprocessing model output port + * + * @since 8.2.0 + */ + public static final String PREPROCESSING_MODEL_OUTPUT_PORT_NAME = "preprocessing model"; /** * The parameter name for "Indicates if the preprocessing model should also be @@ -65,6 +68,12 @@ public abstract class PreprocessingOperator extends AbstractDataProcessing { */ public static final String PARAMETER_CREATE_VIEW = "create_view"; + private final OutputPort modelOutput = getOutputPorts().createPort(PREPROCESSING_MODEL_OUTPUT_PORT_NAME); + + protected final AttributeSubsetSelector attributeSelector = new AttributeSubsetSelector(this, getExampleSetInputPort(), + getFilterValueTypes()); + + public PreprocessingOperator(OperatorDescription description) { super(description); getTransformer().addRule( @@ -210,7 +219,7 @@ public List getParameterTypes() { type.setHidden(!isSupportingView()); types.add(type); - types.addAll(attributeSelector.getParameterTypes()); + types.addAll(ProcessTools.setSubsetSelectorPrimaryParameter(attributeSelector.getParameterTypes(), true)); return types; } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/discretization/MinimalEntropyDiscretization.java b/src/main/java/com/rapidminer/operator/preprocessing/discretization/MinimalEntropyDiscretization.java index baeafe0b0..3e680ef5c 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/discretization/MinimalEntropyDiscretization.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/discretization/MinimalEntropyDiscretization.java @@ -43,6 +43,7 @@ import com.rapidminer.operator.ports.metadata.AttributeMetaData; import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; import com.rapidminer.operator.ports.metadata.SimpleMetaDataError; +import com.rapidminer.operator.ports.quickfix.AttributeToNominalQuickFixProvider; import com.rapidminer.operator.preprocessing.PreprocessingModel; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeBoolean; @@ -52,6 +53,7 @@ import com.rapidminer.parameter.conditions.EqualTypeCondition; import com.rapidminer.tools.Ontology; import com.rapidminer.tools.OperatorResourceConsumptionHandler; +import com.rapidminer.tools.OperatorService; import com.rapidminer.tools.math.MathFunctions; @@ -102,10 +104,10 @@ public MinimalEntropyDiscretization(OperatorDescription description) { protected void checkSelectedSubsetMetaData(ExampleSetMetaData subsetMetaData) { switch (subsetMetaData.containsSpecialAttribute(Attributes.LABEL_NAME)) { case YES: - AttributeMetaData labelMD = subsetMetaData.getAttributeByRole(Attributes.LABEL_NAME); + AttributeMetaData labelMD = subsetMetaData.getLabelMetaData(); if (!labelMD.isNominal()) { getExampleSetInputPort().addError( - new SimpleMetaDataError(Severity.ERROR, getExampleSetInputPort(), "attribute_has_wrong_type", + new SimpleMetaDataError(Severity.ERROR, getExampleSetInputPort(), AttributeToNominalQuickFixProvider.labelToNominal(getExampleSetInputPort(), labelMD), "attribute_has_wrong_type", labelMD.getName(), Ontology.VALUE_TYPE_NAMES[Ontology.NOMINAL])); } break; @@ -312,6 +314,14 @@ private double[][] getRanges(ExampleSet exampleSet) throws UserError { Attributes attributes = exampleSet.getAttributes(); double[][] ranges = new double[attributes.size()][]; Attribute label = attributes.getLabel(); + if (!label.isNominal()) { + OperatorDescription[] descriptions = OperatorService.getOperatorDescriptions(getClass()); + String name = getClass().getSimpleName(); + if (descriptions != null && descriptions.length > 0 && descriptions[0] != null) { + name = descriptions[0].getName(); + } + throw new UserError(this, 101, name, label.getName()); + } int a = 0; for (Attribute attribute : attributes) { diff --git a/src/main/java/com/rapidminer/operator/preprocessing/discretization/UserBasedDiscretization.java b/src/main/java/com/rapidminer/operator/preprocessing/discretization/UserBasedDiscretization.java index ba67e7419..5f1ff701f 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/discretization/UserBasedDiscretization.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/discretization/UserBasedDiscretization.java @@ -45,6 +45,7 @@ import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.tools.Ontology; import com.rapidminer.tools.OperatorResourceConsumptionHandler; +import com.rapidminer.tools.ProcessTools; import com.rapidminer.tools.container.Tupel; @@ -126,6 +127,7 @@ public Class getPreprocessingModelClass() { @Override public List getParameterTypes() { List types = super.getParameterTypes(); + ProcessTools.setSubsetSelectorPrimaryParameter(types, false); ParameterType classType = new ParameterTypeString(PARAMETER_CLASS_NAME, "The name of this range."); ParameterType threshold = new ParameterTypeDouble(PARAMETER_UPPER_LIMIT, "The upper limit.", @@ -138,6 +140,7 @@ public List getParameterTypes() { ParameterType type = new ParameterTypeList(PARAMETER_RANGE_NAMES, "Defines the classes and the upper limits of each class.", classType, threshold, defaultList); type.setExpert(false); + type.setPrimary(true); types.add(type); return types; diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/AbstractFilteredDataProcessing.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/AbstractFilteredDataProcessing.java index 4677e86d7..34106522a 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/AbstractFilteredDataProcessing.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/AbstractFilteredDataProcessing.java @@ -42,6 +42,7 @@ import com.rapidminer.operator.tools.AttributeSubsetSelector; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.UndefinedParameterError; +import com.rapidminer.tools.ProcessTools; /** @@ -227,7 +228,7 @@ public final ExampleSet apply(ExampleSet exampleSet) throws OperatorException { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.addAll(attributeSelector.getParameterTypes()); + types.addAll(ProcessTools.setSubsetSelectorPrimaryParameter(attributeSelector.getParameterTypes(), true)); return types; } } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueMapper.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueMapper.java index 0c5ff59f7..ad1a19ba0 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueMapper.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueMapper.java @@ -455,6 +455,7 @@ public List getParameterTypes() { PARAMETER_OLD_VALUES, "The original values which should be replaced.", false), new ParameterTypeString( PARAMETER_NEW_VALUES, "Specifies the new value", false)); type.setExpert(false); + type.setPrimary(true); types.add(type); type = new ParameterTypeString(PARAMETER_REPLACE_WHAT, "All occurrences of this value will be replaced.", true); diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueReplace.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueReplace.java index e81f878ce..3595c5542 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueReplace.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueReplace.java @@ -176,8 +176,10 @@ protected int[] getFilterValueTypes() { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeRegexp(PARAMETER_REPLACE_WHAT, "A regular expression specifying what should be replaced.", - false, false)); + ParameterType type = new ParameterTypeRegexp(PARAMETER_REPLACE_WHAT, "A regular expression specifying what should be replaced.", + false, false); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeString(PARAMETER_REPLACE_BY, "The replacement for the region matched by the regular expression. Possibly including capturing groups.", true, false)); diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueSplit.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueSplit.java index 6368b3b8e..f32fcab18 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueSplit.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/AttributeValueSplit.java @@ -341,6 +341,7 @@ public List getParameterTypes() { ParameterType type = new ParameterTypeRegexp(PARAMETER_SPLIT_PATTERN, "The pattern which is used for dividing the nominal values into different parts.", ","); type.setExpert(false); + type.setPrimary(true); types.add(type); type = new ParameterTypeCategory(PARAMETER_SPLIT_MODE, diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeName.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeName.java index 7c9f549e6..05ea55750 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeName.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeName.java @@ -21,7 +21,6 @@ import java.util.List; import com.rapidminer.example.Attribute; -import com.rapidminer.example.AttributeRole; import com.rapidminer.example.ExampleSet; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; @@ -97,8 +96,7 @@ protected MetaData modifyMetaData(ExampleSetMetaData metaData) { return metaData; } - private void renameAttributeMetaData(ExampleSetMetaData metaData, String oldName, String newName) - throws UndefinedParameterError { + private void renameAttributeMetaData(ExampleSetMetaData metaData, String oldName, String newName) { AttributeMetaData amd = metaData.getAttributeByName(oldName); if (amd != null && newName != null) { if (metaData.containsAttributeName(newName) == MetaDataInfo.YES) { @@ -139,10 +137,6 @@ private void renameAttribute(ExampleSet exampleSet, String oldName, String newNa if (attribute == null) { throw new AttributeNotFoundError(this, PARAMETER_OLD_NAME, oldName); } - AttributeRole existingAttributeRole = exampleSet.getAttributes().findRoleByName(newName); - if (existingAttributeRole != null) { - throw new UserError(this, 152, newName); - } attribute.setName(newName); } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeNames2Generic.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeNames2Generic.java index 0888908c9..4432d8270 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeNames2Generic.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeNames2Generic.java @@ -18,6 +18,9 @@ */ package com.rapidminer.operator.preprocessing.filter; +import java.util.List; +import java.util.Set; + import com.rapidminer.example.Attribute; import com.rapidminer.example.ExampleSet; import com.rapidminer.operator.OperatorDescription; @@ -32,9 +35,7 @@ import com.rapidminer.parameter.ParameterTypeString; import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.tools.OperatorResourceConsumptionHandler; - -import java.util.List; -import java.util.Set; +import com.rapidminer.tools.ProcessTools; /** @@ -88,7 +89,7 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.addAll(attributeSelector.getParameterTypes()); + types.addAll(ProcessTools.setSubsetSelectorPrimaryParameter(attributeSelector.getParameterTypes(), true)); ParameterType type = new ParameterTypeString( PARAMETER_NAME_STEM, "Specifies the name stem which is used to build generic names. Using 'att' as stem would lead to 'att1', 'att2', etc. as attribute names.", diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeNamesReplace.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeNamesReplace.java index 8d5e18b77..94d708bbe 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeNamesReplace.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/ChangeAttributeNamesReplace.java @@ -18,6 +18,11 @@ */ package com.rapidminer.operator.preprocessing.filter; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + import com.rapidminer.example.Attribute; import com.rapidminer.example.ExampleSet; import com.rapidminer.operator.OperatorDescription; @@ -37,11 +42,6 @@ import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.tools.OperatorResourceConsumptionHandler; -import java.util.List; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - /** *

@@ -104,8 +104,6 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { } } catch (IndexOutOfBoundsException e) { throw new UserError(this, 215, replaceByString, PARAMETER_REPLACE_WHAT); - } catch (IllegalArgumentException e) { - throw new UserError(this, 152, replaceByString); } return exampleSet; @@ -120,6 +118,7 @@ public List getParameterTypes() { "A regular expression defining what should be replaced in the attribute names.", "\\W"); type.setShowRange(false); type.setExpert(false); + type.setPrimary(true); types.add(type); types.add(new ParameterTypeString(PARAMETER_REPLACE_BY, diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/Date2Nominal.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/Date2Nominal.java index 9d10e9f45..3b8486171 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/Date2Nominal.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/Date2Nominal.java @@ -19,20 +19,20 @@ package com.rapidminer.operator.preprocessing.filter; import java.text.SimpleDateFormat; -import java.util.Collections; +import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Locale; import com.rapidminer.example.Attribute; +import com.rapidminer.example.AttributeRole; import com.rapidminer.example.Example; import com.rapidminer.example.ExampleSet; import com.rapidminer.example.table.AttributeFactory; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; -import com.rapidminer.operator.ProcessSetupError.Severity; -import com.rapidminer.operator.SimpleProcessSetupError; +import com.rapidminer.operator.OperatorVersion; import com.rapidminer.operator.UserError; import com.rapidminer.operator.annotation.ResourceConsumptionEstimator; import com.rapidminer.operator.error.AttributeNotFoundError; @@ -42,13 +42,11 @@ import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; import com.rapidminer.operator.ports.metadata.MetaData; import com.rapidminer.operator.ports.metadata.SetRelation; -import com.rapidminer.operator.ports.quickfix.ParameterSettingQuickFix; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeAttribute; import com.rapidminer.parameter.ParameterTypeBoolean; import com.rapidminer.parameter.ParameterTypeCategory; import com.rapidminer.parameter.ParameterTypeDateFormat; -import com.rapidminer.parameter.ParameterTypeStringCategory; import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.tools.Ontology; import com.rapidminer.tools.OperatorResourceConsumptionHandler; @@ -185,7 +183,11 @@ public class Date2Nominal extends AbstractDateDataProcessing { public static final String PARAMETER_ATTRIBUTE_NAME = "attribute_name"; - public static final String PARAMETER_DATE_FORMAT = "date_format"; + /** + * @deprecated since 8.2; use {@link ParameterTypeDateFormat#PARAMETER_DATE_FORMAT} instead. + */ + @Deprecated + public static final String PARAMETER_DATE_FORMAT = ParameterTypeDateFormat.PARAMETER_DATE_FORMAT; public static final String PARAMETER_TIME_ZONE = "time_zone"; @@ -193,6 +195,9 @@ public class Date2Nominal extends AbstractDateDataProcessing { public static final String PARAMETER_KEEP_OLD_ATTRIBUTE = "keep_old_attribute"; + /** The last version that removed the special role of the selected attribute */ + public static final OperatorVersion VERSION_DOES_NOT_KEEP_ROLE = new OperatorVersion(8, 1, 2); + public Date2Nominal(OperatorDescription description) { super(description); getExampleSetInputPort().addPrecondition( @@ -212,20 +217,19 @@ protected MetaData modifyMetaData(ExampleSetMetaData metaData) throws UndefinedP newAttribute.getMean().setUnkown(); HashSet valueSet = new HashSet<>(); if (amd.getValueRange() != null) { - String dateFormat = getParameterAsString(PARAMETER_DATE_FORMAT); int localeIndex = getParameterAsInt(PARAMETER_LOCALE); Locale selectedLocale = Locale.US; if (localeIndex >= 0 && localeIndex < availableLocales.size()) { selectedLocale = availableLocales.get(getParameterAsInt(PARAMETER_LOCALE)); } - SimpleDateFormat parser; + SimpleDateFormat parser = null; try { - parser = new SimpleDateFormat(dateFormat, selectedLocale); - } catch (IllegalArgumentException | NullPointerException e) { - addError(new SimpleProcessSetupError(Severity.ERROR, getPortOwner(), - Collections.singletonList(new ParameterSettingQuickFix(this, PARAMETER_DATE_FORMAT)), - "parameter_invalid_time_format", PARAMETER_DATE_FORMAT)); + parser = ParameterTypeDateFormat.createCheckedDateFormat(this, selectedLocale, true); + } catch (UserError userError) { + // will not happen because of setup error + } + if (parser == null) { return metaData; } @@ -241,6 +245,9 @@ protected MetaData modifyMetaData(ExampleSetMetaData metaData) throws UndefinedP newAttribute.setValueSet(valueSet, SetRelation.SUPERSET); if (!getParameterAsBoolean(PARAMETER_KEEP_OLD_ATTRIBUTE)) { metaData.removeAttribute(amd); + if (getCompatibilityLevel().isAbove(VERSION_DOES_NOT_KEEP_ROLE)) { + newAttribute.setRole(amd.getRole()); + } } else { newAttribute.setName(newAttribute.getName() + ATTRIBUTE_NAME_POSTFIX); } @@ -266,18 +273,12 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { exampleSet.getExampleTable().addAttribute(newAttribute); exampleSet.getAttributes().addRegular(newAttribute); - String dateFormat = getParameterAsString(PARAMETER_DATE_FORMAT); int localeIndex = getParameterAsInt(PARAMETER_LOCALE); Locale selectedLocale = Locale.US; if (localeIndex >= 0 && localeIndex < availableLocales.size()) { selectedLocale = availableLocales.get(getParameterAsInt(PARAMETER_LOCALE)); } - SimpleDateFormat parser; - try { - parser = new SimpleDateFormat(dateFormat, selectedLocale); - } catch (IllegalArgumentException | NullPointerException e) { - throw new UserError(this, "invalid_date_format", dateFormat, e.getMessage()); - } + SimpleDateFormat parser = ParameterTypeDateFormat.createCheckedDateFormat(this, selectedLocale, false); parser.setTimeZone(Tools.getTimeZone(getParameterAsInt(PARAMETER_TIME_ZONE))); for (Example example : exampleSet) { @@ -291,8 +292,12 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { } if (!getParameterAsBoolean(PARAMETER_KEEP_OLD_ATTRIBUTE)) { + AttributeRole dateAttributeRole = exampleSet.getAttributes().getRole(dateAttribute); exampleSet.getAttributes().remove(dateAttribute); newAttribute.setName(attributeName); + if (dateAttributeRole.isSpecial() && getCompatibilityLevel().isAbove(VERSION_DOES_NOT_KEEP_ROLE)) { + exampleSet.getAttributes().getRole(newAttribute).setSpecial(dateAttributeRole.getSpecialName()); + } } else { newAttribute.setName(attributeName + ATTRIBUTE_NAME_POSTFIX); } @@ -305,9 +310,8 @@ public List getParameterTypes() { ParameterTypeAttribute attributeParamType = new ParameterTypeAttribute(PARAMETER_ATTRIBUTE_NAME, "The attribute which should be parsed.", getExampleSetInputPort(), false, false, Ontology.DATE_TIME); types.add(attributeParamType); - ParameterType type = new ParameterTypeStringCategory(PARAMETER_DATE_FORMAT, - "The output format of the date values, for example \"yyyy/MM/dd\".", - ParameterTypeDateFormat.PREDEFINED_DATE_FORMATS); + ParameterType type = new ParameterTypeDateFormat(ParameterTypeDateFormat.PARAMETER_DATE_FORMAT, + "The output format of the date values, for example \"yyyy/MM/dd\".", false); types.add(type); type = new ParameterTypeCategory(PARAMETER_TIME_ZONE, @@ -334,4 +338,13 @@ public boolean writesIntoExistingData() { public ResourceConsumptionEstimator getResourceConsumptionEstimator() { return OperatorResourceConsumptionHandler.getResourceConsumptionEstimator(getInputPort(), Date2Nominal.class, null); } + + @Override + public OperatorVersion[] getIncompatibleVersionChanges() { + OperatorVersion[] changes = super.getIncompatibleVersionChanges(); + changes = Arrays.copyOf(changes, changes.length + 1); + changes[changes.length - 1] = VERSION_DOES_NOT_KEEP_ROLE; + return changes; + } + } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/DateAdjust.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/DateAdjust.java index 539b75478..dfb56002e 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/DateAdjust.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/DateAdjust.java @@ -242,10 +242,12 @@ public List getParameterTypes() { types.add(new ParameterTypeAttribute(PARAMETER_ATTRIBUTE_NAME, "The attribute which should be parsed.", getExampleSetInputPort(), false, Ontology.DATE_TIME)); - types.add(new ParameterTypeList(PARAMETER_ADJUSTMENTS, "This list defines all date adjustments.", + ParameterType type = new ParameterTypeList(PARAMETER_ADJUSTMENTS, "This list defines all date adjustments.", new ParameterTypeInt("adjustment", "The number of units to add to the dates.", -Integer.MAX_VALUE, Integer.MAX_VALUE), new ParameterTypeCategory(PARAMETER_DATE_UNIT, - "The unit which should be adjusted.", CALENDAR_FIELDS, CALENDAR_FIELD_HOUR))); + "The unit which should be adjusted.", CALENDAR_FIELDS, CALENDAR_FIELD_HOUR)); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeBoolean(PARAMETER_KEEP_OLD_ATTRIBUTE, "Indicates if the original date attribute should be kept.", false)); diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/DeclareMissingValueOperator.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/DeclareMissingValueOperator.java index c3ba75853..2fa5d6e18 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/DeclareMissingValueOperator.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/DeclareMissingValueOperator.java @@ -48,6 +48,7 @@ import com.rapidminer.parameter.conditions.EqualTypeCondition; import com.rapidminer.tools.Ontology; import com.rapidminer.tools.OperatorResourceConsumptionHandler; +import com.rapidminer.tools.ProcessTools; import com.rapidminer.tools.expression.ExampleResolver; import com.rapidminer.tools.expression.Expression; import com.rapidminer.tools.expression.ExpressionException; @@ -257,7 +258,7 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { public List getParameterTypes() { List parameters = super.getParameterTypes(); - parameters.addAll(subsetSelector.getParameterTypes()); + parameters.addAll(ProcessTools.setSubsetSelectorPrimaryParameter(subsetSelector.getParameterTypes(), true)); ParameterType type = new ParameterTypeCategory(PARAMETER_MODE, "Select the value type of the missing value", VALUE_TYPES, 0); @@ -280,6 +281,7 @@ public List getParameterTypes() { "This parameter defines the expression which if true equals the missing value", getInputPort(), true, false); type.registerDependencyCondition(new EqualTypeCondition(this, PARAMETER_MODE, VALUE_TYPES, true, 2)); type.setExpert(false); + type.setPrimary(true); parameters.add(type); return parameters; diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/ExampleFilter.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/ExampleFilter.java index 207353824..762e74376 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/ExampleFilter.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/ExampleFilter.java @@ -229,6 +229,7 @@ public List getParameterTypes() { type.registerDependencyCondition(new EqualStringCondition(this, PARAMETER_CONDITION_CLASS, false, ConditionedExampleSet.KNOWN_CONDITION_NAMES[ConditionedExampleSet.CONDITION_CUSTOM_FILTER])); type.setExpert(false); + type.setPrimary(true); types.add(type); type = new ParameterTypeString(PARAMETER_PARAMETER_STRING, diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/InternalBinominalRemapping.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/InternalBinominalRemapping.java index e51e3ce8c..f727098c3 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/InternalBinominalRemapping.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/InternalBinominalRemapping.java @@ -38,6 +38,7 @@ import com.rapidminer.parameter.ParameterTypeString; import com.rapidminer.tools.Ontology; import com.rapidminer.tools.OperatorResourceConsumptionHandler; +import com.rapidminer.tools.ProcessTools; /** @@ -124,7 +125,7 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.addAll(attributeSelector.getParameterTypes()); + types.addAll(ProcessTools.setSubsetSelectorPrimaryParameter(attributeSelector.getParameterTypes(), true)); types.add(new ParameterTypeString(PARAMETER_NEGATIVE_VALUE, "The first/negative/false value.", false)); types.add(new ParameterTypeString(PARAMETER_POSITIVE_VALUE, "The second/positive/true value.", false)); return types; diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/MissingValueImputation.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/MissingValueImputation.java index a4d8edff2..3111c598a 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/MissingValueImputation.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/MissingValueImputation.java @@ -57,6 +57,7 @@ import com.rapidminer.parameter.ParameterTypeCategory; import com.rapidminer.tools.OperatorResourceConsumptionHandler; import com.rapidminer.tools.OperatorService; +import com.rapidminer.tools.ProcessTools; import com.rapidminer.tools.RandomGenerator; @@ -366,7 +367,7 @@ private Attribute[] getOrderedAttributes(ExampleSet exampleSet, int order, boole @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.addAll(attributeSelector.getParameterTypes()); + types.addAll(ProcessTools.setSubsetSelectorPrimaryParameter(attributeSelector.getParameterTypes(), true)); ParameterType type = new ParameterTypeBoolean(PARAMETER_ITERATE, "Impute missing values immediately after having learned the corresponding concept and iterate.", true); type.setExpert(false); diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/MissingValueReplenishment.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/MissingValueReplenishment.java index 50e025d68..7b29e1451 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/MissingValueReplenishment.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/MissingValueReplenishment.java @@ -42,6 +42,7 @@ import com.rapidminer.operator.ports.metadata.MDInteger; import com.rapidminer.operator.ports.metadata.SimpleMetaDataError; import com.rapidminer.parameter.ParameterType; +import com.rapidminer.parameter.ParameterTypeDateFormat; import com.rapidminer.parameter.ParameterTypeString; import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.parameter.conditions.ParameterCondition; @@ -235,7 +236,7 @@ public double getReplenishmentValue(int functionIndex, ExampleSet exampleSet, At if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(attribute.getValueType(), Ontology.DATE_TIME)) { String formatString = null; if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(attribute.getValueType(), Ontology.DATE)) { - formatString = "MM/dd/yyyy"; + formatString = ParameterTypeDateFormat.DATE_FORMAT_MM_DD_YYYY; } else if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(attribute.getValueType(), Ontology.TIME)) { formatString = "hh.mm a"; } else if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(attribute.getValueType(), Ontology.DATE_TIME)) { diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/Nominal2Date.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/Nominal2Date.java index c2cc8f7d4..b8938d56a 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/Nominal2Date.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/Nominal2Date.java @@ -22,7 +22,6 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; -import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; @@ -34,7 +33,6 @@ import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; import com.rapidminer.operator.ProcessSetupError.Severity; -import com.rapidminer.operator.SimpleProcessSetupError; import com.rapidminer.operator.UserError; import com.rapidminer.operator.annotation.ResourceConsumptionEstimator; import com.rapidminer.operator.error.AttributeNotFoundError; @@ -44,7 +42,6 @@ import com.rapidminer.operator.ports.metadata.MetaData; import com.rapidminer.operator.ports.metadata.SetRelation; import com.rapidminer.operator.ports.metadata.SimpleMetaDataError; -import com.rapidminer.operator.ports.quickfix.ParameterSettingQuickFix; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeAttribute; import com.rapidminer.parameter.ParameterTypeBoolean; @@ -185,7 +182,11 @@ public class Nominal2Date extends AbstractDateDataProcessing { public static final String PARAMETER_DATE_TYPE = "date_type"; - public static final String PARAMETER_DATE_FORMAT = "date_format"; + /** + * @deprecated since 8.2; use {@link ParameterTypeDateFormat#PARAMETER_DATE_FORMAT} instead. + */ + @Deprecated + public static final String PARAMETER_DATE_FORMAT = ParameterTypeDateFormat.PARAMETER_DATE_FORMAT; public static final String PARAMETER_TIME_ZONE = "time_zone"; @@ -211,18 +212,15 @@ public Nominal2Date(OperatorDescription description) { protected MetaData modifyMetaData(ExampleSetMetaData metaData) throws UndefinedParameterError { // testing the date format DateFormat format = null; + int localeIndex = getParameterAsInt(PARAMETER_LOCALE); + Locale selectedLocale = Locale.US; + if (localeIndex >= 0 && localeIndex < availableLocales.size()) { + selectedLocale = availableLocales.get(getParameterAsInt(PARAMETER_LOCALE)); + } try { - String dateFormat = getParameterAsString(PARAMETER_DATE_FORMAT); - int localeIndex = getParameterAsInt(PARAMETER_LOCALE); - Locale selectedLocale = Locale.US; - if (localeIndex >= 0 && localeIndex < availableLocales.size()) { - selectedLocale = availableLocales.get(getParameterAsInt(PARAMETER_LOCALE)); - } - format = new SimpleDateFormat(dateFormat, selectedLocale); - } catch (IllegalArgumentException e) { - addError(new SimpleProcessSetupError(Severity.ERROR, getPortOwner(), - Collections.singletonList(new ParameterSettingQuickFix(this, PARAMETER_DATE_FORMAT)), - "parameter_invalid_time_format", PARAMETER_DATE_FORMAT)); + format = ParameterTypeDateFormat.createCheckedDateFormat(this, selectedLocale, true); + } catch (UserError userError) { + // will not happen because of setup error } // attribute: change type AttributeMetaData amd = metaData.getAttributeByName(getParameterAsString(PARAMETER_ATTRIBUTE_NAME)); @@ -287,7 +285,7 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { throw new AttributeNotFoundError(this, PARAMETER_ATTRIBUTE_NAME, attributeName); } - String dateFormat = getParameterAsString(PARAMETER_DATE_FORMAT); + String dateFormat = getParameterAsString(ParameterTypeDateFormat.PARAMETER_DATE_FORMAT); int dateType = getParameterAsInt(PARAMETER_DATE_TYPE); int localeIndex = getParameterAsInt(PARAMETER_LOCALE); Locale selectedLocale = Locale.US; @@ -307,13 +305,8 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { exampleSet.getExampleTable().addAttribute(newAttribute); exampleSet.getAttributes().addRegular(newAttribute); - SimpleDateFormat parser; - try { - parser = new SimpleDateFormat(dateFormat, selectedLocale); - } catch (IllegalArgumentException | NullPointerException e) { - throw new UserError(this, "invalid_date_format", dateFormat, e.getMessage()); - } - + // parser can not be null; either the pattern specified is working or a UserError is thrown + SimpleDateFormat parser = ParameterTypeDateFormat.createCheckedDateFormat(this, selectedLocale, false); parser.setTimeZone(Tools.getTimeZone(getParameterAsInt(PARAMETER_TIME_ZONE))); int row = 1; @@ -363,11 +356,7 @@ public List getParameterTypes() { type.setExpert(false); types.add(type); - type = new ParameterTypeDateFormat(attributeParamType, PARAMETER_DATE_FORMAT, - "The parse format of the date values, for example \"yyyy/MM/dd\".", getExampleSetInputPort(), false); - type.setExpert(false); - type.setOptional(false); - types.add(type); + types.add(new ParameterTypeDateFormat(attributeParamType, getExampleSetInputPort())); type = new ParameterTypeCategory(PARAMETER_TIME_ZONE, "The time zone used for the date objects if not specified in the date string itself.", diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/NominalToNumericModel.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/NominalToNumericModel.java index 52788afb4..f624ff77e 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/NominalToNumericModel.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/NominalToNumericModel.java @@ -285,7 +285,7 @@ private ExampleSet applyOnDataDummyCoding(ExampleSet exampleSet, boolean effectC for (Attribute targetAttribute : targetAttributesFromSources.get(nominalAttribute)) { example.setValue(targetAttribute, getValue(targetAttribute, sourceValue)); } - if (++progressCompletedCounter % 10_000 == 0) { + if (progress != null && ++progressCompletedCounter % 10_000 == 0) { progress.setCompleted((int) (1000.0d * progressCompletedCounter / progressTotal)); } } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/NonDominatedSorting.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/NonDominatedSorting.java index 73be5d94f..264ee43e6 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/NonDominatedSorting.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/NonDominatedSorting.java @@ -18,6 +18,12 @@ */ package com.rapidminer.operator.preprocessing.filter; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Pattern; + import com.rapidminer.example.Attribute; import com.rapidminer.example.Example; import com.rapidminer.example.ExampleSet; @@ -30,12 +36,6 @@ import com.rapidminer.parameter.ParameterTypeAttributes; import com.rapidminer.tools.OperatorResourceConsumptionHandler; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.regex.Pattern; - /** * This operator sorts a given example set according to a subset of attributes and will sort pareto @@ -176,6 +176,7 @@ public List getParameterTypes() { ParameterType type = new ParameterTypeAttributes(PARAMETER_ATTRIBUTES, "Defines the attributes which should be used for the sorting.", getExampleSetInputPort(), false); type.setExpert(false); + type.setPrimary(true); types.add(type); return types; } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/RemoveDuplicates.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/RemoveDuplicates.java index 6086cdd7e..cd9d5ead5 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/RemoveDuplicates.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/RemoveDuplicates.java @@ -43,6 +43,7 @@ import com.rapidminer.parameter.ParameterTypeBoolean; import com.rapidminer.parameter.UndefinedParameterError; import com.rapidminer.tools.OperatorResourceConsumptionHandler; +import com.rapidminer.tools.ProcessTools; /** @@ -171,7 +172,7 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.addAll(subsetSelector.getParameterTypes()); + types.addAll(ProcessTools.setSubsetSelectorPrimaryParameter(subsetSelector.getParameterTypes(), true)); ParameterType type = new ParameterTypeBoolean(PARAMETER_TREAT_MISSING_VALUES_AS_DUPLICATES, "If set to true, treats missing values as duplicates", false); diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/attributes/AttributeFilter.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/attributes/AttributeFilter.java index bf0434b2b..12b048772 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/attributes/AttributeFilter.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/attributes/AttributeFilter.java @@ -18,6 +18,10 @@ */ package com.rapidminer.operator.preprocessing.filter.attributes; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + import com.rapidminer.example.Attribute; import com.rapidminer.example.Attributes; import com.rapidminer.example.ExampleSet; @@ -31,10 +35,7 @@ import com.rapidminer.operator.tools.AttributeSubsetSelector; import com.rapidminer.parameter.ParameterType; import com.rapidminer.tools.OperatorResourceConsumptionHandler; - -import java.util.Iterator; -import java.util.List; -import java.util.Set; +import com.rapidminer.tools.ProcessTools; /** @@ -101,7 +102,7 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.addAll(attributeSelector.getParameterTypes()); + types.addAll(ProcessTools.setSubsetSelectorPrimaryParameter(attributeSelector.getParameterTypes(), true)); return types; } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/filter/attributes/AttributeOrderingOperator.java b/src/main/java/com/rapidminer/operator/preprocessing/filter/attributes/AttributeOrderingOperator.java index d2ab5bdc6..628cef421 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/filter/attributes/AttributeOrderingOperator.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/filter/attributes/AttributeOrderingOperator.java @@ -481,6 +481,7 @@ public List getParameterTypes() { type = new ParameterTypeAttributeOrderingRules(PARAMETER_ORDER_RULES, "Rules to order attributes.", getInputPorts().getPortByIndex(0), true); type.setExpert(false); + type.setPrimary(true); type.registerDependencyCondition( new EqualTypeCondition(this, PARAMETER_ORDER_MODE, SORT_MODES, true, USER_SPECIFIED_RULES_MODE_INDEX)); parameterTypes.add(type); diff --git a/src/main/java/com/rapidminer/operator/preprocessing/sampling/PartitionOperator.java b/src/main/java/com/rapidminer/operator/preprocessing/sampling/PartitionOperator.java index ec275b1e2..38f45f3e9 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/sampling/PartitionOperator.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/sampling/PartitionOperator.java @@ -171,8 +171,10 @@ public void doWork() throws OperatorException { @Override public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeEnumeration(PARAMETER_PARTITIONS, "The partitions that should be created.", - new ParameterTypeDouble(PARAMETER_RATIO, "The relative size of this partition.", 0, 1), false)); + ParameterTypeEnumeration type = new ParameterTypeEnumeration(PARAMETER_PARTITIONS, "The partitions that should be created.", + new ParameterTypeDouble(PARAMETER_RATIO, "The relative size of this partition.", 0, 1), false); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeCategory(PARAMETER_SAMPLING_TYPE, "Defines the sampling type of this operator.", SplittedExampleSet.SAMPLING_NAMES, SplittedExampleSet.AUTOMATIC, false)); types.addAll(RandomGenerator.getRandomGeneratorParameters(this)); diff --git a/src/main/java/com/rapidminer/operator/preprocessing/transformation/Attribute2ExamplePivoting.java b/src/main/java/com/rapidminer/operator/preprocessing/transformation/Attribute2ExamplePivoting.java index 7c24e339a..6a662647a 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/transformation/Attribute2ExamplePivoting.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/transformation/Attribute2ExamplePivoting.java @@ -19,8 +19,10 @@ package com.rapidminer.operator.preprocessing.transformation; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Vector; import java.util.regex.Matcher; @@ -40,6 +42,7 @@ import com.rapidminer.operator.SimpleProcessSetupError; import com.rapidminer.operator.UserError; import com.rapidminer.operator.annotation.ResourceConsumptionEstimator; +import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.metadata.AttributeMetaData; import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; import com.rapidminer.operator.ports.metadata.ExampleSetPrecondition; @@ -108,7 +111,7 @@ protected MetaData modifyMetaData(ExampleSetMetaData metaData) throws UndefinedP String[] seriesNames = new String[numberOfSeries]; Pattern[] seriesPatterns = new Pattern[numberOfSeries]; - ArrayList> seriesAttributes = new ArrayList>(numberOfSeries); + ArrayList> seriesAttributes = new ArrayList<>(numberOfSeries); int[] attributeTypes = new int[numberOfSeries]; Iterator iterator = seriesList.iterator(); int j = 0; @@ -116,7 +119,7 @@ protected MetaData modifyMetaData(ExampleSetMetaData metaData) throws UndefinedP String[] pair = iterator.next(); seriesNames[j] = pair[0]; seriesPatterns[j] = Pattern.compile(pair[1]); - seriesAttributes.add(j, new Vector()); + seriesAttributes.add(j, new Vector<>()); j++; } String indexParamName = getParameterAsString(PARAMETER_INDEX_ATTRIBUTE); @@ -181,7 +184,7 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { String[] seriesNames = new String[numberOfSeries]; Pattern[] seriesPatterns = new Pattern[numberOfSeries]; - ArrayList> seriesAttributes = new ArrayList>(numberOfSeries); + ArrayList> seriesAttributes = new ArrayList<>(numberOfSeries); int[] attributeTypes = new int[numberOfSeries]; Iterator iterator = seriesList.iterator(); int j = 0; @@ -189,13 +192,13 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { String[] pair = iterator.next(); seriesNames[j] = pair[0]; seriesPatterns[j] = Pattern.compile(pair[1]); - seriesAttributes.add(j, new Vector()); + seriesAttributes.add(j, new Vector<>()); attributeTypes[j] = Ontology.ATTRIBUTE_VALUE; j++; } - Vector newAttributes = new Vector(); - Vector constantAttributes = new Vector(); + Vector newAttributes = new Vector<>(); + Vector constantAttributes = new Vector<>(); // identify series attributes and check attribute types Iterator attributes; @@ -221,12 +224,11 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { seriesAttributes.get(i).add(attribute); if (attributeTypes[i] != Ontology.ATTRIBUTE_VALUE) { if (attribute.getValueType() != attributeTypes[i]) { - throw new OperatorException("attributes have different value types: no conversion is performed"); + throw new UserError(this, "de_pivot.type_mismatch", attribute.getName(), Ontology.VALUE_TYPE_NAMES[attribute.getValueType()], Ontology.VALUE_TYPE_NAMES[attributeTypes[i]], seriesNames[i]); } } else { attributeTypes[i] = attribute.getValueType(); } - break; } } if (!matched) { @@ -246,7 +248,7 @@ public ExampleSet apply(ExampleSet exampleSet) throws OperatorException { for (int i = 0; i < numberOfSeries - 1; i++) { seriesLength = seriesAttributes.get(i).size(); if (seriesLength != seriesAttributes.get(i + 1).size()) { - throw new OperatorException("series must have the same length: no conversion is performed"); + throw new UserError(this, "de_pivot.length_mismatch", seriesNames[i], seriesAttributes.get(i).size(), seriesNames[i + 1], seriesAttributes.get(i + 1).size()); } } } @@ -350,7 +352,27 @@ public List getParameterTypes() { ParameterType type = new ParameterTypeList(PARAMETER_SERIES, "Maps a number of source attributes onto result attributes.", new ParameterTypeString("attribute_name", "Specifies the name of the resulting attribute"), - new ParameterTypeRegexp(PARAMETER_ATTRIBUTE_NAME_REGEX, "Attributes that forms series.", false)); + new ParameterTypeRegexp(PARAMETER_ATTRIBUTE_NAME_REGEX, "Attributes that forms series.", false) { + + private static final long serialVersionUID = 8133149560984042645L; + + @Override + public Collection getPreviewList() { + InputPort inPort = getInputPort(); + Collection regExpPreviewList = new LinkedList<>(); + if (inPort == null) { + return super.getPreviewList(); + } + MetaData metaData = inPort.getMetaData(); + if (metaData instanceof ExampleSetMetaData) { + ExampleSetMetaData emd = (ExampleSetMetaData) metaData; + for (AttributeMetaData amd : emd.getAllAttributes()) { + regExpPreviewList.add(amd.getName()); + } + } + return regExpPreviewList; + } + }); type.setExpert(false); types.add(type); type = new ParameterTypeString(PARAMETER_INDEX_ATTRIBUTE, "Name of newly created index attribute.", false, false); diff --git a/src/main/java/com/rapidminer/operator/preprocessing/transformation/aggregation/AbstractCountRatioAggregationFunction.java b/src/main/java/com/rapidminer/operator/preprocessing/transformation/aggregation/AbstractCountRatioAggregationFunction.java index 2ff251174..1282dcaaf 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/transformation/aggregation/AbstractCountRatioAggregationFunction.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/transformation/aggregation/AbstractCountRatioAggregationFunction.java @@ -60,6 +60,9 @@ public void postProcessing(List allAggregators) { // calculate total count for (Aggregator aggregator : allAggregators) { + if(aggregator == null){ + continue; + } double value = ((CountIncludingMissingsAggregator) aggregator).getCount(); if (Double.isNaN(value)) { totalCount = Double.NaN; @@ -68,8 +71,11 @@ public void postProcessing(List allAggregators) { totalCount += value; } - // devide by total count + // divide by total count for (Aggregator aggregator : allAggregators) { + if(aggregator == null){ + continue; + } CountIncludingMissingsAggregator countAggregator = (CountIncludingMissingsAggregator) aggregator; countAggregator.setCount((countAggregator.getCount() / totalCount) * getRatioFactor()); } diff --git a/src/main/java/com/rapidminer/operator/preprocessing/transformation/aggregation/AggregationOperator.java b/src/main/java/com/rapidminer/operator/preprocessing/transformation/aggregation/AggregationOperator.java index 30ad3087d..f87e53524 100644 --- a/src/main/java/com/rapidminer/operator/preprocessing/transformation/aggregation/AggregationOperator.java +++ b/src/main/java/com/rapidminer/operator/preprocessing/transformation/aggregation/AggregationOperator.java @@ -729,12 +729,14 @@ public List getParameterTypes() { type.setExpert(false); types.add(type); - types.add(new ParameterTypeList(PARAMETER_AGGREGATION_ATTRIBUTES, "The attributes which should be aggregated.", + ParameterTypeList aggregation_attribute = new ParameterTypeList(PARAMETER_AGGREGATION_ATTRIBUTES, "The attributes which should be aggregated.", new ParameterTypeAttribute("aggregation_attribute", "Specifies the attribute which is aggregated.", getExampleSetInputPort(), false), new ParameterTypeStringCategory(PARAMETER_AGGREGATION_FUNCTIONS, "The type of the used aggregation function.", functions, functions[0]), - false)); + false); + aggregation_attribute.setPrimary(true); + types.add(aggregation_attribute); types.add(new ParameterTypeAttributes(PARAMETER_GROUP_BY_ATTRIBUTES, "Performs a grouping by the values of the attributes by the selected attributes.", getExampleSetInputPort(), true, false)); diff --git a/src/main/java/com/rapidminer/operator/repository/RepositoryEntryDeleteOperator.java b/src/main/java/com/rapidminer/operator/repository/RepositoryEntryDeleteOperator.java index 54333bab9..d9fc8fb8d 100644 --- a/src/main/java/com/rapidminer/operator/repository/RepositoryEntryDeleteOperator.java +++ b/src/main/java/com/rapidminer/operator/repository/RepositoryEntryDeleteOperator.java @@ -18,6 +18,8 @@ */ package com.rapidminer.operator.repository; +import java.util.List; + import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; import com.rapidminer.operator.UserError; @@ -27,8 +29,6 @@ import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; -import java.util.List; - /** * An operator to delete repository entries within a process. If the entry does not exists it won't @@ -73,7 +73,9 @@ public void doWork() throws OperatorException { public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeRepositoryLocation(ELEMENT_TO_DELETE, "Element that should be deleted", true, true, false)); + ParameterType type = new ParameterTypeRepositoryLocation(ELEMENT_TO_DELETE, "Element that should be deleted", true, true, false); + type.setPrimary(true); + types.add(type); return types; } diff --git a/src/main/java/com/rapidminer/operator/repository/RepositoryEntryRenameOperator.java b/src/main/java/com/rapidminer/operator/repository/RepositoryEntryRenameOperator.java index 6be2d8144..18149c915 100644 --- a/src/main/java/com/rapidminer/operator/repository/RepositoryEntryRenameOperator.java +++ b/src/main/java/com/rapidminer/operator/repository/RepositoryEntryRenameOperator.java @@ -18,6 +18,8 @@ */ package com.rapidminer.operator.repository; +import java.util.List; + import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.OperatorException; import com.rapidminer.operator.UserError; @@ -31,8 +33,6 @@ import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; -import java.util.List; - /** * An Operator to rename repository entries. The user can select the entry to rename, a new name and @@ -133,7 +133,9 @@ public void doWork() throws OperatorException { public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeRepositoryLocation(ELEMENT_TO_RENAME, "Entry that should be renamed", true, true, false)); + ParameterType type = new ParameterTypeRepositoryLocation(ELEMENT_TO_RENAME, "Entry that should be renamed", true, true, false); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeString(NEW_ELEMENT_NAME, "New entry name", false, false)); types.add(new ParameterTypeBoolean(OVERWRITE, "Overwrite already existing entry with same name?", false, false)); diff --git a/src/main/java/com/rapidminer/operator/tools/SendMailOperator.java b/src/main/java/com/rapidminer/operator/tools/SendMailOperator.java index 4751bdbc0..3d2f7b625 100644 --- a/src/main/java/com/rapidminer/operator/tools/SendMailOperator.java +++ b/src/main/java/com/rapidminer/operator/tools/SendMailOperator.java @@ -153,6 +153,7 @@ public List getParameterTypes() { ParameterType type = new ParameterTypeText(PARAMETER_BODY_PLAIN, "Body of the email.", TextType.PLAIN, true); type.registerDependencyCondition(new BooleanParameterCondition(this, PARAMETER_USE_HTML, true, false)); type.setExpert(false); + type.setPrimary(true); types.add(type); ParameterTypeText typeText = new ParameterTypeText(PARAMETER_BODY_HTML, "Body of the email in HTML format.", diff --git a/src/main/java/com/rapidminer/operator/util/annotations/AnnotateOperator.java b/src/main/java/com/rapidminer/operator/util/annotations/AnnotateOperator.java index 0efc87355..e30dfb32a 100644 --- a/src/main/java/com/rapidminer/operator/util/annotations/AnnotateOperator.java +++ b/src/main/java/com/rapidminer/operator/util/annotations/AnnotateOperator.java @@ -18,6 +18,8 @@ */ package com.rapidminer.operator.util.annotations; +import java.util.List; + import com.rapidminer.operator.Annotations; import com.rapidminer.operator.IOObject; import com.rapidminer.operator.Operator; @@ -31,8 +33,6 @@ import com.rapidminer.parameter.ParameterTypeString; import com.rapidminer.parameter.ParameterTypeStringCategory; -import java.util.List; - /** * @author Marius Helf @@ -107,12 +107,14 @@ public void doWork() throws OperatorException { public List getParameterTypes() { List types = super.getParameterTypes(); - types.add(new ParameterTypeList( + ParameterType type = new ParameterTypeList( PARAMETER_ANNOTATIONS, "Defines the pairs of annotation names and annotation values. Click the button, select or type an annotation name into the left input field and enter its value into the right field. You can specify an arbitrary amount of annotations here. Please note that it is not possible to create empty annotations.", new ParameterTypeStringCategory(PARAMETER_NAME, "The name of the annotation", Annotations.ALL_KEYS_IOOBJECT, Annotations.ALL_KEYS_IOOBJECT[0]), new ParameterTypeString(PARAMETER_VALUE, - "The value of the annotation", true), false)); + "The value of the annotation", true), false); + type.setPrimary(true); + types.add(type); types.add(new ParameterTypeStringCategory(PARAMETER_DUPLICATE_HANDLING, "Indicates what should happen if duplicate annotation names are specified.", DUPLICATE_HANDLING_LIST, diff --git a/src/main/java/com/rapidminer/operator/validation/clustering/CentroidBasedEvaluator.java b/src/main/java/com/rapidminer/operator/validation/clustering/CentroidBasedEvaluator.java index 84884cbc8..86cab7e2e 100644 --- a/src/main/java/com/rapidminer/operator/validation/clustering/CentroidBasedEvaluator.java +++ b/src/main/java/com/rapidminer/operator/validation/clustering/CentroidBasedEvaluator.java @@ -1,23 +1,25 @@ /** * Copyright (C) 2001-2018 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero 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 * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.operator.validation.clustering; +import java.util.List; + import com.rapidminer.example.Example; import com.rapidminer.example.ExampleSet; import com.rapidminer.operator.Operator; @@ -41,15 +43,13 @@ import com.rapidminer.tools.math.similarity.divergences.SquaredEuclideanDistance; import com.rapidminer.tools.math.similarity.numerical.EuclideanDistance; -import java.util.List; - /** * An evaluator for centroid based clustering methods. The average within cluster distance is * calculated by averaging the distance between the centroid and all examples of a cluster. - * + * * @author Sebastian Land, Michael Wurst, Ingo Mierswa - * + * */ public class CentroidBasedEvaluator extends Operator { @@ -65,9 +65,9 @@ public class CentroidBasedEvaluator extends Operator { private double daviesBouldin; - public static final String[] CRITERIA_LIST = { "Avg. within centroid distance", "Davies Bouldin" }; + public static final String[] CRITERIA_LIST = {"Avg. within centroid distance", "Davies Bouldin"}; - public static final String[] CRITERIA_LIST_SHORT = { "avg_within_distance", "DaviesBouldin" }; + public static final String[] CRITERIA_LIST_SHORT = {"avg_within_distance", "DaviesBouldin"}; private InputPort exampleSetInput = getInputPorts().createPort("example set", ExampleSet.class); private InputPort clusterModelInput = getInputPorts().createPort("cluster model", CentroidClusterModel.class); @@ -175,7 +175,18 @@ public void doWork() throws OperatorException { clusterModelOutput.deliver(clusterModel); } - private double[] getAverageWithinDistance(CentroidClusterModel model, ExampleSet exampleSet) throws OperatorException { + /** + * Calculates and delivers the average within distances for all clusters and the given data set. + * The last entry of the resulting array is the average across all clusters. + * + * @param model + * the cluster model + * @param exampleSet + * the clustered data + * @return the average within distance for all clusters plus the total avg within distance (last entry) + * @since 8.2 + */ + public static double[] getAverageWithinDistance(CentroidClusterModel model, ExampleSet exampleSet) throws OperatorException { DistanceMeasure measure = new SquaredEuclideanDistance(); measure.init(exampleSet); int numberOfClusters = model.getNumberOfClusters(); @@ -205,7 +216,17 @@ private double[] getAverageWithinDistance(CentroidClusterModel model, ExampleSet return result; } - private double getDaviesBouldin(CentroidClusterModel model, ExampleSet exampleSet) throws OperatorException { + /** + * Calculates and delivers the Davies Bouldin Index for all clusters and the given data set. + * + * @param model + * the cluster model + * @param exampleSet + * the clustered data + * @return the Davies Bouldin Index for this clustering + * @since 8.2 + */ + public static double getDaviesBouldin(CentroidClusterModel model, ExampleSet exampleSet) throws OperatorException { DistanceMeasure measure = new EuclideanDistance(); measure.init(exampleSet); int numberOfClusters = model.getNumberOfClusters(); diff --git a/src/main/java/com/rapidminer/operator/visualization/ProcessLogOperator.java b/src/main/java/com/rapidminer/operator/visualization/ProcessLogOperator.java index 831af53eb..98d8295a6 100644 --- a/src/main/java/com/rapidminer/operator/visualization/ProcessLogOperator.java +++ b/src/main/java/com/rapidminer/operator/visualization/ProcessLogOperator.java @@ -341,6 +341,7 @@ public List getParameterTypes() { new ParameterTypeString(PARAMETER_COLUMN_NAME, "The name of the column in the process log."), new ParameterTypeValue(PARAMETER_COLUMN_VALUE, "operator.OPERATORNAME.[value|parameter].VALUE_NAME")); type.setExpert(false); + type.setPrimary(true); types.add(type); types.add(new ParameterTypeCategory(PARAMETER_SORTING_TYPE, diff --git a/src/main/java/com/rapidminer/parameter/ParameterType.java b/src/main/java/com/rapidminer/parameter/ParameterType.java index cad4dfbfb..4a5c1181f 100644 --- a/src/main/java/com/rapidminer/parameter/ParameterType.java +++ b/src/main/java/com/rapidminer/parameter/ParameterType.java @@ -91,6 +91,11 @@ public abstract class ParameterType implements Comparable, Serial */ private boolean expert = true; + /** + * Indicates if a parameter is a primary parameter of an operator, i.e. one that gets activated with a double-click. + */ + private boolean primary = false; + /** * Indicates if this parameter is hidden and is not shown in the GUI. May be used in conjunction * with a configuration wizard which lets the user configure the parameter. @@ -328,6 +333,28 @@ public boolean isSensitive() { return true; } + /** + * Sets if this parameter type is a primary parameter of an operator, i.e. one that can be opened with a double-click. If not set, defaults to {@code false}, except for {@link ParameterTypeConfiguration}. + * If more than one parameter type of an operator is set to primary, the first one returned in the parameters collection will be considered the primary one. + * + * @param primary + * {@code true} if it is a primary parameter; {@code false} otherwise + * @since 8.2 + */ + public void setPrimary(final boolean primary) { + this.primary = primary; + } + + /** + * Returns if this is a primary parameter of an operator, i.e. one that can be opened with a double-click. Defaults to {@code false}. + * + * @return {@code true} if it is a primary parameter; {@code false} otherwise + * @since 8.2 + */ + public boolean isPrimary() { + return primary; + } + /** * This method gives a hook for the parameter type to react on a renaming of an operator. It * must return the correctly modified String value. The default implementation does nothing. diff --git a/src/main/java/com/rapidminer/parameter/ParameterTypeConfiguration.java b/src/main/java/com/rapidminer/parameter/ParameterTypeConfiguration.java index c78dcbfde..a085b7b9e 100644 --- a/src/main/java/com/rapidminer/parameter/ParameterTypeConfiguration.java +++ b/src/main/java/com/rapidminer/parameter/ParameterTypeConfiguration.java @@ -71,6 +71,8 @@ public ParameterTypeConfiguration(Class wi this.parameters = parameters; this.wizardListener = wizardListener; this.wizardConstructionArguments = constructorArguments; + + setPrimary(true); } /** diff --git a/src/main/java/com/rapidminer/parameter/ParameterTypeDateFormat.java b/src/main/java/com/rapidminer/parameter/ParameterTypeDateFormat.java index a9c957d23..19f6e59ad 100644 --- a/src/main/java/com/rapidminer/parameter/ParameterTypeDateFormat.java +++ b/src/main/java/com/rapidminer/parameter/ParameterTypeDateFormat.java @@ -18,27 +18,65 @@ */ package com.rapidminer.parameter; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Locale; + +import com.rapidminer.operator.Operator; +import com.rapidminer.operator.ProcessSetupError.Severity; +import com.rapidminer.operator.SimpleProcessSetupError; +import com.rapidminer.operator.UserError; import com.rapidminer.operator.ports.InputPort; +import com.rapidminer.operator.ports.quickfix.ParameterSettingQuickFix; /** - * A ParameterType for DateFormats. - * - * @author Simon Fischer + * A ParameterType for DateFormats. Also holds constants for date(/time) formats and static methods to create checked + * {@link SimpleDateFormat SimpleDateFormats}. * + * @author Simon Fischer, Jan Czogalla */ public class ParameterTypeDateFormat extends ParameterTypeStringCategory { private static final long serialVersionUID = 1L; + public static final String INVALID_DATE_FORMAT = "invalid_date_format"; + public static final String INVALID_DATE_FORMAT_PARAMETER = INVALID_DATE_FORMAT + "_parameter"; private transient InputPort inPort; private ParameterTypeAttribute attributeParameter; - public static final String[] PREDEFINED_DATE_FORMATS = new String[] { "", "MM/dd/yyyy", "MM/dd/yyyy h:mm a", - "dd/MM/yyyy", "dd/MM/yyyy HH:mm", "yyyy.MM.dd G 'at' HH:mm:ss z", "EEE, MMM d, ''yy", "h:mm a", + public static final String PARAMETER_DATE_FORMAT = "date_format"; + + public static final String TIME_FORMAT_H_MM_A = "h:mm a"; + public static final String DATE_FORMAT_MM_DD_YYYY = "MM/dd/yyyy"; + public static final String DATE_FORMAT_YYYY_MM_DD = "yyyy-MM-dd"; + public static final String DATE_TIME_FORMAT_MM_DD_YYYY_H_MM_A = DATE_FORMAT_MM_DD_YYYY + " " + TIME_FORMAT_H_MM_A; + public static final String DATE_TIME_FORMAT_YYYY_MM_DD_HH_MM_SS = DATE_FORMAT_YYYY_MM_DD + " HH:mm:ss"; + public static final String DATE_TIME_FORMAT_ISO8601_UTC_MS = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + public static final String DATE_TIME_FORMAT_M_D_YY_H_MM_A = "M/d/yy " + TIME_FORMAT_H_MM_A; + public static final String[] PREDEFINED_DATE_FORMATS = new String[]{"", DATE_FORMAT_MM_DD_YYYY, DATE_TIME_FORMAT_MM_DD_YYYY_H_MM_A, + "dd/MM/yyyy", "dd/MM/yyyy HH:mm", "yyyy.MM.dd G 'at' HH:mm:ss z", "EEE, MMM d, ''yy", TIME_FORMAT_H_MM_A, "hh 'o''clock' a, zzzz", "K:mm a, z", "yyyy.MMMMM.dd GGG hh:mm aaa", "EEE, d MMM yyyy HH:mm:ss Z", - "yyMMddHHmmssZ", "yyyy-MM-dd'T'HH:mm:ss.SSSZ", "yyyy-MM-dd HH:mm:ss", "M/d/yy h:mm a" }; + "yyMMddHHmmssZ", DATE_TIME_FORMAT_ISO8601_UTC_MS, DATE_TIME_FORMAT_YYYY_MM_DD_HH_MM_SS, DATE_TIME_FORMAT_M_D_YY_H_MM_A, DATE_FORMAT_YYYY_MM_DD}; + + /** + * Simple constructor with key {@value PARAMETER_DATE_FORMAT} and default description. + * + * @since 8.2 + */ + public ParameterTypeDateFormat() { + this(null, null); + } + + /** + * Simple constructor with default key and description and example set meta data available. + * + * @since 8.2 + */ + public ParameterTypeDateFormat(ParameterTypeAttribute attributeParameter, InputPort inPort) { + this(attributeParameter, PARAMETER_DATE_FORMAT, "The parse format of the date values, for example \"yyyy/MM/dd\".", inPort, false); + } /** * This is the constructor for date format if no example set meta data is available. @@ -93,4 +131,87 @@ public ParameterTypeAttribute getAttributeParameterType() { public boolean isSensitive() { return false; } + + /** + * Convenience method, same as {@link #createCheckedDateFormat(Operator, String, Locale, boolean) createCheckedDateFormat(op, pattern, null, false)}. + * + * @throws UserError + * iff the pattern is not valid + * @since 8.2 + */ + public static SimpleDateFormat createCheckedDateFormat(Operator op, String pattern) throws UserError { + return createCheckedDateFormat(op, pattern, null, false); + } + + /** + * Convenience method, same as {@link #createCheckedDateFormat(Operator, String, Locale, boolean) createCheckedDateFormat(null, pattern, loc, false)}. + * + * @throws UserError + * iff the pattern is not valid + * @since 8.2 + */ + public static SimpleDateFormat createCheckedDateFormat(String pattern, Locale loc) throws UserError { + return createCheckedDateFormat(null, pattern, loc, false); + } + + /** + * Convenience method, same as {@link #createCheckedDateFormat(Operator, String, Locale, boolean) createCheckedDateFormat(op, null, loc, isSetup)}. + * + * @throws UserError + * iff the pattern is not valid and {@code isSetup} is {@code false} + * @since 8.2 + */ + public static SimpleDateFormat createCheckedDateFormat(Operator op, Locale loc, boolean isSetup) throws UserError { + return createCheckedDateFormat(op, null, loc, isSetup); + } + + /** + * Creates a {@link SimpleDateFormat} from the given parameters if possible. This method returns {@code null} if no + * operator was specified and {@code pattern} is {@code null} or if an error occurs during setup. + *

+ * A date format pattern string is then extracted from the {@link Operator Operator's} parameter if {@code pattern} is {@code null}, + * otherwise the provided {@code pattern} is used. Depending on {@code isSetup} either a {@link SimpleProcessSetupError} + * is added to the given operator, or a {@link UserError} is thrown iff the format pattern is not valid. + * + * @param op + * the operator to check for the parameter {@value #PARAMETER_DATE_FORMAT}; can be {@code null} + * @param pattern + * the pattern to check for validity; can be {@code null} + * @param loc + * the locale; if {@code null}, the default locale will be used + * @param inSetup + * whether the error should be a setup error (building process) or user error (running process) + * @return a {@link SimpleDateFormat} instance or {@code null} + * @throws UserError + * iff the pattern is not valid and {@code isSetup} is {@code false} + * @since 8.2 + */ + public static SimpleDateFormat createCheckedDateFormat(Operator op, String pattern, Locale loc, boolean inSetup) throws UserError { + if (op == null && pattern == null) { + // insufficient information => ignore + return null; + } + String dateFormat = ""; + try { + if (pattern != null) { + dateFormat = pattern; + } else { + dateFormat = op.getParameter(PARAMETER_DATE_FORMAT); + } + return loc == null ? new SimpleDateFormat(dateFormat) : new SimpleDateFormat(dateFormat, loc); + } catch (UndefinedParameterError e) { + // parameter not defined => ignore + } catch (IllegalArgumentException e) { + Object[] arguments = {dateFormat, e.getMessage(), PARAMETER_DATE_FORMAT.replaceAll("_", " ")}; + if (!inSetup) { + throw new UserError(op, INVALID_DATE_FORMAT, arguments); + } + if (op != null) { + op.addError(new SimpleProcessSetupError(Severity.ERROR, op.getPortOwner(), + Collections.singletonList(new ParameterSettingQuickFix(op, PARAMETER_DATE_FORMAT)), + INVALID_DATE_FORMAT_PARAMETER, arguments)); + } + } + return null; + } } diff --git a/src/main/java/com/rapidminer/repository/RepositoryEntryNotFoundException.java b/src/main/java/com/rapidminer/repository/RepositoryEntryNotFoundException.java new file mode 100644 index 000000000..7c5a5d0ba --- /dev/null +++ b/src/main/java/com/rapidminer/repository/RepositoryEntryNotFoundException.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.repository; + +/** + * Thrown if a repository entry cannot be found. + * + * @author Marco Boeck + * @since 8.2 + */ +public class RepositoryEntryNotFoundException extends RepositoryException { + + + public RepositoryEntryNotFoundException() {} + + public RepositoryEntryNotFoundException(String message) { + super(message); + } + + public RepositoryEntryNotFoundException(Throwable cause) { + super(cause); + } + + public RepositoryEntryNotFoundException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/com/rapidminer/repository/RepositoryEntryWrongTypeException.java b/src/main/java/com/rapidminer/repository/RepositoryEntryWrongTypeException.java new file mode 100644 index 000000000..5c83f0dc6 --- /dev/null +++ b/src/main/java/com/rapidminer/repository/RepositoryEntryWrongTypeException.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. +*/ +package com.rapidminer.repository; + +/** + * Thrown if a repository entry was of the wrong type. + * + * @author Marco Boeck + * @since 8.2 + */ +public class RepositoryEntryWrongTypeException extends RepositoryException { + + + public RepositoryEntryWrongTypeException() {} + + public RepositoryEntryWrongTypeException(String message) { + super(message); + } + + public RepositoryEntryWrongTypeException(Throwable cause) { + super(cause); + } + + public RepositoryEntryWrongTypeException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/com/rapidminer/repository/RepositoryLocation.java b/src/main/java/com/rapidminer/repository/RepositoryLocation.java index a0f912424..af8afa736 100644 --- a/src/main/java/com/rapidminer/repository/RepositoryLocation.java +++ b/src/main/java/com/rapidminer/repository/RepositoryLocation.java @@ -285,16 +285,25 @@ public static boolean isAbsolute(String loc) { public Folder createFoldersRecursively() throws RepositoryException { Entry entry = locateEntry(); if (entry == null) { - Folder folder = parent().createFoldersRecursively(); - Folder child = folder.createFolder(getName()); - return child; - } else { - if (entry instanceof Folder) { - return (Folder) entry; - } else { - throw new RepositoryException(toString() + " is not a folder."); + Folder parentFolder = parent().createFoldersRecursively(); + try { + entry = parentFolder.createFolder(getName()); + } catch (RepositoryException re) { + //Recover from concurrent createFolder calls + entry = locateEntry(); + //Rethrow the RepositoryException if recovery failed + if (!(entry instanceof Folder)) { + throw re; + } } } + + if (entry instanceof Folder) { + return (Folder) entry; + } else { + throw new RepositoryException(toString() + " is not a folder."); + } + } @Override diff --git a/src/main/java/com/rapidminer/repository/RepositoryManager.java b/src/main/java/com/rapidminer/repository/RepositoryManager.java index fcba0b750..d4ef1b604 100644 --- a/src/main/java/com/rapidminer/repository/RepositoryManager.java +++ b/src/main/java/com/rapidminer/repository/RepositoryManager.java @@ -107,11 +107,11 @@ public void entryAdded(Entry newEntry, Folder parent) {} * @author Sabrina Kirstein * */ - public static enum RepositoryType { + public enum RepositoryType { /** * The order of repository types is very important as it is used in the - * {@link RepositoryManager#repositoryComparator} + * {@link RepositoryTools#REPOSITORY_COMPARATOR} */ RESOURCES, DB, LOCAL, REMOTE, OTHER; @@ -208,7 +208,19 @@ public static void initCustomRepositories() { } /** - * Replaces the used {@link RepositoryProvider}. The {@link DefaultRepositoryProvider} will be + * Refreshes the sample repository. Will be called by + * {@link RapidMiner#init()} after {@link Plugin#initAll()} and {@link RepositoryManager#init()} + */ + public static void refreshSampleRepository() { + try { + sampleRepository.refresh(); + } catch (Exception e) { + LOGGER.log(Level.INFO, "com.rapidminer.repository.RepositoryManager.sample_repository_refresh_failed", e); + } + } + + /** + * Replaces the used {@link RepositoryProvider}. The {@link FileRepositoryProvider} will be * used as default. * * @param repositoryProvider @@ -394,7 +406,7 @@ public void createRepositoryIfNoneIsDefined() { * will stores the settings to a XML file. Use {@link #setProvider(RepositoryProvider)} to * replace this behavior. * - * @see #load() + * @see #load */ public void save() { provider.save(getRepositories()); @@ -736,7 +748,7 @@ public Entry locate(Repository repository, String path, boolean failIfBlocks) th } } // missed entry -> refresh and try again - if (folder.canRefreshChild(splitted[index])) { + if (retryCount == 0 && folder.canRefreshChild(splitted[index])) { folder.refresh(); } else { break; @@ -761,7 +773,7 @@ public Entry locate(Repository repository, String path, boolean failIfBlocks) th break; } else { // missed entry -> refresh and try again - if (folder.canRefreshChild(splitted[index])) { + if (retryCount == 0 && folder.canRefreshChild(splitted[index])) { folder.refresh(); } else { break; diff --git a/src/main/java/com/rapidminer/repository/gui/RepositoryLocationChooser.java b/src/main/java/com/rapidminer/repository/gui/RepositoryLocationChooser.java index a0113d6f7..bef27310f 100644 --- a/src/main/java/com/rapidminer/repository/gui/RepositoryLocationChooser.java +++ b/src/main/java/com/rapidminer/repository/gui/RepositoryLocationChooser.java @@ -167,6 +167,11 @@ public RepositoryLocationChooser(Dialog owner, RepositoryLocation resolveRelativ this(owner, resolveRelativeTo, initialValue, true, false); } + /** @since 8.2 */ + public RepositoryLocationChooser(Dialog owner, RepositoryLocation resolveRelativeTo, String initialValue, Color backgroundColor) { + this(owner, resolveRelativeTo, initialValue, true, false, false, false, backgroundColor); + } + public RepositoryLocationChooser(Dialog owner, RepositoryLocation resolveRelativeTo, String initialValue, final boolean allowEntries, final boolean allowFolders) { this(owner, resolveRelativeTo, initialValue, allowEntries, allowFolders, false); @@ -206,6 +211,8 @@ public RepositoryLocationChooser(Dialog owner, RepositoryLocation resolveRelativ tree = new RepositoryTree(owner, !allowEntries, onlyWriteableRepositories, false, backgroundColor); if (initialValue != null) { + // called twice to fix bug that it only selects parent on first time + tree.expandIfExists(resolveRelativeTo, initialValue); if (tree.expandIfExists(resolveRelativeTo, initialValue)) { locationField.setText(""); locationFieldRepositoryEntry.setText(""); @@ -215,6 +222,8 @@ public RepositoryLocationChooser(Dialog owner, RepositoryLocation resolveRelativ List repositories = RepositoryManager.getInstance(null).getRepositories(); for (Repository r : repositories) { if (!r.isReadOnly() && LocalRepository.class.isInstance(r)) { + // called twice to fix bug that it only selects parent on first time + tree.expandIfExists(null, r.getLocation().getAbsoluteLocation()); if (tree.expandIfExists(null, r.getLocation().getAbsoluteLocation())) { locationField.setText(""); locationFieldRepositoryEntry.setText(""); diff --git a/src/main/java/com/rapidminer/repository/gui/RepositoryTree.java b/src/main/java/com/rapidminer/repository/gui/RepositoryTree.java index 8418dbdd4..58f59d436 100644 --- a/src/main/java/com/rapidminer/repository/gui/RepositoryTree.java +++ b/src/main/java/com/rapidminer/repository/gui/RepositoryTree.java @@ -41,6 +41,7 @@ import java.util.LinkedList; import java.util.List; import java.util.logging.Level; +import java.util.stream.Collectors; import javax.swing.Action; import javax.swing.Icon; import javax.swing.JComponent; @@ -63,6 +64,7 @@ import com.rapidminer.gui.dnd.DragListener; import com.rapidminer.gui.dnd.RepositoryLocationList; import com.rapidminer.gui.dnd.TransferableOperator; +import com.rapidminer.gui.dnd.TransferableRepositoryEntry; import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.gui.tools.ProgressThreadDialog; import com.rapidminer.gui.tools.RepositoryGuiTools; @@ -110,6 +112,7 @@ import com.rapidminer.tools.LogService; import com.rapidminer.tools.PasswordInputCanceledException; import com.rapidminer.tools.ProgressListener; +import com.rapidminer.tools.usagestats.ActionStatisticsCollector; /** @@ -202,9 +205,17 @@ public boolean importData(final TransferSupport ts) { } else if (flavors.contains(TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_LIST_FLAVOR)) { // Multiple repository entries - RepositoryLocationList locationList = (RepositoryLocationList) ts.getTransferable() + Object transferData = ts.getTransferable() .getTransferData(TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_LIST_FLAVOR); - List locations = locationList.getAll(); + List locations; + if (transferData instanceof RepositoryLocationList) { + locations = ((RepositoryLocationList) transferData).getAll(); + } else if (transferData instanceof RepositoryLocation[]) { + locations = Arrays.asList((RepositoryLocation[]) transferData); + } else { + // should not happen + return false; + } return copyOrMoveRepositoryEntries(droppedOnEntry, locations, ts); } else if (flavors.contains(DataFlavor.javaFileListFlavor)) { @@ -602,82 +613,25 @@ public int getSourceActions(JComponent c) { @Override protected Transferable createTransferable(JComponent c) { - final TreePath[] treePaths = getSelectionPaths(); - if (treePaths.length == 0) { // Nothing selected - return null; - - } else if (treePaths.length == 1) { + } + RepositoryLocation[] repositoryLocations; + if (treePaths.length == 1) { // Exactly one item selected - - Entry e = (Entry) treePaths[0].getLastPathComponent(); - final RepositoryLocation location = e.getLocation(); - return new Transferable() { - - @Override - public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { - if (flavor.equals(DataFlavor.stringFlavor)) { - return location.getAbsoluteLocation(); - } else if (TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR.equals(flavor)) { - return location; - } else { - throw new UnsupportedFlavorException(flavor); - } - } - - @Override - public DataFlavor[] getTransferDataFlavors() { - return new DataFlavor[] { TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR, - DataFlavor.stringFlavor }; - } - - @Override - public boolean isDataFlavorSupported(DataFlavor flavor) { - return TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR.equals(flavor) - || DataFlavor.stringFlavor.equals(flavor); - } - }; - + repositoryLocations = new RepositoryLocation[]{((Entry) treePaths[0].getLastPathComponent()).getLocation()}; } else { // Multiple entries selected - - final RepositoryLocationList locationList = new RepositoryLocationList(); - for (TreePath treePath : treePaths) { - locationList.add(((Entry) treePath.getLastPathComponent()).getLocation()); - } - locationList.removeIntersectedLocations(); - - return new Transferable() { - - final RepositoryLocationList locations = locationList; - - @Override - public boolean isDataFlavorSupported(DataFlavor flavor) { - return TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_LIST_FLAVOR.equals(flavor) - || DataFlavor.stringFlavor.equals(flavor); - } - - @Override - public DataFlavor[] getTransferDataFlavors() { - return new DataFlavor[] { TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_LIST_FLAVOR, - DataFlavor.stringFlavor }; - } - - @Override - public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { - if (TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_LIST_FLAVOR.equals(flavor)) { - return locations; - } else if (DataFlavor.stringFlavor.equals(flavor)) { - return locations.toString(); - } else { - throw new UnsupportedFlavorException(flavor); - } - } - }; + List locationList = Arrays.stream(treePaths). + map(p -> ((Entry) p.getLastPathComponent()).getLocation()).collect(Collectors.toList()); + repositoryLocations = RepositoryLocation.removeIntersectedLocations(locationList).toArray(new RepositoryLocation[0]); } + TransferableRepositoryEntry transferable = new TransferableRepositoryEntry(repositoryLocations); + transferable.setUsageStatsLogger( + () -> ActionStatisticsCollector.getInstance().log(ActionStatisticsCollector.TYPE_REPOSITORY_TREE, "inserted", null)); + return transferable; } @Override @@ -689,11 +643,10 @@ public Icon getVisualRepresentation(Transferable t) { * Checks, if the current operation is a MOVE operation. If not, it is a COPY operation. * * @param ts - * Provides info about the current operation + * Provides info about the current operation * @param isRepositoryInLocations - * This is true, if the sources to copy contain a repository. Repositories can - * not be moved. This results in a copy operation. - * @return true, if the operation is a move operation + * This is true, if the sources to copy contain a repository. Repositories can + * not be moved. This results in a copy operation. * @return false, if the operation is not a move operation (e.g. copy) */ private boolean isMoveOperation(TransferSupport ts, boolean isRepositoryInLocations) { @@ -850,7 +803,6 @@ public RepositoryTree(Dialog owner, boolean onlyFolders, boolean onlyWritableRep DELETE_ACTION.addToActionMap(this, "delete", WHEN_FOCUSED); REFRESH_ACTION.addToActionMap(this, WHEN_FOCUSED); - setLargeModel(true); setRowHeight(Math.max(TREE_ROW_HEIGHT, getRowHeight())); setRootVisible(false); setShowsRootHandles(true); @@ -899,10 +851,9 @@ public void mouseClicked(MouseEvent e) { if (getSelectionCount() == 1) { if (e.getClickCount() == 2) { TreePath path = getSelectionPath(); - if (path == null) { - return; + if (path != null && path.getLastPathComponent() instanceof Entry) { + fireLocationSelected((Entry) path.getLastPathComponent()); } - fireLocationSelected((Entry) path.getLastPathComponent()); } } } diff --git a/src/main/java/com/rapidminer/repository/gui/RepositoryTreeModel.java b/src/main/java/com/rapidminer/repository/gui/RepositoryTreeModel.java index f8422fe47..e5d364506 100644 --- a/src/main/java/com/rapidminer/repository/gui/RepositoryTreeModel.java +++ b/src/main/java/com/rapidminer/repository/gui/RepositoryTreeModel.java @@ -19,10 +19,12 @@ package com.rapidminer.repository.gui; import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import javax.swing.JTree; @@ -61,7 +63,7 @@ public class RepositoryTreeModel implements TreeModel { // sort the entries every time one of these methods is called. // We're using a HashMap to cache the location of folders as keys while storing their subfolders // and entries as their values in a sorted list. - private final HashMap> sortedRepositoryEntriesHashMap = new HashMap<>(); + private final Map> sortedRepositoryEntriesHashMap = new ConcurrentHashMap<>(); private static final String PENDING_FOLDER_NAME = "Pending..."; @@ -124,10 +126,11 @@ public void entryRemoved(final Entry removedEntry, final Folder parent, final in l.treeNodesRemoved(e); l.treeStructureChanged(p); } + // Restore selected path / expansion state of parent + if (parentTree != null) { + treeUtil.restoreSelectionPaths(parentTree); + } }); - - // Restore selected path / expansion state of parent - SwingUtilities.invokeLater(() -> treeUtil.restoreSelectionPaths(parentTree)); } @Override @@ -142,40 +145,41 @@ public void entryChanged(final Entry entry) { } // fire event - final TreeModelEvent e = makeChangeEvent(entry); + SwingTools.invokeAndWait(() -> updateEntry(entry, true)); + } - final RepositoryTreeUtil treeUtil = new RepositoryTreeUtil(); + @Override + public void folderRefreshed(final Folder folder) { + SwingTools.invokeAndWait(() -> updateEntry(folder, false)); + sortedRepositoryEntriesHashMap.clear(); + } + + /** + * Update tree if it was refreshed or an entry was changed. + * + * @param entry the changed entry + * @param changed whether the entry was changed or just refreshed + * @since 8.1.2 + */ + private void updateEntry(Entry entry, boolean changed) { + RepositoryTreeUtil treeUtil = new RepositoryTreeUtil(); + TreeModelEvent e = makeChangeEvent(entry); if (parentTree != null) { treeUtil.saveExpansionState(parentTree); + if (!changed) { + //Fix for UI glitches if children of a refreshed folder are selected during a refresh + treeUtil.retainRootSelections(parentTree); + } } - - SwingTools.invokeAndWait(() -> { - for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) { + for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) { + if (changed) { l.treeNodesChanged(e); - l.treeStructureChanged(e); } - }); - if (parentTree != null) { - SwingUtilities.invokeLater(() -> treeUtil.restoreExpansionState(parentTree)); + l.treeStructureChanged(e); } - } - - @Override - public void folderRefreshed(final Folder folder) { - final RepositoryTreeUtil treeUtil = new RepositoryTreeUtil(); - final TreeModelEvent e = makeChangeEvent(folder); - SwingTools.invokeAndWait(() -> { - if (parentTree != null) { - treeUtil.saveExpansionState(parentTree); - } - for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) { - l.treeStructureChanged(e); - } - }); if (parentTree != null) { - SwingUtilities.invokeLater(() -> treeUtil.restoreExpansionState(parentTree)); + treeUtil.restoreExpansionState(parentTree); } - sortedRepositoryEntriesHashMap.clear(); } }; @@ -277,7 +281,11 @@ public Object getChild(Object parent, int index) { Folder folder = (Folder) parent; if (folder.willBlock()) { unblock(folder); - return PENDING_FOLDER_NAME; + if (index == 0) { + return PENDING_FOLDER_NAME; + } else { + return null; + } } else { try { List sortedEntries = sortedRepositoryEntriesHashMap.get(folder.getLocation()); @@ -319,18 +327,17 @@ public Object getChild(Object parent, int index) { } } - private final Set pendingFolders = new HashSet<>(); - private final Set brokenFolders = new HashSet<>(); + private final ConcurrentMap pendingFolders = new ConcurrentHashMap<>(); + private final Set brokenFolders = Collections.newSetFromMap(new ConcurrentHashMap<>()); /** * Asynchronously fetches data from the folder so it will no longer block and then notifies * listeners on the EDT. */ private void unblock(final Folder folder) { - if (pendingFolders.contains(folder)) { + if (pendingFolders.putIfAbsent(folder, Boolean.TRUE) != null) { return; } - pendingFolders.add(folder); new Thread("wait-for-" + folder.getName()) { @@ -340,8 +347,7 @@ public void run() { final List children = new ArrayList<>(); final AtomicBoolean folderBroken = new AtomicBoolean(false); try { - List subfolders = folder.getSubfolders(); - children.addAll(subfolders); // this may take some time + children.addAll(folder.getSubfolders()); // this may take some time children.addAll(folder.getDataEntries()); // this may take some time } catch (Exception e) { // this occurs for example if the remote repository is unreachable @@ -380,7 +386,6 @@ public void run() { brokenFolders.remove(folder); } } - }); } } diff --git a/src/main/java/com/rapidminer/repository/gui/RepositoryTreeUtil.java b/src/main/java/com/rapidminer/repository/gui/RepositoryTreeUtil.java index e013b3a17..431b5ce19 100644 --- a/src/main/java/com/rapidminer/repository/gui/RepositoryTreeUtil.java +++ b/src/main/java/com/rapidminer/repository/gui/RepositoryTreeUtil.java @@ -18,8 +18,11 @@ */ package com.rapidminer.repository.gui; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; - +import java.util.LinkedHashSet; +import java.util.List; import javax.swing.JTree; import javax.swing.tree.TreePath; @@ -45,8 +48,8 @@ public class RepositoryTreeUtil { /** Saved expanded nodes */ private HashSet expandedNodes; - /** Saved expanded repositories */ - private HashSet expandedRepositories; + /** Saved selected nodes */ + private HashSet selectedNodes; /** Saves single path */ public void saveSelectionPath(TreePath treePath) { @@ -82,24 +85,26 @@ public void restoreSelectionPaths(JTree tree) { */ public void saveExpansionState(JTree tree) { - saveSelectionPaths(tree.getSelectionPaths()); - expandedNodes = new HashSet<>(); - expandedRepositories = new HashSet<>(); + expandedNodes = new LinkedHashSet<>(); + selectedNodes = new LinkedHashSet<>(); for (int i = 0; i < tree.getRowCount(); i++) { TreePath path = tree.getPathForRow(i); - if (tree.isExpanded(path)) { + boolean isExpanded = tree.isExpanded(path); + boolean isSelected = tree.isPathSelected(path); + if (isExpanded ||isSelected) { Entry entry = (Entry) path.getLastPathComponent(); String absoluteLocation = entry.getLocation().getAbsoluteLocation(); - if (entry instanceof Repository) { - expandedRepositories.add(absoluteLocation); - } else { + if (isExpanded) { expandedNodes.add(absoluteLocation); } - + if (isSelected) { + selectedNodes.add(absoluteLocation); + } } } + } /** @@ -110,6 +115,7 @@ public void saveExpansionState(JTree tree) { * The related tree, containing the path(s) */ public void restoreExpansionState(JTree tree) { + List selectedPathList = new ArrayList<>(); for (int i = 0; i < tree.getRowCount(); i++) { TreePath path = tree.getPathForRow(i); // sanity check for concurrent refreshes @@ -118,19 +124,23 @@ public void restoreExpansionState(JTree tree) { if (entryObject instanceof Entry) { Entry entry = (Entry) entryObject; String absoluteLocation = entry.getLocation().getAbsoluteLocation(); - if (expandedRepositories.contains(absoluteLocation) || expandedNodes.contains(absoluteLocation)) { + if (expandedNodes.contains(absoluteLocation)) { tree.expandPath(path); } + if (selectedNodes.contains(absoluteLocation)) { + selectedPathList.add(path); + } } } } - - restoreSelectionPaths(tree); + if (!selectedPathList.isEmpty()) { + tree.setSelectionPaths(selectedPathList.toArray(new TreePath[0])); + } } /** * Calls {@link RepositoryLocation###locateEntry()} on every node which was saved with ## - * {@link #saveExpansionState(JTree)}. This calls {@link RepositoryManager###locate(Repository, + * {@link #saveExpansionState(JTree)}. This calls {@link com.rapidminer.repository.RepositoryManager#locate(Repository, * String, boolean)} which refreshes parent folders of missed entries. */ public void locateExpandedEntries() { @@ -145,4 +155,28 @@ public void locateExpandedEntries() { } } + /** + * Retain only the root selectedPaths + * + * @param tree + * The related tree, containing the path(s) + */ + public void retainRootSelections(JTree tree) { + if (selectedPaths == null) { + selectedPaths = tree.getSelectionPaths(); + } + if (selectedPaths != null) { + List parents = Arrays.asList(selectedPaths); + List newSelection = new ArrayList<>(parents); + for (TreePath entry : parents) { + for (TreePath parent = entry.getParentPath(); parent != null; parent = parent.getParentPath()) { + if (parents.contains(parent)) { + newSelection.remove(entry); + break; + } + } + } + selectedPaths = newSelection.toArray(new TreePath[0]); + } + } } diff --git a/src/main/java/com/rapidminer/repository/gui/actions/RefreshRepositoryEntryAction.java b/src/main/java/com/rapidminer/repository/gui/actions/RefreshRepositoryEntryAction.java index 784c87866..91e998ca2 100644 --- a/src/main/java/com/rapidminer/repository/gui/actions/RefreshRepositoryEntryAction.java +++ b/src/main/java/com/rapidminer/repository/gui/actions/RefreshRepositoryEntryAction.java @@ -18,6 +18,11 @@ */ package com.rapidminer.repository.gui.actions; +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.repository.Entry; @@ -38,6 +43,19 @@ public RefreshRepositoryEntryAction(RepositoryTree tree) { super(tree, Entry.class, false, "repository_refresh_folder"); } + @Override + public void loggedActionPerformed(ActionEvent e) { + List folders = new ArrayList<>(); + //Don't trigger a folder refresh for every selected data entry + for (Entry entry : tree.getSelectedEntries()){ + folders.add(entry instanceof Folder ? entry : entry.getContainingFolder()); + } + // use hashset to eliminate duplicates + for (Entry entry : new HashSet<>(removeIntersectedEntries(folders))) { + actionPerformed(entry); + } + } + @Override public void actionPerformed(final Entry entry) { ProgressThread openProgressThread = new ProgressThread("refreshing") { diff --git a/src/main/java/com/rapidminer/repository/gui/search/RepositoryGlobalSearchGUIProvider.java b/src/main/java/com/rapidminer/repository/gui/search/RepositoryGlobalSearchGUIProvider.java index 9c794fd4a..d552d111d 100644 --- a/src/main/java/com/rapidminer/repository/gui/search/RepositoryGlobalSearchGUIProvider.java +++ b/src/main/java/com/rapidminer/repository/gui/search/RepositoryGlobalSearchGUIProvider.java @@ -60,6 +60,7 @@ import com.rapidminer.search.GlobalSearchUtilities; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; +import com.rapidminer.tools.usagestats.DefaultUsageLoggable; /** @@ -73,7 +74,7 @@ public class RepositoryGlobalSearchGUIProvider implements GlobalSearchableGUIPro /** * Drag & Drop support for repository entries. */ - private static final class RepositoryDragGesture implements DragGestureListener { + private static final class RepositoryDragGesture extends DefaultUsageLoggable implements DragGestureListener { private final RepositoryLocation location; @@ -95,8 +96,15 @@ public void dragGestureRecognized(DragGestureEvent event) { } // set the repository entry as the Transferable - event.startDrag(cursor, new TransferableRepositoryEntry(location)); + TransferableRepositoryEntry transferable = new TransferableRepositoryEntry(location); + if (usageLogger != null) { + transferable.setUsageStatsLogger(usageLogger); + } else if (usageObject != null) { + transferable.setUsageObject(usageObject); + } + event.startDrag(cursor, transferable); } + } private static final ImageIcon PROCESS_ICON = SwingTools.createIcon("16/gearwheel.png"); diff --git a/src/main/java/com/rapidminer/repository/local/SimpleFolder.java b/src/main/java/com/rapidminer/repository/local/SimpleFolder.java index 5aa2fbade..ee593c011 100644 --- a/src/main/java/com/rapidminer/repository/local/SimpleFolder.java +++ b/src/main/java/com/rapidminer/repository/local/SimpleFolder.java @@ -19,6 +19,7 @@ package com.rapidminer.repository.local; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -378,7 +379,23 @@ public BlobEntry createBlobEntry(String name) throws RepositoryException { @Override public boolean canRefreshChild(String childName) throws RepositoryException { // check existence of properties file - return new File(getFile(), childName + SimpleEntry.PROPERTIES_SUFFIX).exists(); + childName += SimpleEntry.PROPERTIES_SUFFIX; + File propFile = new File(getFile(), childName); + if (!propFile.exists()) { + return false; + } + try { + // Relevant for Windows only; since file system is case insensitive, we have to be sure + // that the referenced path is correct in regards to cases; the canonical path on Windows + // will return the proper cased path (if it exists) + // see https://stackoverflow.com/a/7896461, especially second comment + if (!propFile.getCanonicalPath().endsWith(childName)) { + return false; + } + } catch (IOException e) { + return false; + } + return true; } private void acquireReadLock() throws RepositoryException { diff --git a/src/main/java/com/rapidminer/studio/internal/StartupDialogRegistry.java b/src/main/java/com/rapidminer/studio/internal/StartupDialogRegistry.java index de0e34e99..c18a0406c 100644 --- a/src/main/java/com/rapidminer/studio/internal/StartupDialogRegistry.java +++ b/src/main/java/com/rapidminer/studio/internal/StartupDialogRegistry.java @@ -57,14 +57,7 @@ public void showStartupDialog(final ToolbarButton startButton) throws NoStartupD if (provider == null) { throw new NoStartupDialogRegistreredException(); } else { - SwingTools.invokeLater(new Runnable() { - - @Override - public void run() { - provider.show(startButton); - } - - }); + SwingTools.invokeLater(() -> provider.show(startButton)); } } } diff --git a/src/main/java/com/rapidminer/studio/io/data/internal/ResultSetAdapterUtils.java b/src/main/java/com/rapidminer/studio/io/data/internal/ResultSetAdapterUtils.java index c5410b49e..f41d3b971 100644 --- a/src/main/java/com/rapidminer/studio/io/data/internal/ResultSetAdapterUtils.java +++ b/src/main/java/com/rapidminer/studio/io/data/internal/ResultSetAdapterUtils.java @@ -66,28 +66,61 @@ private ResultSetAdapterUtils() { * the logic from {@link DataResultSetTranslator} is used. * * @param resultSet - * the {@link DataResultSet} that should be used to extract the meta data + * the {@link DataResultSet} that should be used to extract the meta data * @param numberFormat - * the number format that should be used during column type guessing + * the number format that should be used during column type guessing * @param startingRowIndex - * the 0-based index of the first data row (not including the header row) + * the 0-based index of the first data row (not including the header row) * @param headerRowIndex - * the 0-based index for the header row (if any, - * {@link ResultSetAdapter#NO_HEADER_ROW} otherwise) + * the 0-based index for the header row (if any, + * {@link ResultSetAdapter#NO_HEADER_ROW} otherwise) * @return the new {@link DataSetMetaData} instance which contains meta data retrieved from the - * column name extraction and column type guessing + * column name extraction and column type guessing * @throws HeaderRowNotFoundException - * if the header row was not found + * if the header row was not found * @throws StartRowNotFoundException - * if the data start row was not found + * if the data start row was not found * @throws HeaderRowBehindStartRowException - * in case the headerRowIndex > startingRowIndex + * in case the headerRowIndex > startingRowIndex * @throws DataSetException - * if the meta data fetching fails + * if the meta data fetching fails */ - public static DataSetMetaData createMetaData(DataResultSet resultSet, NumberFormat numberFormat, int startingRowIndex, - int headerRowIndex) throws HeaderRowNotFoundException, StartRowNotFoundException, - HeaderRowBehindStartRowException, DataSetException { + public static DataSetMetaData createMetaData(DataResultSet resultSet, NumberFormat numberFormat, int startingRowIndex, int headerRowIndex) + throws HeaderRowNotFoundException, StartRowNotFoundException, HeaderRowBehindStartRowException, DataSetException { + return createMetaData(resultSet, numberFormat, startingRowIndex, headerRowIndex, false); + } + + /** + * Creates a new {@link DataSetMetaData} instance for the provided {@link DataResultSet} based + * on the provided data range and header row index (if any). This includes reading the column + * names and guessing the column types for the selected columns. For guessing the column types + * the logic from {@link DataResultSetTranslator} is used. + * + * @param resultSet + * the {@link DataResultSet} that should be used to extract the meta data + * @param numberFormat + * the number format that should be used during column type guessing + * @param startingRowIndex + * the 0-based index of the first data row (not including the header row) + * @param headerRowIndex + * the 0-based index for the header row (if any, + * {@link ResultSetAdapter#NO_HEADER_ROW} otherwise) + * @param trimAttributeNames + * whether to trim attribute names before creating meta data or not + * @return the new {@link DataSetMetaData} instance which contains meta data retrieved from the + * column name extraction and column type guessing + * @throws HeaderRowNotFoundException + * if the header row was not found + * @throws StartRowNotFoundException + * if the data start row was not found + * @throws HeaderRowBehindStartRowException + * in case the headerRowIndex > startingRowIndex + * @throws DataSetException + * if the meta data fetching fails + * @since 8.1.1 + */ + public static DataSetMetaData createMetaData(DataResultSet resultSet, NumberFormat numberFormat, int startingRowIndex, int headerRowIndex, boolean trimAttributeNames) + throws HeaderRowNotFoundException, StartRowNotFoundException, HeaderRowBehindStartRowException, DataSetException { // check whether the header row index is lower or equal to the starting row if (headerRowIndex > startingRowIndex) { @@ -97,10 +130,9 @@ public static DataSetMetaData createMetaData(DataResultSet resultSet, NumberForm try { int numberOfColumns = resultSet.getNumberOfColumns(); - String[] columnNames = getColumnNames(resultSet, headerRowIndex, startingRowIndex, numberOfColumns); + String[] columnNames = getColumnNames(resultSet, headerRowIndex, startingRowIndex, numberOfColumns, trimAttributeNames); List columnTypes = guessColumnTypes(resultSet, startingRowIndex, headerRowIndex, numberOfColumns, numberFormat); - return new DefaultDataSetMetaData(Arrays.asList(columnNames), columnTypes); } catch (OperatorException e) { throw new DataSetException(e.getMessage(), e); @@ -132,24 +164,26 @@ private static void checkStartRow(DataResultSet resultSet, int startingRowIndex) * Reads the column names from the resultSet given the configuration. * * @param resultSet - * the data set + * the data set * @param headerRowIndex - * the index of the row that should be used to extract column names from or - * {@link ResultSetAdapter#NO_HEADER_ROW} in case the default names should be used + * the index of the row that should be used to extract column names from or + * {@link ResultSetAdapter#NO_HEADER_ROW} in case the default names should be used * @param startingRowIndex - * the index of the actual data start row + * the index of the actual data start row * @param numberOfColumns - * the number of columns for the {@link DataSource} + * the number of columns for the {@link DataSource} + * @param trimAttributeNames + * whether to trim attribute names before creating meta data or not * @return the column names as a String array * @throws HeaderRowNotFoundException - * if the header row was not found + * if the header row was not found * @throws StartRowNotFoundException - * if the data start row was not found + * if the data start row was not found * @throws OperatorException - * if reading the resultSet failed + * if reading the resultSet failed */ private static String[] getColumnNames(DataResultSet resultSet, int headerRowIndex, int startingRowIndex, - int numberOfColumns) throws HeaderRowNotFoundException, OperatorException, StartRowNotFoundException { + int numberOfColumns, boolean trimAttributeNames) throws HeaderRowNotFoundException, OperatorException, StartRowNotFoundException { resultSet.reset(null); String[] defaultNames = resultSet.getColumnNames(); @@ -184,6 +218,9 @@ private static String[] getColumnNames(DataResultSet resultSet, int headerRowInd case STRING: default: columnNames[i] = resultSet.getString(i); + if (trimAttributeNames && columnNames[i] != null) { + columnNames[i] = columnNames[i].trim(); + } break; } diff --git a/src/main/java/com/rapidminer/studio/io/data/internal/file/excel/ExcelDataSource.java b/src/main/java/com/rapidminer/studio/io/data/internal/file/excel/ExcelDataSource.java index 2ee249ab2..6598a5d77 100644 --- a/src/main/java/com/rapidminer/studio/io/data/internal/file/excel/ExcelDataSource.java +++ b/src/main/java/com/rapidminer/studio/io/data/internal/file/excel/ExcelDataSource.java @@ -19,7 +19,6 @@ package com.rapidminer.studio.io.data.internal.file.excel; import java.nio.file.Path; -import java.text.DateFormat; import com.rapidminer.core.io.data.DataSet; import com.rapidminer.core.io.data.DataSetException; @@ -143,13 +142,7 @@ private ExcelResultSetAdapter createDataSet(XlsxReadMode readMode, int maxPrevie endRow = endRowByLength; } } - DateFormatProvider provider = new DateFormatProvider() { - - @Override - public DateFormat geDateFormat() { - return getMetadata().getDateFormat(); - } - }; + DateFormatProvider provider = () -> getMetadata().getDateFormat(); return new ExcelResultSetAdapter(getResultSetConfiguration().makeDataResultSet(null, readMode, provider), startRow, endRow); } diff --git a/src/main/java/com/rapidminer/studio/io/gui/internal/steps/configuration/ConfigureDataView.java b/src/main/java/com/rapidminer/studio/io/gui/internal/steps/configuration/ConfigureDataView.java index 293cd83d6..27fd381c9 100644 --- a/src/main/java/com/rapidminer/studio/io/gui/internal/steps/configuration/ConfigureDataView.java +++ b/src/main/java/com/rapidminer/studio/io/gui/internal/steps/configuration/ConfigureDataView.java @@ -71,6 +71,7 @@ import com.rapidminer.gui.tools.ResourceLabel; import com.rapidminer.gui.tools.RowNumberTable; import com.rapidminer.gui.tools.SwingTools; +import com.rapidminer.operator.UserError; import com.rapidminer.parameter.ParameterTypeDateFormat; import com.rapidminer.studio.io.gui.internal.DataImportWizardUtils; import com.rapidminer.studio.io.gui.internal.DataWizardEventType; @@ -162,13 +163,7 @@ public ConfigureDataView(JDialog owner) { this.owner = owner; validator = new ConfigureDataValidator(); errorTableModel = new ErrorWarningTableModel(validator); - errorTableModel.addTableModelListener(new TableModelListener() { - - @Override - public void tableChanged(TableModelEvent e) { - fireStateChanged(); - } - }); + errorTableModel.addTableModelListener(e -> fireStateChanged()); collapsibleErrorTable = new CollapsibleErrorTable(errorTableModel); setLayout(new BorderLayout()); @@ -179,20 +174,16 @@ public void tableChanged(TableModelEvent e) { JPanel errorHandlingPanel = new JPanel(new BorderLayout()); errorHandlingCheckBox = new JCheckBox( I18N.getGUILabel("io.dataimport.step.data_column_configuration.replace_errors_checkbox.label")); - errorHandlingCheckBox.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - DataImportWizardUtils.logStats(DataWizardEventType.ERROR_HANDLING_CHANGED, - Boolean.toString(errorHandlingCheckBox.isSelected())); - errorTableModel.setFaultTolerant(errorHandlingCheckBox.isSelected()); - tableModel.fireTableDataChanged(); - } + errorHandlingCheckBox.addActionListener(e -> { + DataImportWizardUtils.logStats(DataWizardEventType.ERROR_HANDLING_CHANGED, + Boolean.toString(errorHandlingCheckBox.isSelected())); + errorTableModel.setFaultTolerant(errorHandlingCheckBox.isSelected()); + tableModel.fireTableDataChanged(); }); errorHandlingPanel.add(errorHandlingCheckBox, BorderLayout.CENTER); SwingTools.addTooltipHelpIconToLabel(ERROR_TOOLTIP_CONTENT, errorHandlingPanel, owner); - dateFormatField = new JComboBox(ParameterTypeDateFormat.PREDEFINED_DATE_FORMATS); + dateFormatField = new JComboBox<>(ParameterTypeDateFormat.PREDEFINED_DATE_FORMATS); dateFormatField.setEditable(true); // do not fire action event when using keyboard to move up and down @@ -207,7 +198,12 @@ public void actionPerformed(ActionEvent e) { } String datePattern = (String) dateFormatField.getSelectedItem(); if (datePattern != null && !datePattern.isEmpty()) { - updateDateFormat(new SimpleDateFormat(datePattern)); + try { + updateDateFormat(ParameterTypeDateFormat.createCheckedDateFormat(datePattern, null)); + } catch (UserError userError) { + // reset + dateFormatField.setSelectedItem(""); + } } } @@ -281,13 +277,7 @@ void updatePreviewContent(final DataSource dataSource) throws InvalidConfigurati @Override public void run() { getProgressListener().setTotal(100); - SwingTools.invokeLater(new Runnable() { - - @Override - public void run() { - showNotificationLabel("io.dataimport.step.data_column_configuration.loading_preview"); - } - }); + SwingTools.invokeLater(() -> showNotificationLabel("io.dataimport.step.data_column_configuration.loading_preview")); // load table model try { @@ -295,123 +285,111 @@ public void run() { validator.setParsingErrors(tableModel.getParsingErrors()); // adapt view after table has been loaded - SwingTools.invokeLater(new Runnable() { - - @Override - public void run() { - - // show error message in case preview is empty - if (tableModel.getRowCount() == 0) { - showErrorNotification("io.dataimport.step.data_column_configuration.no_data_available"); - return; - } - - // remove all components - centerPanel.removeAll(); - - // add preview table - ExtendedJTable previewTable = new ExtendedJTable(tableModel, false, false, false) { - - private static final long serialVersionUID = 1L; + SwingTools.invokeLater(() -> { + // show error message in case preview is empty + if (tableModel.getRowCount() == 0) { + showErrorNotification("io.dataimport.step.data_column_configuration.no_data_available"); + return; + } - @Override - public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { - Component c = super.prepareRenderer(renderer, row, column); - ColumnMetaData metaData = dataSetMetaData.getColumnMetaData().get(column); - if (metaData.isRemoved()) { - c.setBackground(BACKGROUND_COLUMN_DISABLED); - c.setForeground(FOREGROUND_COLUMN_DISABLED); + // remove all components + centerPanel.removeAll(); + + // add preview table + ExtendedJTable previewTable = new ExtendedJTable(tableModel, false, false, false) { + + private static final long serialVersionUID = 1L; + + @Override + public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { + Component c = super.prepareRenderer(renderer, row, column); + ColumnMetaData metaData = dataSetMetaData.getColumnMetaData().get(column); + if (metaData.isRemoved()) { + c.setBackground(BACKGROUND_COLUMN_DISABLED); + c.setForeground(FOREGROUND_COLUMN_DISABLED); + } else { + String role = metaData.getRole(); + if (role != null) { + c.setBackground(AttributeGuiTools.getColorForAttributeRole(role)); } else { - String role = metaData.getRole(); - if (role != null) { - c.setBackground(AttributeGuiTools.getColorForAttributeRole(role)); - } else { - c.setBackground(Color.WHITE); - } - c.setForeground(Color.BLACK); + c.setBackground(Color.WHITE); } - return c; - }; - }; - previewTable.setColumnSelectionAllowed(false); - previewTable.setCellSelectionEnabled(false); - previewTable.setRowSelectionAllowed(false); - previewTable.setColoredTableCellRenderer(ERROR_MARKING_CELL_RENDERER); - previewTable.setShowPopupMenu(false); - - // ensure same background as JPanels in case of only few rows - previewTable.setBackground(Colors.PANEL_BACKGROUND); - - TableColumnModel columnModel = previewTable.getColumnModel(); - - // set cell renderer for column headers - previewTable.setTableHeader(new JTableHeader(columnModel)); - for (int columnIndex = 0; columnIndex < columnModel.getColumnCount(); columnIndex++) { - TableColumn column = columnModel.getColumn(columnIndex); - ConfigureDataTableHeader headerRenderer = new ConfigureDataTableHeader(previewTable, - columnIndex, dataSetMetaData, validator, ConfigureDataView.this); - column.setHeaderRenderer(headerRenderer); - column.setMinWidth(120); + c.setForeground(Color.BLACK); + } + return c; } + }; + previewTable.setColumnSelectionAllowed(false); + previewTable.setCellSelectionEnabled(false); + previewTable.setRowSelectionAllowed(false); + previewTable.setColoredTableCellRenderer(ERROR_MARKING_CELL_RENDERER); + previewTable.setShowPopupMenu(false); + + // ensure same background as JPanels in case of only few rows + previewTable.setBackground(Colors.PANEL_BACKGROUND); + + TableColumnModel columnModel = previewTable.getColumnModel(); + + // set cell renderer for column headers + previewTable.setTableHeader(new JTableHeader(columnModel)); + for (int columnIndex = 0; columnIndex < columnModel.getColumnCount(); columnIndex++) { + TableColumn column = columnModel.getColumn(columnIndex); + ConfigureDataTableHeader headerRenderer = new ConfigureDataTableHeader(previewTable, + columnIndex, dataSetMetaData, validator, ConfigureDataView.this); + column.setHeaderRenderer(headerRenderer); + column.setMinWidth(120); + } - previewTable.getTableHeader().setReorderingAllowed(false); - - // Create a layered pane to display both, the data table and a - // "preview" overlay - JLayeredPane layeredPane = new JLayeredPane(); - layeredPane.setLayout(new OverlayLayout(layeredPane)); + previewTable.getTableHeader().setReorderingAllowed(false); - /* - * Hack to enlarge table columns in case of few columns. Add table to a - * full size JPanel and add the table header to the scroll pane. - */ - JPanel tablePanel = new JPanel(new BorderLayout()); - tablePanel.add(previewTable, BorderLayout.CENTER); + // Create a layered pane to display both, the data table and a + // "preview" overlay + JLayeredPane layeredPane = new JLayeredPane(); + layeredPane.setLayout(new OverlayLayout(layeredPane)); - JScrollPane scrollPane = new ExtendedJScrollPane(tablePanel); - scrollPane.setColumnHeaderView(previewTable.getTableHeader()); + /* + * Hack to enlarge table columns in case of few columns. Add table to a + * full size JPanel and add the table header to the scroll pane. + */ + JPanel tablePanel = new JPanel(new BorderLayout()); + tablePanel.add(previewTable, BorderLayout.CENTER); - scrollPane.setBorder(null); + JScrollPane scrollPane = new ExtendedJScrollPane(tablePanel); + scrollPane.setColumnHeaderView(previewTable.getTableHeader()); - // show row numbers - scrollPane.setRowHeaderView(new RowNumberTable(previewTable)); - layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER); + scrollPane.setBorder(null); - // Add "Preview" overlay - JPanel previewPanel = new JPanel(new BorderLayout()); - previewPanel.setOpaque(false); - JLabel previewLabel = new JLabel(I18N.getGUILabel("csv_format_specification.preview_background"), - SwingConstants.CENTER); - previewLabel.setFont(previewLabel.getFont().deriveFont(Font.BOLD, 180)); - previewLabel.setForeground(DataImportWizardUtils.getPreviewFontColor()); - previewPanel.add(previewLabel, BorderLayout.CENTER); - layeredPane.add(previewPanel, JLayeredPane.PALETTE_LAYER); + // show row numbers + scrollPane.setRowHeaderView(new RowNumberTable(previewTable)); + layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER); - GridBagConstraints constraint = new GridBagConstraints(); - constraint.fill = GridBagConstraints.BOTH; - constraint.weightx = 1.0; - constraint.weighty = 1.0; - centerPanel.add(layeredPane, constraint); + // Add "Preview" overlay + JPanel previewPanel = new JPanel(new BorderLayout()); + previewPanel.setOpaque(false); + JLabel previewLabel = new JLabel(I18N.getGUILabel("csv_format_specification.preview_background"), + SwingConstants.CENTER); + previewLabel.setFont(previewLabel.getFont().deriveFont(Font.BOLD, 180)); + previewLabel.setForeground(DataImportWizardUtils.getPreviewFontColor()); + previewPanel.add(previewLabel, BorderLayout.CENTER); + layeredPane.add(previewPanel, JLayeredPane.PALETTE_LAYER); - centerPanel.revalidate(); - centerPanel.repaint(); + GridBagConstraints constraint = new GridBagConstraints(); + constraint.fill = GridBagConstraints.BOTH; + constraint.weightx = 1.0; + constraint.weighty = 1.0; + centerPanel.add(layeredPane, constraint); - upperPanel.setVisible(true); - setupErrorTable(); + centerPanel.revalidate(); + centerPanel.repaint(); - fireStateChanged(); - } + upperPanel.setVisible(true); + setupErrorTable(); + fireStateChanged(); }); } catch (final DataSetException e) { - SwingTools.invokeLater(new Runnable() { - - @Override - public void run() { - showErrorNotification("io.dataimport.step.data_column_configuration.error_loading_data", - e.getMessage()); - } - }); + SwingTools.invokeLater(() -> showErrorNotification("io.dataimport.step.data_column_configuration.error_loading_data", + e.getMessage())); } finally { getProgressListener().complete(); } @@ -554,23 +532,13 @@ public void run() { try { tableModel.reread(getProgressListener()); } catch (final DataSetException e) { - SwingTools.invokeLater(new Runnable() { - - @Override - public void run() { - showErrorNotification("io.dataimport.step.data_column_configuration.error_loading_data", - e.getMessage()); - } - }); + SwingTools.invokeLater(() -> showErrorNotification("io.dataimport.step.data_column_configuration.error_loading_data", + e.getMessage())); return; } - SwingTools.invokeLater(new Runnable() { - - @Override - public void run() { - validator.setParsingErrors(tableModel.getParsingErrors()); - tableModel.fireTableDataChanged(); - } + SwingTools.invokeLater(() -> { + validator.setParsingErrors(tableModel.getParsingErrors()); + tableModel.fireTableDataChanged(); }); } diff --git a/src/main/java/com/rapidminer/template/PreparedTemplates.java b/src/main/java/com/rapidminer/template/PreparedTemplates.java new file mode 100644 index 000000000..42ae8a2e6 --- /dev/null +++ b/src/main/java/com/rapidminer/template/PreparedTemplates.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2001-2018 by RapidMiner and the contributors + * + * Complete list of developers available at our web site: + * + * http://rapidminer.com + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along with this program. + * If not, see http://www.gnu.org/licenses/. + */ +package com.rapidminer.template; + +/** + * Here we keep the {@link Template Templates} that are required in different places. + */ +public final class PreparedTemplates { + + /** + * This class should not be instantiated + */ + private PreparedTemplates() { + throw new UnsupportedOperationException(); + } + + /** + * special template which is not loaded from a resource but simply creates a new, empty process + */ + public static final Template BLANK_PROCESS_TEMPLATE = new Template.SpecialTemplate("new.empty"); + + /** + * special template which fires up the Auto Modeler which is included as a bundled extension + */ + public static final Template AUTO_MODEL_TEMPLATE = new Template.SpecialTemplate("automodel"); + + /** + * special template to access the online documentation + */ + public static final Template GETTING_STARTED_DOCUMENTATION = new Template.SpecialTemplate("documentation"); + + /** + * special template to acces the community + */ + public static final Template GETTING_STARTED_COMMUNITY = new Template.SpecialTemplate("community"); + + /** + * special template which loads the online training videos + */ + public static final Template GETTING_STARTED_TRAINING_VIDEOS = new Template.SpecialTemplate("trainingvideo"); +} diff --git a/src/main/java/com/rapidminer/template/Template.java b/src/main/java/com/rapidminer/template/Template.java index bb52835f5..eabed08e6 100644 --- a/src/main/java/com/rapidminer/template/Template.java +++ b/src/main/java/com/rapidminer/template/Template.java @@ -1,21 +1,21 @@ /** * Copyright (C) 2001-2018 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero 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 * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.template; import java.io.IOException; @@ -27,7 +27,6 @@ import java.util.Properties; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; - import javax.swing.Icon; import javax.swing.ImageIcon; @@ -67,48 +66,57 @@ * the files from the .Rapidminer folder are loaded automatically. * * @author Simon Fischer, Gisa Schaefer - * */ public class Template implements ZipStreamResource { - /** the repository location for templates */ + /** + * the repository location for templates + */ private static final String TEMPLATES_PATH = "//Samples/Templates/"; - /** the resources location for templates */ + /** + * the resources location for templates + */ private static final String RESOURCES_LOCATION = "template/"; private static final String NO_DESCRIPTION = I18N.getGUILabel("template.no_description"); private static final String NO_TITLE = I18N.getGUILabel("template.no_title"); - /** special template which is not loaded from a resource but simply creates a new, empty process */ - public static final Template BLANK_PROCESS_TEMPLATE = new Template() { + protected String title = NO_TITLE; + protected String shortDescription = NO_DESCRIPTION; + protected String processName; + protected List demoData; + protected Icon icon; - @Override - public Process makeProcess() throws IOException, XMLException, MalformedRepositoryLocationException { - return new Process(); - } - }; - - private String title = NO_TITLE; - private String shortDescription = NO_DESCRIPTION; - private String processName; - private List demoData; - private Icon icon; - - private final Path path; - private final String name; + protected Path path; + protected String name; /** - * Private constructor for special blank process template only. + * Private constructor for special template only. */ private Template() { - this.title = I18N.getGUILabel("getting_started.new.empty.title"); - this.shortDescription = I18N.getGUILabel("getting_started.new.empty.description"); - this.icon = SwingTools.createIcon("64/" + I18N.getGUILabel("getting_started.new.empty.icon")); - this.demoData = new LinkedList<>(); - this.name = this.title; - this.processName = null; - this.path = null; + } + + /** + * Private special template class for special process templates only. + */ + static final class SpecialTemplate extends Template { + private static final String GETTING_STARTED_LITERAL = "getting_started."; + + SpecialTemplate(String key) { + this.title = I18N.getGUILabel(GETTING_STARTED_LITERAL + key + ".title"); + this.shortDescription = I18N.getGUILabel(GETTING_STARTED_LITERAL + key + ".description"); + this.icon = SwingTools.createIcon("64/" + I18N.getGUILabel(GETTING_STARTED_LITERAL + key + ".icon")); + this.demoData = new LinkedList<>(); + this.name = this.title; + this.processName = null; + this.path = null; + } + + @Override + public Process makeProcess() throws IOException, XMLException, MalformedRepositoryLocationException { + return new Process(); + } } Template(String name) throws IOException, RepositoryException { diff --git a/src/main/java/com/rapidminer/template/TemplateManager.java b/src/main/java/com/rapidminer/template/TemplateManager.java index 6e623aa91..0fd84c10a 100644 --- a/src/main/java/com/rapidminer/template/TemplateManager.java +++ b/src/main/java/com/rapidminer/template/TemplateManager.java @@ -1,27 +1,28 @@ /** * Copyright (C) 2001-2018 by RapidMiner and the contributors - * + * * Complete list of developers available at our web site: - * + * * http://rapidminer.com - * + * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero 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 * Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. -*/ + */ package com.rapidminer.template; import java.io.File; import java.io.IOException; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -50,7 +51,9 @@ public enum TemplateManager { private TemplateManager() { // blank process template - register(Template.BLANK_PROCESS_TEMPLATE); + register(PreparedTemplates.BLANK_PROCESS_TEMPLATE); + // auto model + register(PreparedTemplates.AUTO_MODEL_TEMPLATE); // load templates from bundled resources registerTemplate("telco_churn_modeling"); registerTemplate("direct_marketing"); @@ -72,7 +75,7 @@ private TemplateManager() { register(new Template(Paths.get(file.toURI()))); } catch (IOException | RepositoryException e) { LogService.getRoot().log(Level.WARNING, - "com.rapidminer.template.TemplateManager.failed_to_load_templatefile", new Object[] { file, e }); + "com.rapidminer.template.TemplateManager.failed_to_load_templatefile", new Object[]{file, e}); } } } @@ -93,15 +96,13 @@ private TemplateManager() { * } * * - * @see Template Description of the .template file contents - * * @param templateName - * the unique name of the template + * the unique name of the template + * @see Template Description of the .template file contents */ public void registerTemplate(String templateName) { if (templatesByName.containsKey(templateName)) { - LogService.getRoot().log(Level.INFO, - "Template with name '" + templateName + "' was already registerd. Skipping registration."); + LogService.getRoot().log(Level.INFO, "com.rapidminer.template.TemplateManager.template_already_registered", templateName); return; } try { @@ -122,4 +123,7 @@ public List