| /******************************************************************************* |
| * Copyright (c) 2006, 2007 Obeo. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * Obeo - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.emf.compare.ui.viewerpart; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.eclipse.emf.common.util.TreeIterator; |
| import org.eclipse.emf.compare.diff.metamodel.AddModelElement; |
| import org.eclipse.emf.compare.diff.metamodel.AttributeChange; |
| import org.eclipse.emf.compare.diff.metamodel.DiffElement; |
| import org.eclipse.emf.compare.diff.metamodel.DiffGroup; |
| import org.eclipse.emf.compare.diff.metamodel.RemoveModelElement; |
| import org.eclipse.emf.compare.match.metamodel.Match2Elements; |
| import org.eclipse.emf.compare.match.metamodel.MatchModel; |
| import org.eclipse.emf.compare.match.metamodel.UnMatchElement; |
| import org.eclipse.emf.compare.ui.ICompareEditorPartListener; |
| import org.eclipse.emf.compare.ui.ModelCompareInput; |
| import org.eclipse.emf.compare.ui.TypedElementWrapper; |
| import org.eclipse.emf.compare.ui.contentmergeviewer.ModelContentMergeViewer; |
| import org.eclipse.emf.compare.ui.contentprovider.PropertyContentProvider; |
| import org.eclipse.emf.compare.ui.util.EMFAdapterFactoryProvider; |
| import org.eclipse.emf.compare.ui.util.EMFCompareConstants; |
| import org.eclipse.emf.compare.ui.util.EMFCompareEObjectUtils; |
| import org.eclipse.emf.ecore.EObject; |
| import org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider; |
| import org.eclipse.jface.viewers.ISelectionChangedListener; |
| import org.eclipse.jface.viewers.SelectionChangedEvent; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.custom.CTabFolder; |
| import org.eclipse.swt.custom.CTabItem; |
| import org.eclipse.swt.events.PaintEvent; |
| import org.eclipse.swt.events.PaintListener; |
| import org.eclipse.swt.events.SelectionAdapter; |
| import org.eclipse.swt.events.SelectionEvent; |
| import org.eclipse.swt.events.SelectionListener; |
| import org.eclipse.swt.events.TreeEvent; |
| import org.eclipse.swt.events.TreeListener; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.RGB; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.layout.GridData; |
| import org.eclipse.swt.layout.GridLayout; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Item; |
| import org.eclipse.swt.widgets.TableColumn; |
| import org.eclipse.swt.widgets.TableItem; |
| import org.eclipse.swt.widgets.TreeItem; |
| import org.eclipse.swt.widgets.Widget; |
| |
| // TODO handle horizontal sync of viewer parts |
| /** |
| * Describes a part of a {@link ModelContentMergeViewer}. |
| * |
| * @author Cedric Brun <a href="mailto:cedric.brun@obeo.fr">cedric.brun@obeo.fr</a> |
| */ |
| public class ModelContentMergeViewerPart { |
| protected static final String INVALID_TAB = "Invalid tab index"; //$NON-NLS-1$ |
| |
| private final List<ICompareEditorPartListener> editorPartListeners = new ArrayList<ICompareEditorPartListener>(); |
| |
| private int selectedTab; |
| |
| private CTabFolder tabFolder; |
| |
| private ModelContentMergeViewer parentViewer; |
| |
| private ModelContentMergeTreePart tree; |
| |
| private ModelContentMergePropertyPart properties; |
| |
| private int partSide; |
| |
| /** |
| * Instantiates a {@link ModelContentMergeViewerPart} given its parent {@link Composite} and its side. |
| * |
| * @param viewer |
| * Parent viewer of this viewer part. |
| * @param composite |
| * Parent {@link Composite} for this part. |
| * @param side |
| * Comparison side of this part. Must be one of |
| * {@link EMFCompareConstants#LEFT EMFCompareConstants.RIGHT}, |
| * {@link EMFCompareConstants#RIGHT EMFCompareConstants.LEFT} or |
| * {@link EMFCompareConstants#ANCESTOR EMFCompareConstants.ANCESTOR}. |
| */ |
| public ModelContentMergeViewerPart(ModelContentMergeViewer viewer, Composite composite, int side) { |
| if (side != EMFCompareConstants.RIGHT && side != EMFCompareConstants.LEFT |
| && side != EMFCompareConstants.ANCESTOR) |
| throw new IllegalArgumentException("PartSide cannot be " + side); //$NON-NLS-1$ |
| |
| parentViewer = viewer; |
| selectedTab = ModelContentMergeViewer.TREE_TAB; |
| partSide = side; |
| createContents(composite); |
| } |
| |
| /** |
| * Creates the contents of this viewer part given its parent composite. |
| * |
| * @param composite |
| * Parent composite of this viewer parts's widgets. |
| */ |
| public void createContents(Composite composite) { |
| tabFolder = new CTabFolder(composite, SWT.BOTTOM); |
| final CTabItem treeTab = new CTabItem(tabFolder, SWT.NONE); |
| treeTab.setText("Tree"); //$NON-NLS-1$ |
| |
| final CTabItem propertiesTab = new CTabItem(tabFolder, SWT.NONE); |
| propertiesTab.setText("Properties"); //$NON-NLS-1$ |
| |
| final Composite treePanel = new Composite(tabFolder, SWT.NONE); |
| treePanel.setLayout(new GridLayout()); |
| treePanel.setLayoutData(new GridData(GridData.FILL_BOTH)); |
| treePanel.setFont(composite.getFont()); |
| tree = createTreePart(treePanel); |
| treeTab.setControl(treePanel); |
| |
| final Composite propertyPanel = new Composite(tabFolder, SWT.NONE); |
| propertyPanel.setLayout(new GridLayout()); |
| propertyPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); |
| propertyPanel.setFont(composite.getFont()); |
| properties = createPropertiesPart(propertyPanel, partSide); |
| propertiesTab.setControl(propertyPanel); |
| |
| tabFolder.addSelectionListener(new SelectionListener() { |
| public void widgetSelected(SelectionEvent e) { |
| if (e.item.equals(treeTab)) { |
| ModelContentMergeViewerPart.this.selectedTab = ModelContentMergeViewer.TREE_TAB; |
| } else { |
| if (e.item.equals(propertiesTab)) { |
| ModelContentMergeViewerPart.this.selectedTab = ModelContentMergeViewer.PROPERTIES_TAB; |
| } |
| } |
| fireSelectedtabChanged(); |
| } |
| |
| public void widgetDefaultSelected(SelectionEvent e) { |
| widgetSelected(e); |
| } |
| }); |
| |
| tabFolder.setSelection(treeTab); |
| } |
| |
| /** |
| * Returns the {@link Widget} representing the given element or <code>null</code> if it cannot be found. |
| * |
| * @param element |
| * Element to find the {@link Widget} for. |
| * @return The {@link Widget} representing the given element. |
| */ |
| public Widget find(EObject element) { |
| Widget widget = null; |
| if (element != null) { |
| if (selectedTab == ModelContentMergeViewer.TREE_TAB) { |
| widget = tree.findVisibleTreeItemFor(element); |
| } else if (selectedTab == ModelContentMergeViewer.PROPERTIES_TAB) { |
| if (element instanceof DiffElement) |
| widget = properties.find((DiffElement)element); |
| } else { |
| throw new IllegalStateException(INVALID_TAB); |
| } |
| } |
| return widget; |
| } |
| |
| /** |
| * Returns the height of the tab control's header. |
| * |
| * @return The height of the tab control's header. |
| */ |
| public int getHeaderHeight() { |
| int headerHeight = 0; |
| if (selectedTab == ModelContentMergeViewer.TREE_TAB) { |
| headerHeight = tree.getTree().getHeaderHeight(); |
| } else if (selectedTab == ModelContentMergeViewer.PROPERTIES_TAB) { |
| headerHeight = properties.getTable().getHeaderHeight(); |
| } else { |
| throw new IllegalStateException(INVALID_TAB); |
| } |
| return headerHeight; |
| } |
| |
| /** |
| * Returns a list of the selected tab's selected Elements. |
| * |
| * @return The selected tab's selected Elements. |
| */ |
| public List<TreeItem> getSelectedElements() { |
| List<TreeItem> selectedElements = null; |
| if (selectedTab == ModelContentMergeViewer.TREE_TAB) { |
| selectedElements = tree.getSelectedElements(); |
| } else if (selectedTab != ModelContentMergeViewer.PROPERTIES_TAB) { |
| throw new IllegalStateException(INVALID_TAB); |
| } |
| return selectedElements; |
| } |
| |
| /** |
| * Returns the width of the columns shown on the properties tab. |
| * |
| * @return The width of the columns shown on the properties tab. |
| */ |
| public int getTotalColumnsWidth() { |
| int width = 0; |
| if (selectedTab == ModelContentMergeViewer.PROPERTIES_TAB) { |
| for (final TableColumn col : properties.getTable().getColumns()) { |
| width += col.getWidth(); |
| } |
| } |
| return width; |
| } |
| |
| /** |
| * Returns the first root of the tree. |
| * |
| * @return The first root of the tree. |
| */ |
| public TreeItem getTreeRoot() { |
| if (tree.getVisibleElements().size() > 0) |
| return tree.getVisibleElements().get(0); |
| return null; |
| } |
| |
| /** |
| * Checks wether a given {@link Item} is visible. |
| * |
| * @param item |
| * Item to check. |
| * @return <code>True</code> if the item is visible, <code>False</code> otherwise. |
| */ |
| public boolean isVisible(Item item) { |
| if (item instanceof TreeItem) |
| return tree.getTree().getClientArea().contains(((TreeItem)item).getBounds().x, ((TreeItem)item).getBounds().y); |
| else |
| return true; |
| } |
| |
| /** |
| * Redraws this viewer part. |
| */ |
| public void layout() { |
| if (selectedTab == ModelContentMergeViewer.TREE_TAB) { |
| tree.getTree().redraw(); |
| } else if (selectedTab == ModelContentMergeViewer.PROPERTIES_TAB) { |
| properties.getTable().redraw(); |
| } else { |
| throw new IllegalStateException(INVALID_TAB); |
| } |
| } |
| |
| /** |
| * Sets the input of this viewer part. |
| * |
| * @param input |
| * New input of this viewer part. |
| */ |
| public void setInput(Object input) { |
| Object typedInput = input; |
| if (typedInput instanceof TypedElementWrapper) { |
| typedInput = ((TypedElementWrapper)typedInput).getObject(); |
| } |
| if (selectedTab == ModelContentMergeViewer.TREE_TAB) { |
| tree.setReflectiveInput((EObject)typedInput); |
| } else if (selectedTab == ModelContentMergeViewer.PROPERTIES_TAB) { |
| properties.setInput(typedInput); |
| } else { |
| throw new IllegalStateException(INVALID_TAB); |
| } |
| } |
| |
| /** |
| * Sets the receiver's size and location to the rectangular area specified by the arguments. |
| * |
| * @param x |
| * Desired x coordinate of the part. |
| * @param y |
| * Desired y coordinate of the part. |
| * @param width |
| * Desired width of the part. |
| * @param height |
| * Desired height of the part. |
| */ |
| public void setBounds(int x, int y, int width, int height) { |
| setBounds(new Rectangle(x, y, width, height)); |
| } |
| |
| /** |
| * Sets the receiver's size and location to given rectangular area. |
| * |
| * @param bounds |
| * Desired bounds for this receiver. |
| */ |
| public void setBounds(Rectangle bounds) { |
| tabFolder.setBounds(bounds); |
| resizeBounds(); |
| } |
| |
| /** |
| * Changes the current tab. |
| * |
| * @param index |
| * New tab to set selected. |
| */ |
| public void setSelectedTab(int index) { |
| selectedTab = index; |
| tabFolder.setSelection(selectedTab); |
| resizeBounds(); |
| } |
| |
| /** |
| * Shows the given item on the tree tab or its properties on the property tab. |
| * |
| * @param diff |
| * Item to scroll to. |
| */ |
| public void navigateToDiff(DiffElement diff) { |
| EObject target = null; |
| if (partSide == EMFCompareConstants.RIGHT) { |
| target = EMFCompareEObjectUtils.getLeftElement(diff); |
| final TreeItem treeItem = (TreeItem)find(target); |
| if (diff instanceof AddModelElement && treeItem != null) |
| treeItem.setExpanded(true); |
| } else if (partSide == EMFCompareConstants.LEFT) { |
| target = EMFCompareEObjectUtils.getRightElement(diff); |
| final TreeItem treeItem = (TreeItem)find(target); |
| if (diff instanceof RemoveModelElement && treeItem != null) |
| treeItem.setExpanded(true); |
| } |
| if (selectedTab == ModelContentMergeViewer.TREE_TAB) { |
| tree.showItem(target); |
| properties.setInput(findMatchFromElement(target)); |
| } else if (selectedTab == ModelContentMergeViewer.PROPERTIES_TAB) { |
| properties.setInput(findMatchFromElement(target)); |
| properties.showItem(diff); |
| } else { |
| throw new IllegalStateException(INVALID_TAB); |
| } |
| parentViewer.getConfiguration().setProperty(EMFCompareConstants.PROPERTY_CONTENT_SELECTION, diff); |
| parentViewer.updateCenter(); |
| } |
| |
| /** |
| * Registers the given listener for notification. If the identical listener is already registered the |
| * method has no effect. |
| * |
| * @param listener |
| * The listener to register for changes of this input. |
| */ |
| public void addCompareEditorPartListener(ICompareEditorPartListener listener) { |
| editorPartListeners.add(listener); |
| } |
| |
| protected void fireSelectedtabChanged() { |
| for (ICompareEditorPartListener listener : editorPartListeners) { |
| listener.selectedTabChanged(selectedTab); |
| } |
| } |
| |
| protected void fireSelectionChanged(SelectionChangedEvent event) { |
| for (ICompareEditorPartListener listener : editorPartListeners) { |
| listener.selectionChanged(event); |
| } |
| } |
| |
| protected void fireUpdateCenter() { |
| for (ICompareEditorPartListener listener : editorPartListeners) { |
| listener.updateCenter(); |
| } |
| } |
| |
| private Object findMatchFromElement(EObject element) { |
| Object theElement = null; |
| final MatchModel match = ((ModelCompareInput)parentViewer.getInput()).getMatch(); |
| |
| for (final TreeIterator iterator = match.eAllContents(); iterator.hasNext(); ) { |
| final Object object = iterator.next(); |
| |
| if (object instanceof Match2Elements) { |
| final Match2Elements matchElement = (Match2Elements)object; |
| if (matchElement.getLeftElement().equals(element) |
| || matchElement.getRightElement().equals(element)) { |
| theElement = matchElement; |
| } |
| } else if (object instanceof UnMatchElement) { |
| final UnMatchElement matchElement = (UnMatchElement)object; |
| if (matchElement.getElement().equals(element)) { |
| theElement = matchElement; |
| } |
| } |
| } |
| |
| return theElement; |
| } |
| |
| private ModelContentMergeTreePart createTreePart(Composite composite) { |
| final ModelContentMergeTreePart treePart = new ModelContentMergeTreePart(composite); |
| |
| treePart.setContentProvider(new AdapterFactoryContentProvider(EMFAdapterFactoryProvider |
| .getAdapterFactory())); |
| |
| treePart.getTree().addPaintListener(new TreePaintListener()); |
| |
| treePart.getTree().getVerticalBar().addSelectionListener(new SelectionListener() { |
| public void widgetSelected(SelectionEvent e) { |
| fireUpdateCenter(); |
| } |
| |
| public void widgetDefaultSelected(SelectionEvent e) { |
| widgetSelected(e); |
| } |
| }); |
| |
| treePart.getTree().addTreeListener(new TreeListener() { |
| public void treeCollapsed(TreeEvent e) { |
| ((TreeItem)e.item).setExpanded(false); |
| e.doit = false; |
| parentViewer.update(); |
| } |
| |
| public void treeExpanded(TreeEvent e) { |
| ((TreeItem)e.item).setExpanded(true); |
| e.doit = false; |
| parentViewer.update(); |
| } |
| }); |
| |
| treePart.addSelectionChangedListener(new ISelectionChangedListener() { |
| public void selectionChanged(SelectionChangedEvent event) { |
| fireSelectionChanged(event); |
| } |
| }); |
| |
| treePart.getTree().addSelectionListener(new SelectionAdapter() { |
| public void widgetSelected(SelectionEvent e) { |
| if (tree.getSelectedElements().size() > 0) { |
| final TreeItem selected = tree.getSelectedElements().get(0); |
| for (final DiffElement diff : ((ModelCompareInput)parentViewer.getInput()).getDiffAsList()) { |
| if (!(diff instanceof DiffGroup) && partSide == EMFCompareConstants.RIGHT) { |
| if (selected.getData().equals(EMFCompareEObjectUtils.getLeftElement(diff))) { |
| parentViewer.setSelection(diff); |
| } |
| } else if (!(diff instanceof DiffGroup) && partSide == EMFCompareConstants.LEFT) { |
| if (selected.getData().equals(EMFCompareEObjectUtils.getRightElement(diff))) { |
| parentViewer.setSelection(diff); |
| } |
| } |
| } |
| if (!selected.isDisposed() && selected.getData() instanceof EObject) |
| properties.setInput(findMatchFromElement((EObject)selected.getData())); |
| } |
| } |
| }); |
| |
| return treePart; |
| } |
| |
| private ModelContentMergePropertyPart createPropertiesPart(Composite composite, int side) { |
| final ModelContentMergePropertyPart propertiesPart = new ModelContentMergePropertyPart(composite, |
| SWT.NONE, partSide); |
| |
| propertiesPart.setContentProvider(new PropertyContentProvider()); |
| propertiesPart.getTable().setHeaderVisible(true); |
| propertiesPart.getTable().addPaintListener(new PropertyPaintListener()); |
| |
| propertiesPart.getTable().getVerticalBar().addSelectionListener(new SelectionListener() { |
| public void widgetSelected(SelectionEvent e) { |
| parentViewer.updateCenter(); |
| } |
| |
| public void widgetDefaultSelected(SelectionEvent e) { |
| widgetSelected(e); |
| } |
| }); |
| |
| propertiesPart.addSelectionChangedListener(new ISelectionChangedListener() { |
| public void selectionChanged(SelectionChangedEvent event) { |
| fireSelectionChanged(event); |
| } |
| }); |
| |
| return propertiesPart; |
| |
| } |
| |
| private void resizeBounds() { |
| if (selectedTab == ModelContentMergeViewer.TREE_TAB) { |
| tree.getTree().setBounds(tabFolder.getClientArea()); |
| } else if (selectedTab == ModelContentMergeViewer.PROPERTIES_TAB) { |
| properties.getTable().setBounds(tabFolder.getClientArea()); |
| } else { |
| throw new IllegalStateException(INVALID_TAB); |
| } |
| } |
| |
| /** |
| * This implementation of {@link PaintListener} handles the drawing of blocks around modified members in |
| * the tree tab. |
| */ |
| private class TreePaintListener implements PaintListener { |
| public void paintControl(PaintEvent event) { |
| // This will avoid strange random resize behavior on linux OS |
| if (tree.getTree().getBounds() != tabFolder.getClientArea()) |
| resizeBounds(); |
| for (final DiffElement diff : ((ModelCompareInput)parentViewer.getInput()).getDiffAsList()) { |
| if (partSide == EMFCompareConstants.RIGHT) { |
| drawRectangle(event, (TreeItem)parentViewer.getLeftItem(diff), diff); |
| } else if (partSide == EMFCompareConstants.LEFT) { |
| drawRectangle(event, (TreeItem)parentViewer.getRightItem(diff), diff); |
| } |
| } |
| } |
| |
| private void drawRectangle(PaintEvent event, TreeItem treeItem, DiffElement diff) { |
| final Rectangle treeBounds = tree.getTree().getBounds(); |
| final Rectangle treeItemBounds = treeItem.getBounds(); |
| RGB color = parentViewer.getChangedColor(); |
| |
| // Defines the circling Color |
| if (diff instanceof AddModelElement) { |
| color = parentViewer.getAddedColor(); |
| } else if (diff instanceof RemoveModelElement) { |
| color = parentViewer.getRemovedColor(); |
| } |
| |
| /* |
| * We add a margin before the rectangle to circle the "+" as well as the tree line. |
| */ |
| final int margin = 60; |
| |
| // Defines all variables needed for drawing the rectangle. |
| final int rectangleX = treeItemBounds.x - margin; |
| final int rectangleY = treeItemBounds.y; |
| final int rectangleWidth = treeItemBounds.width + margin; |
| final int rectangleHeight = treeItemBounds.height - 1; |
| final int rectangleArcWidth = 5; |
| final int rectangleArcHeight = 5; |
| |
| int lineWidth = 1; |
| // if the item is selected, we set a bigger line width |
| if (getSelectedElements().contains(treeItem)) { |
| lineWidth = 2; |
| } |
| |
| // Performs the actual drawing |
| event.gc.setLineWidth(lineWidth); |
| event.gc.setForeground(new Color(treeItem.getDisplay(), color)); |
| if (partSide == EMFCompareConstants.RIGHT) { |
| if (!treeItem.getData().equals(EMFCompareEObjectUtils.getLeftElement(diff)) |
| || diff instanceof AddModelElement) { |
| event.gc.setLineStyle(SWT.LINE_SOLID); |
| event.gc.drawLine(rectangleX, rectangleY + rectangleHeight, treeBounds.width, rectangleY |
| + rectangleHeight); |
| } else { |
| event.gc.setLineStyle(SWT.LINE_DASHDOT); |
| event.gc.drawRoundRectangle(rectangleX, rectangleY, rectangleWidth, rectangleHeight, |
| rectangleArcWidth, rectangleArcHeight); |
| event.gc.setLineStyle(SWT.LINE_SOLID); |
| event.gc.drawLine(rectangleX + rectangleWidth, rectangleY + rectangleHeight / 2, |
| treeBounds.width, rectangleY + rectangleHeight / 2); |
| } |
| } else if (partSide == EMFCompareConstants.LEFT) { |
| if (!treeItem.getData().equals(EMFCompareEObjectUtils.getRightElement(diff)) |
| || diff instanceof RemoveModelElement) { |
| event.gc.setLineStyle(SWT.LINE_SOLID); |
| event.gc.drawLine(rectangleX + rectangleWidth, rectangleY + rectangleHeight, |
| treeBounds.x, rectangleY + rectangleHeight); |
| } else { |
| event.gc.setLineStyle(SWT.LINE_DASHDOT); |
| event.gc.drawRoundRectangle(rectangleX, rectangleY, rectangleWidth, rectangleHeight, |
| rectangleArcWidth, rectangleArcHeight); |
| event.gc.setLineStyle(SWT.LINE_SOLID); |
| event.gc.drawLine(rectangleX, rectangleY + rectangleHeight / 2, treeBounds.x, rectangleY |
| + rectangleHeight / 2); |
| } |
| } |
| } |
| } |
| |
| /** |
| * This implementation of {@link PaintListener} handles the drawing of blocks around modified members in |
| * the properties tab. |
| */ |
| private class PropertyPaintListener implements PaintListener { |
| public void paintControl(PaintEvent event) { |
| for (final DiffElement diff : ((ModelCompareInput)parentViewer.getInput()).getDiffAsList()) { |
| if (diff instanceof AttributeChange && find(diff) != null |
| && partSide == EMFCompareConstants.RIGHT) { |
| drawLine(event, (TableItem)parentViewer.getLeftItem(diff)); |
| } |
| } |
| } |
| |
| private void drawLine(PaintEvent event, TableItem tableItem) { |
| final Rectangle tableBounds = properties.getTable().getBounds(); |
| final Rectangle tableItemBounds = tableItem.getBounds(); |
| tableItem.setBackground(new Color(tableItem.getDisplay(), parentViewer.getHighlightColor())); |
| |
| final int lineY = tableItemBounds.y + tableItemBounds.height / 2; |
| |
| event.gc.setLineWidth(2); |
| event.gc.setForeground(new Color(tableItem.getDisplay(), parentViewer.getChangedColor())); |
| event.gc.drawLine(getTotalColumnsWidth(), lineY, tableBounds.width, lineY); |
| } |
| } |
| } |