Author: kmenard
Date: Sun Jun 8 10:07:41 2008
New Revision: 664525
URL: http://svn.apache.org/viewvc?rev=664525&view=rev
Log:
Largely fixes CAY-888: CM Usability: Object Select Query Improvements.
Added:
cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/util/ValidatorTextAdapter.java
Modified:
cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryMainTab.java
cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryOrderingTab.java
Modified:
cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryMainTab.java
URL:
http://svn.apache.org/viewvc/cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryMainTab.java?rev=664525&r1=664524&r2=664525&view=diff
==============================================================================
---
cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryMainTab.java
(original)
+++
cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryMainTab.java
Sun Jun 8 10:07:41 2008
@@ -22,7 +22,10 @@
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
import java.util.Arrays;
+import java.util.Iterator;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JCheckBox;
@@ -31,7 +34,10 @@
import javax.swing.JTextField;
import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionException;
+import org.apache.cayenne.exp.parser.ASTPath;
import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.Entity;
import org.apache.cayenne.map.event.QueryEvent;
import org.apache.cayenne.modeler.ProjectController;
import org.apache.cayenne.modeler.util.CayenneWidgetFactory;
@@ -40,10 +46,12 @@
import org.apache.cayenne.modeler.util.ExpressionConvertor;
import org.apache.cayenne.modeler.util.ProjectUtil;
import org.apache.cayenne.modeler.util.TextAdapter;
+import org.apache.cayenne.modeler.util.ValidatorTextAdapter;
import org.apache.cayenne.modeler.util.combo.AutoCompletion;
import org.apache.cayenne.query.AbstractQuery;
import org.apache.cayenne.query.Query;
import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.util.CayenneMapEntry;
import org.apache.cayenne.util.Util;
import org.apache.cayenne.validation.ValidationException;
@@ -87,12 +95,17 @@
AutoCompletion.enable(queryRoot);
queryRoot.setRenderer(CellRenderers.listRendererWithIcons());
- qualifier = new TextAdapter(new JTextField()) {
+ qualifier = new ValidatorTextAdapter(new JTextField()) {
@Override
protected void updateModel(String text) {
setQueryQualifier(text);
}
+
+ @Override
+ protected void validate(String text) throws ValidationException {
+ createQualifier(text);
+ }
};
distinct = new JCheckBox();
@@ -123,17 +136,11 @@
}
private void initController() {
-
- queryRoot.addActionListener(new ActionListener() {
-
- public void actionPerformed(ActionEvent event) {
- SelectQuery query = getQuery();
- if (query != null) {
- query.setRoot(queryRoot.getModel().getSelectedItem());
- mediator.fireQueryEvent(new QueryEvent(this, query));
- }
- }
- });
+ RootSelectionHandler rootHandler = new RootSelectionHandler();
+
+ queryRoot.addActionListener(rootHandler);
+ queryRoot.addFocusListener(rootHandler);
+
queryRoot.getEditor().getEditorComponent().addFocusListener(rootHandler);
distinct.addActionListener(new ActionListener() {
@@ -206,19 +213,46 @@
text = null;
}
+ Expression qualifier = createQualifier(text);
+ if (qualifier != null)
+ {
+ //getQuery() is not null if we reached here
+ getQuery().setQualifier(qualifier);
+ mediator.fireQueryEvent(new QueryEvent(this, getQuery()));
+ }
+
+ }
+
+ /**
+ * Method to create and check an expression
+ * @param text String to be converted as Expression
+ * @return Expression if a new expression was created, null otherwise.
+ * @throws ValidationException if <code>text</code> can't be converted
+ */
+ Expression createQualifier(String text) throws ValidationException
+ {
SelectQuery query = getQuery();
if (query == null) {
- return;
+ return null;
}
-
+
ExpressionConvertor convertor = new ExpressionConvertor();
try {
String oldQualifier =
convertor.valueAsString(query.getQualifier());
if (!Util.nullSafeEquals(oldQualifier, text)) {
Expression exp = (Expression) convertor.stringAsValue(text);
- query.setQualifier(exp);
- mediator.fireQueryEvent(new QueryEvent(this, query));
+
+ /**
+ * Advanced checking. See CAY-888 #1
+ */
+ if(query.getRoot() != null) {
+ checkExpression((Entity) query.getRoot(), exp);
+ }
+
+ return exp;
}
+
+ return null;
}
catch (IllegalArgumentException ex) {
// unparsable qualifier
@@ -264,4 +298,110 @@
+ "'. Use a different name.");
}
}
+
+ /**
+ * Advanced checking of an expression, needed because
Expression.fromString()
+ * might terminate normally, but returned Expression will not be appliable
+ * for real Entities.
+ * Current implementation assures all attributes in expression are present
in
+ * Entity
+ * @param root Root of a query
+ * @param ex Expression to check
+ * @throws ValidationException when something's wrong
+ */
+ static void checkExpression(Entity root, Expression ex) throws
ValidationException {
+ try {
+ if (ex instanceof ASTPath) {
+ /**
+ * Try to iterate through path, if some attributes are not
present,
+ * exception will be raised
+ */
+
+ Iterator<CayenneMapEntry> path =
root.resolvePathComponents(ex);
+ while (path.hasNext()) {
+ path.next();
+ }
+ }
+
+ if (ex != null) {
+ for (int i = 0; i < ex.getOperandCount(); i++) {
+ if (ex.getOperand(i) instanceof Expression) {
+ checkExpression(root, (Expression)ex.getOperand(i));
+ }
+ }
+ }
+ }
+ catch (ExpressionException eex) {
+ throw new ValidationException(eex.getUnlabeledMessage());
+ }
+ }
+
+ /**
+ * Handler to user's actions with root selection combobox
+ */
+ class RootSelectionHandler implements FocusListener, ActionListener {
+ String newName = null;
+ boolean needChangeName;
+
+ public void actionPerformed(ActionEvent ae) {
+ SelectQuery query = getQuery();
+ if (query != null) {
+ query.setRoot(queryRoot.getModel().getSelectedItem());
+
+ if (needChangeName) { //not changed by user
+ /**
+ * Doing auto name change, following CAY-888 #2
+ */
+ Entity e = (Entity)queryRoot.getModel().getSelectedItem();
+
+ String newPrefix = e.getName() + "Query";
+ newName = newPrefix;
+
+ DataMap map = mediator.getCurrentDataMap();
+ long postfix = 1;
+
+ while (map.getQuery(newName) != null) {
+ newName = newPrefix + (postfix++);
+ }
+
+ name.setText(newName);
+ }
+ }
+ }
+
+ public void focusGained(FocusEvent e) {
+ //reset new name tracking
+ newName = null;
+
+ SelectQuery query = getQuery();
+ if (query != null) {
+ needChangeName = hasDefaultName(query);
+ }
+ else {
+ needChangeName = false;
+ }
+ }
+
+ public void focusLost(FocusEvent e) {
+ if (newName != null) {
+ setQueryName(newName);
+ }
+
+ newName = null;
+ needChangeName = false;
+ }
+
+ /**
+ * @return whether specified's query name is 'default' i.e. Cayenne
generated
+ * A query's name is 'default' if it starts with 'UntitledQuery' or
with root name.
+ *
+ * We cannot follow user input because tab might be opened many times
+ */
+ boolean hasDefaultName(SelectQuery query) {
+ String prefix = query.getRoot() == null ? "UntitledQuery" :
+ ((Entity)query.getRoot()).getName() + "Query";
+
+ return name.getComponent().getText().startsWith(prefix);
+ }
+ }
}
Modified:
cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryOrderingTab.java
URL:
http://svn.apache.org/viewvc/cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryOrderingTab.java?rev=664525&r1=664524&r2=664525&view=diff
==============================================================================
---
cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryOrderingTab.java
(original)
+++
cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryOrderingTab.java
Sun Jun 8 10:07:41 2008
@@ -31,6 +31,7 @@
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JToolBar;
import javax.swing.ListSelectionModel;
@@ -42,6 +43,7 @@
import org.apache.cayenne.map.Entity;
import org.apache.cayenne.map.event.QueryEvent;
+import org.apache.cayenne.modeler.Application;
import org.apache.cayenne.modeler.ProjectController;
import org.apache.cayenne.modeler.util.EntityTreeModel;
import org.apache.cayenne.modeler.util.ModelerUtil;
@@ -90,9 +92,15 @@
messagePanel = new JPanel(new BorderLayout());
cardLayout = new CardLayout();
- JPanel mainPanel = new JPanel(new BorderLayout());
- mainPanel.add(createEditorPanel(), BorderLayout.CENTER);
- mainPanel.add(createSelectorPanel(), BorderLayout.SOUTH);
+ /**
+ * As of CAY-888 #3 main pane is now a JSplitPane.
+ * Top component is a bit larger.
+ */
+ JSplitPane mainPanel = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
+ mainPanel.setDividerLocation(Application.getFrame().getHeight() / 2);
+
+ mainPanel.setTopComponent(createEditorPanel());
+ mainPanel.setBottomComponent(createSelectorPanel());
setLayout(cardLayout);
add(mainPanel, REAL_PANEL);
@@ -161,7 +169,7 @@
browser = new MultiColumnBrowser();
browser.setPreferredColumnSize(BROWSER_CELL_DIM);
browser.setDefaultRenderer();
-
+
JPanel panel = new JPanel(new BorderLayout());
panel.add(createToolbar(), BorderLayout.NORTH);
panel.add(new JScrollPane(
@@ -169,6 +177,10 @@
JScrollPane.VERTICAL_SCROLLBAR_NEVER,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED),
BorderLayout.CENTER);
+ //setting minimal size, otherwise scrolling looks awful, because of
+ //VERTICAL_SCROLLBAR_NEVER strategy
+ panel.setMinimumSize(panel.getPreferredSize());
+
return panel;
}
@@ -274,7 +286,7 @@
final class OrderingModel extends AbstractTableModel {
Ordering getOrdering(int row) {
- return (Ordering) selectQuery.getOrderings().get(row);
+ return selectQuery.getOrderings().get(row);
}
public int getColumnCount() {
@@ -300,6 +312,7 @@
}
}
+ @Override
public Class getColumnClass(int column) {
switch (column) {
case 0:
@@ -312,6 +325,7 @@
}
}
+ @Override
public String getColumnName(int column) {
switch (column) {
case 0:
@@ -325,10 +339,12 @@
}
}
+ @Override
public boolean isCellEditable(int row, int column) {
return column == 1 || column == 2;
}
+ @Override
public void setValueAt(Object value, int row, int column) {
Ordering ordering = getOrdering(row);
Added:
cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/util/ValidatorTextAdapter.java
URL:
http://svn.apache.org/viewvc/cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/util/ValidatorTextAdapter.java?rev=664525&view=auto
==============================================================================
---
cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/util/ValidatorTextAdapter.java
(added)
+++
cayenne/main/trunk/framework/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/util/ValidatorTextAdapter.java
Sun Jun 8 10:07:41 2008
@@ -0,0 +1,171 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+package org.apache.cayenne.modeler.util;
+
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.JTextComponent;
+
+import org.apache.cayenne.validation.ValidationException;
+
+/**
+ * Text adapter with live validation, which is fired in
+ * VALIDATION_DELAY time.
+ * @author Andrey Razumovsky
+ */
+public abstract class ValidatorTextAdapter extends TextAdapter {
+ /**
+ * Time between end of user input and validation firing
+ */
+ static final long VALIDATION_DELAY = 1500L;
+
+ /**
+ * Is the live-checking enabled for the text component
+ */
+ boolean liveCheckEnabled;
+
+ public ValidatorTextAdapter(JTextField textField) {
+ this(textField, true);
+ }
+
+ public ValidatorTextAdapter(JTextField textField, boolean
liveCheckEnabled) {
+ super(textField, true, false, true);
+ setLiveCheckEnabled(liveCheckEnabled);
+ install(textField);
+ }
+
+ public ValidatorTextAdapter(JTextArea textArea) {
+ this(textArea, true);
+ }
+
+ public ValidatorTextAdapter(JTextArea textArea, boolean liveCheckEnabled) {
+ super(textArea, true, false);
+ setLiveCheckEnabled(liveCheckEnabled);
+ install(textArea);
+ }
+
+ protected void install(JTextComponent textComponent) {
+ TimerScheduler ts = new TimerScheduler();
+
+ textComponent.getDocument().addDocumentListener(ts);
+ textComponent.addFocusListener(ts);
+ }
+
+ /**
+ * Live-checks if text is correct
+ * @throws ValidationException if the text is incorrect
+ */
+ protected abstract void validate(String text) throws ValidationException;
+
+ /**
+ * @return Is the live-checking enabled for the text component
+ */
+ public boolean isLiveCheckEnabled() {
+ return liveCheckEnabled;
+ }
+
+ /**
+ * Enables/disables live-checking
+ */
+ public void setLiveCheckEnabled(boolean b) {
+ liveCheckEnabled = b;
+ }
+
+ /**
+ * Task to be fired after some delay
+ */
+ class ValidationTimerTask extends TimerTask {
+ @Override
+ public void run() {
+ validate();
+ }
+ }
+
+ protected void validate() {
+ try {
+ validate(textComponent.getText());
+ clear();
+ }
+ catch (ValidationException vex) {
+ textComponent.setBackground(errorColor);
+ textComponent.setToolTipText(vex.getUnlabeledMessage());
+ }
+ }
+
+ /**
+ * Listener to user input, which fires validation timer
+ */
+ class TimerScheduler implements DocumentListener, FocusListener {
+ /**
+ * The timer, which fires validation after some delay
+ */
+ Timer validationTimer;
+
+ Object sync; //to prevent concurrent collisions
+
+ TimerScheduler() {
+ sync = new Object();
+ }
+
+ public void insertUpdate(DocumentEvent e) {
+ schedule();
+ }
+
+ public void changedUpdate(DocumentEvent e) {
+ schedule();
+ }
+
+ public void removeUpdate(DocumentEvent e) {
+ schedule();
+ }
+
+ void schedule() {
+ if(isLiveCheckEnabled()) {
+ synchronized (sync) {
+ if(validationTimer != null) {
+ validationTimer.cancel();
+ }
+
+ clear();
+
+ validationTimer = new Timer("cayenne-validation-timer");
+ validationTimer.schedule(new ValidationTimerTask(),
VALIDATION_DELAY);
+ }
+ }
+ }
+
+ public void focusGained(FocusEvent e) {
+ }
+
+ public void focusLost(FocusEvent e) {
+ synchronized (sync) {
+ if(validationTimer != null) {
+ validationTimer.cancel();
+ }
+ }
+ };
+ }
+}