| /***************************************************************** |
| * 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 |
| * |
| * https://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.editor; |
| |
| import org.apache.cayenne.configuration.event.QueryEvent; |
| import org.apache.cayenne.map.Entity; |
| import org.apache.cayenne.map.QueryDescriptor; |
| import org.apache.cayenne.map.SelectQueryDescriptor; |
| import org.apache.cayenne.modeler.Application; |
| import org.apache.cayenne.modeler.ProjectController; |
| import org.apache.cayenne.modeler.util.CayenneAction; |
| import org.apache.cayenne.modeler.util.EntityTreeModel; |
| import org.apache.cayenne.modeler.util.ModelerUtil; |
| import org.apache.cayenne.modeler.util.MultiColumnBrowser; |
| import org.apache.cayenne.modeler.util.UIUtil; |
| import org.apache.cayenne.query.Ordering; |
| import org.apache.cayenne.query.SortOrder; |
| import org.apache.cayenne.swing.components.image.FilteredIconFactory; |
| import org.apache.cayenne.util.CayenneMapEntry; |
| |
| import javax.swing.BorderFactory; |
| import javax.swing.Icon; |
| import javax.swing.JButton; |
| import javax.swing.JComponent; |
| 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; |
| import javax.swing.table.AbstractTableModel; |
| import javax.swing.table.TableModel; |
| import javax.swing.tree.TreeModel; |
| import java.awt.BorderLayout; |
| import java.awt.CardLayout; |
| import java.awt.Dimension; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.ActionListener; |
| import java.beans.PropertyChangeEvent; |
| import java.beans.PropertyChangeListener; |
| import java.util.prefs.Preferences; |
| |
| /** |
| * A panel for picking SelectQuery orderings. |
| * |
| */ |
| public class SelectQueryOrderingTab extends JPanel implements PropertyChangeListener { |
| |
| |
| // property for split pane divider size |
| private static final String SPLIT_DIVIDER_LOCATION_PROPERTY = "query.orderings.divider.location"; |
| |
| static final Dimension BROWSER_CELL_DIM = new Dimension(150, 100); |
| static final Dimension TABLE_DIM = new Dimension(460, 60); |
| |
| static final String PATH_HEADER = "Path"; |
| static final String ASCENDING_HEADER = "Ascending"; |
| static final String IGNORE_CASE_HEADER = "Ignore Case"; |
| |
| static final String REAL_PANEL = "real"; |
| static final String PLACEHOLDER_PANEL = "placeholder"; |
| |
| protected ProjectController mediator; |
| protected SelectQueryDescriptor selectQuery; |
| |
| protected MultiColumnBrowser browser; |
| protected JTable table; |
| |
| protected CardLayout cardLayout; |
| protected JPanel messagePanel; |
| |
| public SelectQueryOrderingTab(ProjectController mediator) { |
| this.mediator = mediator; |
| |
| initView(); |
| initController(); |
| } |
| |
| protected void initView() { |
| |
| messagePanel = new JPanel(new BorderLayout()); |
| cardLayout = new CardLayout(); |
| |
| Preferences detail = Application.getInstance().getPreferencesNode(this.getClass(), ""); |
| |
| int defLocation = Application.getFrame().getHeight() / 2; |
| int location = detail != null ? detail.getInt( |
| getDividerLocationProperty(), |
| defLocation) : defLocation; |
| |
| //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.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, this); |
| mainPanel.setDividerLocation(location); |
| |
| mainPanel.setTopComponent(createEditorPanel()); |
| mainPanel.setBottomComponent(createSelectorPanel()); |
| |
| setLayout(cardLayout); |
| add(mainPanel, REAL_PANEL); |
| add(messagePanel, PLACEHOLDER_PANEL); |
| } |
| |
| protected void initController() { |
| |
| // scroll to selected row whenever a selection even occurs |
| table.getSelectionModel().addListSelectionListener(e -> { |
| if (!e.getValueIsAdjusting()) { |
| UIUtil.scrollToSelectedRow(table); |
| } |
| }); |
| } |
| |
| protected void initFromModel() { |
| QueryDescriptor query = mediator.getCurrentQuery(); |
| |
| if (query == null || !QueryDescriptor.SELECT_QUERY.equals(query.getType())) { |
| processInvalidModel("Unknown query."); |
| return; |
| } |
| |
| if (!(query.getRoot() instanceof Entity)) { |
| processInvalidModel("SelectQuery has no root set."); |
| return; |
| } |
| |
| this.selectQuery = (SelectQueryDescriptor) query; |
| browser.setModel(createBrowserModel((Entity) selectQuery.getRoot())); |
| table.setModel(createTableModel()); |
| |
| // init column sizes |
| table.getColumnModel().getColumn(0).setPreferredWidth(250); |
| |
| cardLayout.show(this, REAL_PANEL); |
| } |
| |
| protected void processInvalidModel(String message) { |
| JLabel messageLabel = new JLabel(message, JLabel.CENTER); |
| |
| messagePanel.removeAll(); |
| messagePanel.add(messageLabel, BorderLayout.CENTER); |
| cardLayout.show(this, PLACEHOLDER_PANEL); |
| } |
| |
| protected JPanel createEditorPanel() { |
| |
| table = new JTable(); |
| table.setRowHeight(25); |
| table.setRowMargin(3); |
| table.setPreferredScrollableViewportSize(TABLE_DIM); |
| table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); |
| |
| JPanel panel = new JPanel(new BorderLayout()); |
| panel.add(new JScrollPane(table), BorderLayout.CENTER); |
| |
| return panel; |
| } |
| |
| protected JPanel createSelectorPanel() { |
| 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( |
| browser, |
| 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; |
| } |
| |
| protected JComponent createToolbar() { |
| |
| JButton add = new CayenneAction.CayenneToolbarButton(null, 1); |
| add.setText("Add Ordering"); |
| Icon addIcon = ModelerUtil.buildIcon("icon-plus.png"); |
| add.setIcon(addIcon); |
| add.setDisabledIcon(FilteredIconFactory.createDisabledIcon(addIcon)); |
| add.addActionListener(e -> addOrdering()); |
| |
| JButton remove = new CayenneAction.CayenneToolbarButton(null, 3); |
| remove.setText("Remove Ordering"); |
| Icon removeIcon = ModelerUtil.buildIcon("icon-trash.png"); |
| remove.setIcon(removeIcon); |
| remove.setDisabledIcon(FilteredIconFactory.createDisabledIcon(removeIcon)); |
| remove.addActionListener(new ActionListener() { |
| |
| public void actionPerformed(ActionEvent e) { |
| removeOrdering(); |
| } |
| |
| }); |
| |
| JToolBar toolBar = new JToolBar(); |
| toolBar.setBorder(BorderFactory.createEmptyBorder()); |
| toolBar.setFloatable(false); |
| toolBar.add(add); |
| toolBar.add(remove); |
| return toolBar; |
| } |
| |
| protected TreeModel createBrowserModel(Entity entity) { |
| return new EntityTreeModel(entity); |
| } |
| |
| protected TableModel createTableModel() { |
| return new OrderingModel(); |
| } |
| |
| protected String getSelectedPath() { |
| Object[] path = browser.getSelectionPath().getPath(); |
| |
| // first item in the path is Entity, so we must have |
| // at least two elements to constitute a valid ordering path |
| if (path.length < 2) { |
| return null; |
| } |
| |
| StringBuilder buffer = new StringBuilder(); |
| |
| // attribute or relationships |
| CayenneMapEntry first = (CayenneMapEntry) path[1]; |
| buffer.append(first.getName()); |
| |
| for (int i = 2; i < path.length; i++) { |
| CayenneMapEntry pathEntry = (CayenneMapEntry) path[i]; |
| buffer.append(".").append(pathEntry.getName()); |
| } |
| |
| return buffer.toString(); |
| } |
| |
| void addOrdering() { |
| String orderingPath = getSelectedPath(); |
| if (orderingPath == null) { |
| return; |
| } |
| |
| // check if such ordering already exists |
| for (Ordering ord : selectQuery.getOrderings()) { |
| if (orderingPath.equals(ord.getSortSpecString())) { |
| return; |
| } |
| } |
| |
| selectQuery.addOrdering(new Ordering(orderingPath, SortOrder.ASCENDING)); |
| int index = selectQuery.getOrderings().size() - 1; |
| |
| OrderingModel model = (OrderingModel) table.getModel(); |
| model.fireTableRowsInserted(index, index); |
| mediator.fireQueryEvent(new QueryEvent(SelectQueryOrderingTab.this, selectQuery)); |
| } |
| |
| void removeOrdering() { |
| int selection = table.getSelectedRow(); |
| if (selection < 0) { |
| return; |
| } |
| |
| OrderingModel model = (OrderingModel) table.getModel(); |
| Ordering ordering = model.getOrdering(selection); |
| selectQuery.removeOrdering(ordering); |
| |
| model.fireTableRowsDeleted(selection, selection); |
| mediator.fireQueryEvent(new QueryEvent(SelectQueryOrderingTab.this, selectQuery)); |
| } |
| |
| /** |
| * A table model for the Ordering editing table. |
| */ |
| final class OrderingModel extends AbstractTableModel { |
| |
| Ordering getOrdering(int row) { |
| return selectQuery.getOrderings().get(row); |
| } |
| |
| public int getColumnCount() { |
| return 3; |
| } |
| |
| public int getRowCount() { |
| return (selectQuery != null) ? selectQuery.getOrderings().size() : 0; |
| } |
| |
| public Object getValueAt(int row, int column) { |
| Ordering ordering = getOrdering(row); |
| |
| switch (column) { |
| case 0: |
| return ordering.getSortSpecString(); |
| case 1: |
| return ordering.isAscending() ? Boolean.TRUE : Boolean.FALSE; |
| case 2: |
| return ordering.isCaseInsensitive() ? Boolean.TRUE : Boolean.FALSE; |
| default: |
| throw new IndexOutOfBoundsException("Invalid column: " + column); |
| } |
| } |
| |
| @Override |
| public Class getColumnClass(int column) { |
| switch (column) { |
| case 0: |
| return String.class; |
| case 1: |
| case 2: |
| return Boolean.class; |
| default: |
| throw new IndexOutOfBoundsException("Invalid column: " + column); |
| } |
| } |
| |
| @Override |
| public String getColumnName(int column) { |
| switch (column) { |
| case 0: |
| return PATH_HEADER; |
| case 1: |
| return ASCENDING_HEADER; |
| case 2: |
| return IGNORE_CASE_HEADER; |
| default: |
| throw new IndexOutOfBoundsException("Invalid column: " + column); |
| } |
| } |
| |
| @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); |
| |
| switch (column) { |
| case 1: |
| if (((Boolean) value).booleanValue()) { |
| ordering.setAscending(); |
| } |
| else { |
| ordering.setDescending(); |
| } |
| break; |
| case 2: |
| if (((Boolean) value).booleanValue()) { |
| ordering.setCaseInsensitive(); |
| } |
| else { |
| ordering.setCaseSensitive(); |
| } |
| break; |
| default: |
| throw new IndexOutOfBoundsException("Invalid editable column: " |
| + column); |
| } |
| |
| mediator.fireQueryEvent(new QueryEvent( |
| SelectQueryOrderingTab.this, |
| selectQuery)); |
| } |
| } |
| |
| /** |
| * Updates split pane divider location in properties |
| */ |
| public void propertyChange(PropertyChangeEvent evt) { |
| if (JSplitPane.DIVIDER_LOCATION_PROPERTY.equals(evt.getPropertyName())) { |
| int value = (Integer) evt.getNewValue(); |
| |
| Preferences detail = Application.getInstance().getPreferencesNode(this.getClass(), ""); |
| detail.putInt(getDividerLocationProperty(), value); |
| } |
| } |
| |
| /** |
| * Returns name of a property for divider location. |
| */ |
| protected String getDividerLocationProperty() { |
| return SPLIT_DIVIDER_LOCATION_PROPERTY; |
| } |
| } |