/******************************************************************************* * Copyright (c) 2004, 2008 John Krasnay and others. * 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: * John Krasnay - initial API and implementation *******************************************************************************/ package net.sf.vex.action; import java.util.ArrayList; import java.util.List; import net.sf.vex.core.IntRange; import net.sf.vex.css.CSS; import net.sf.vex.css.StyleSheet; import net.sf.vex.dom.Document; import net.sf.vex.dom.Element; import net.sf.vex.dom.Node; import net.sf.vex.layout.BlockBox; import net.sf.vex.layout.Box; import net.sf.vex.layout.ElementOrRangeCallback; import net.sf.vex.layout.LayoutUtils; import net.sf.vex.layout.TableRowBox; import net.sf.vex.widget.IBoxFilter; import net.sf.vex.widget.IVexWidget; /** * Static helper methods used across actions. */ public class ActionUtils { public static class RowColumnInfo { public Object row; public Object cell; public int rowIndex; public int cellIndex; public int rowCount; public int columnCount; public int maxColumnCount; } /** * Clone the table cells from the given TableRowBox to the current offset in vexWidget. * @param vexWidget IVexWidget to modify. * @param tr TableRowBox whose cells are to be cloned. * @param moveToFirstCell TODO */ public static void cloneTableCells(final IVexWidget vexWidget, final TableRowBox tr, final boolean moveToFirstCell) { vexWidget.doWork(new Runnable() { public void run() { int offset = vexWidget.getCaretOffset(); boolean firstCellIsAnonymous = false; Box[] cells = tr.getChildren(); for (int i = 0; i < cells.length; i++) { if (cells[i].isAnonymous()) { vexWidget.insertText(" "); if (i == 0) { firstCellIsAnonymous = true; } } else { vexWidget.insertElement((Element) cells[i].getElement().clone()); vexWidget.moveBy(+1); } } if (moveToFirstCell) { vexWidget.moveTo(offset + 1); if (firstCellIsAnonymous) { vexWidget.moveBy(-1, true); } } } }); } /** * Duplicate the given table row, inserting a new empty one below it. The new * row contains empty children corresponding to the given row's children. * @param vexWidget IVexWidget with which we're working * @param tr TableRowBox to be duplicated. */ public static void duplicateTableRow(final IVexWidget vexWidget, final TableRowBox tr) { vexWidget.doWork(new Runnable() { public void run() { vexWidget.moveTo(tr.getEndOffset()); if (!tr.isAnonymous()) { vexWidget.moveBy(+1); // Move past sentinel in current row vexWidget.insertElement((Element) tr.getElement().clone()); } cloneTableCells(vexWidget, tr, true); } }); } /** * Returns true if the given element or range is at least partially selected. * * @param vexWidget IVexWidget being tested. * @param elementOrRange Element or IntRange being tested. */ public static boolean elementOrRangeIsPartiallySelected(IVexWidget vexWidget, Object elementOrRange) { IntRange range = getInnerRange(elementOrRange); return range.getEnd() >= vexWidget.getSelectionStart() && range.getStart() <= vexWidget.getSelectionEnd(); } /** * Returns the zero-based index of the table column containing the * current offset. Returns -1 if we are not inside a table. */ public static int getCurrentColumnIndex(IVexWidget vexWidget) { Element row = getCurrentTableRow(vexWidget); if (row == null) { return -1; } final int offset = vexWidget.getCaretOffset(); final int[] column = new int[] { -1 }; LayoutUtils.iterateTableCells(vexWidget.getStyleSheet(), row, new ElementOrRangeCallback() { private int i = 0; public void onElement(Element child, String displayStyle) { if (offset > child.getStartOffset() && offset <= child.getEndOffset()) { column[0] = i; } i++; } public void onRange(Element parent, int startOffset, int endOffset) { i++; } }); return column[0]; } /** * Returns the innermost Element with style table-row containing the caret, * or null if no such element exists. * @param vexWidget IVexWidget to use. */ public static Element getCurrentTableRow(IVexWidget vexWidget) { StyleSheet ss = vexWidget.getStyleSheet(); Element element = vexWidget.getCurrentElement(); while (element != null) { if (ss.getStyles(element).getDisplay().equals(CSS.TABLE_ROW)) { return element; } element = element.getParent(); } return null; } /** * Returns the start offset of the next sibling of the parent element. * Returns -1 if there is no previous sibling in the parent. * @param vexWidget VexWidget to use. */ public static int getPreviousSiblingStart(IVexWidget vexWidget) { int startOffset; if (vexWidget.hasSelection()) { startOffset = vexWidget.getSelectionStart(); } else { Box box = vexWidget.findInnermostBox(new IBoxFilter() { public boolean matches(Box box) { return box instanceof BlockBox && box.getElement() != null; } }); if (box.getElement() == vexWidget.getDocument().getRootElement()) { return -1; } startOffset = box.getElement().getStartOffset(); } int previousSiblingStart = -1; Element parent = vexWidget.getDocument().getElementAt(startOffset); Node[] children = parent.getChildNodes(); for (int i = 0; i < children.length; i++) { Node child = children[i]; if (startOffset == child.getStartOffset()) { break; } previousSiblingStart = child.getStartOffset(); } return previousSiblingStart; } /** * Returns an array of the selected block boxes. Text nodes between boxes * are not returned. If the selection does not enclose any block boxes, * returns an empty array. * @param vexWidget VexWidget to use. */ public static BlockBox[] getSelectedBlockBoxes(final IVexWidget vexWidget) { if (!vexWidget.hasSelection()) { return new BlockBox[0]; } Box parent = vexWidget.findInnermostBox(new IBoxFilter() { public boolean matches(Box box) { System.out.println("Matching " + box); return box instanceof BlockBox && box.getStartOffset() <= vexWidget.getSelectionStart() && box.getEndOffset() >= vexWidget.getSelectionEnd(); } }); System.out.println("Matched " + parent); List blockList = new ArrayList(); Box[] children = parent.getChildren(); System.out.println("Parent has " + children.length + " children"); for (int i = 0; i < children.length; i++) { Box child = children[i]; if (child instanceof BlockBox && child.getStartOffset() >= vexWidget.getSelectionStart() && child.getEndOffset() <= vexWidget.getSelectionEnd()) { System.out.println(" adding " + child); blockList.add(child); } else { System.out.println(" skipping " + child); } } return (BlockBox[]) blockList.toArray(new BlockBox[blockList.size()]); } /** * Returns the currently selected table rows, or the current row if * ther is no selection. If no row can be found, returns an empty array. * @param vexWidget IVexWidget to use. */ public static SelectedRows getSelectedTableRows(final IVexWidget vexWidget) { final SelectedRows selected = new SelectedRows(); ActionUtils.iterateTableCells(vexWidget, new TableCellCallback() { public void startRow(Object row, int rowIndex) { if (ActionUtils.elementOrRangeIsPartiallySelected(vexWidget, row)) { if (selected.rows == null) { selected.rows = new ArrayList(); } selected.rows.add(row); } else { if (selected.rows == null) { selected.rowBefore = row; } else { if (selected.rowAfter == null) { selected.rowAfter = row; } } } } public void onCell(Object row, Object cell, int rowIndex, int cellIndex) { } public void endRow(Object row, int rowIndex) { } }); return selected; } public static void iterateTableCells(IVexWidget vexWidget, final TableCellCallback callback) { final StyleSheet ss = vexWidget.getStyleSheet(); iterateTableRows(vexWidget, new ElementOrRangeCallback() { final private int[] rowIndex = { 0 }; public void onElement(final Element row, String displayStyle) { callback.startRow(row, rowIndex[0]); LayoutUtils.iterateTableCells(ss, row, new ElementOrRangeCallback() { private int cellIndex = 0; public void onElement(Element cell, String displayStyle) { callback.onCell(row, cell, rowIndex[0], cellIndex); cellIndex++; } public void onRange(Element parent, int startOffset, int endOffset) { callback.onCell(row, new IntRange(startOffset, endOffset), rowIndex[0], cellIndex); cellIndex++; } }); callback.endRow(row, rowIndex[0]); rowIndex[0]++; } public void onRange(Element parent, final int startOffset, final int endOffset) { final IntRange row = new IntRange(startOffset, endOffset); callback.startRow(row, rowIndex[0]); LayoutUtils.iterateTableCells(ss, parent, startOffset, endOffset, new ElementOrRangeCallback() { private int cellIndex = 0; public void onElement(Element cell, String displayStyle) { callback.onCell(row, cell, rowIndex[0], cellIndex); cellIndex++; } public void onRange(Element parent, int startOffset, int endOffset) { callback.onCell(row, new IntRange(startOffset, endOffset), rowIndex[0], cellIndex); cellIndex++; } }); callback.endRow(row, rowIndex[0]); rowIndex[0]++; } }); } /** * Returns a RowColumnInfo structure containing information about the table * containing the caret. Returns null if the caret is not currently inside * a table. * * @param vexWidget IVexWidget to inspect. */ public static RowColumnInfo getRowColumnInfo(IVexWidget vexWidget) { final boolean[] found = new boolean[1]; final RowColumnInfo[] rcInfo = new RowColumnInfo[] { new RowColumnInfo() }; final int offset = vexWidget.getCaretOffset(); rcInfo[0].cellIndex = -1; rcInfo[0].rowIndex = -1; iterateTableCells(vexWidget, new TableCellCallback() { int rowColumnCount; public void startRow(Object row, int rowIndex) { rowColumnCount = 0; } public void onCell(Object row, Object cell, int rowIndex, int cellIndex) { found[0] = true; if (LayoutUtils.elementOrRangeContains(row, offset)) { rcInfo[0].row = row; rcInfo[0].rowIndex = rowIndex; rcInfo[0].columnCount++; if (LayoutUtils.elementOrRangeContains(cell, offset)) { rcInfo[0].cell = cell; rcInfo[0].cellIndex = cellIndex; } } rowColumnCount++; } public void endRow(Object row, int rowIndex) { rcInfo[0].rowCount++; rcInfo[0].maxColumnCount = Math.max(rcInfo[0].maxColumnCount, rowColumnCount); } }); if (found[0]) { return rcInfo[0]; } else { return null; } } /** * Iterate over all rows in the table containing the caret. * * @param vexWidget IVexWidget to iterate over. * @param callback Caller-provided callback that this method calls for * each row in the current table. */ public static void iterateTableRows(IVexWidget vexWidget, ElementOrRangeCallback callback) { final StyleSheet ss = vexWidget.getStyleSheet(); final Document doc = vexWidget.getDocument(); final int offset = vexWidget.getCaretOffset(); // This may or may not be a table // In any case, it's the element that contains the top-level table children Element table = doc.getElementAt(offset); while (table != null && !LayoutUtils.isTableChild(ss, table)) { table = table.getParent(); } while (table != null && LayoutUtils.isTableChild(ss, table)) { table = table.getParent(); } if (table == null || table.getParent() == null) { return; } final List tableChildren = new ArrayList(); final boolean[] found = new boolean[] { false }; LayoutUtils.iterateChildrenByDisplayStyle(ss, LayoutUtils.TABLE_CHILD_STYLES, table, new ElementOrRangeCallback() { public void onElement(Element child, String displayStyle) { if (offset >= child.getStartOffset() && offset <= child.getEndOffset()) { found[0] = true; } tableChildren.add(child); } public void onRange(Element parent, int startOffset, int endOffset) { if (!found[0]) { tableChildren.clear(); } } }); if (!found[0]) { return; } int startOffset = ((Element) tableChildren.get(0)).getStartOffset(); int endOffset = ((Element) tableChildren.get(tableChildren.size() - 1)).getEndOffset() + 1; LayoutUtils.iterateTableRows(ss, table, startOffset, endOffset, callback); } /** * Returns an IntRange representing the offsets inside the given Element or * IntRange. If an Element is passed, returns the offsets inside the * sentinels. If an IntRange is passed it is returned directly. * * @param elementOrRange Element or IntRange to be inspected. */ public static IntRange getInnerRange(Object elementOrRange) { if (elementOrRange instanceof Element) { Element element = (Element) elementOrRange; return new IntRange(element.getStartOffset() + 1, element.getEndOffset()); } else { return (IntRange) elementOrRange; } } /** * Returns an IntRange representing the offsets outside the given Element or * IntRange. If an Element is passed, returns the offsets outside the * sentinels. If an IntRange is passed it is returned directly. * * @param elementOrRange Element or IntRange to be inspected. */ public static IntRange getOuterRange(Object elementOrRange) { if (elementOrRange instanceof Element) { Element element = (Element) elementOrRange; return new IntRange(element.getStartOffset(), element.getEndOffset() + 1); } else { return (IntRange) elementOrRange; } } public static class SelectedRows { private SelectedRows() { } public List getRows() { return this.rows; } public Object getRowBefore() { return this.rowBefore; } public Object getRowAfter() { return this.rowAfter; } private List rows; private Object rowBefore; private Object rowAfter; } }