org.sigmah.client.ui.view.project.logframe.grid.FlexTableView.java Source code

Java tutorial

Introduction

Here is the source code for org.sigmah.client.ui.view.project.logframe.grid.FlexTableView.java

Source

package org.sigmah.client.ui.view.project.logframe.grid;

/*
 * #%L
 * Sigmah
 * %%
 * Copyright (C) 2010 - 2016 URD
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import com.allen_sauer.gwt.log.client.Log;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;

/**
 * Maintains a group of rows in a flex table. The group of rows is shown with a rowspan on the first column of the
 * table.
 * 
 * @author tmi (v1.3)
 * @author HUZHE (v1.3)
 * @author Denis Colliot (dcolliot@ideia.fr) (v2.0)
 */
public class FlexTableView {

    /**
     * Listen to view events.
     * 
     * @author tmi (v1.3)
     * @author Denis Colliot (dcolliot@ideia.fr) (v2.0)
     */
    public static interface FlexTableViewListener {

        /**
         * Method called when a group is added to this view.
         * 
         * @param group
         *          The new group.
         */
        void groupAdded(RowsGroup<?> group);

        /**
         * Method called when a row is added to a group.
         * 
         * @param group
         *          The group.
         * @param row
         *          The new row.
         */
        void rowAdded(RowsGroup<?> group, Row<?> row);

        /**
         * Method called when a row is removed from a group.
         * 
         * @param group
         *          The group.
         * @param row
         *          The old row.
         */
        void rowRemoved(RowsGroup<?> group, Row<?> row);
    }

    /**
     * CSS style name for the entire view.
     */
    private static final String CSS_FLEX_TABLE_VIEW_STYLE_NAME = "flextable-view";

    /**
     * CSS style name for the cells which display groups.
     */
    private static final String CSS_GROUP_CELL_STYLE_NAME = CSS_FLEX_TABLE_VIEW_STYLE_NAME + "-group-cell";

    /**
     * CSS style name for the labels which display groups.
     */
    private static final String CSS_GROUP_LABEL_STYLE_NAME = CSS_FLEX_TABLE_VIEW_STYLE_NAME + "-group-label";

    /**
     * CSS style name for the merged rows.
     */
    private static final String CSS_MERGED_ROWS_STYLE_NAME = CSS_FLEX_TABLE_VIEW_STYLE_NAME + "-merged-row";

    /**
     * The parent flex table.
     */
    private final FlexTable table;

    /**
     * The other views (lower in the same table) which depend on this view. When this view adds or removes rows, its
     * dependent views will be incremented or decremented to keep a consistent index.
     */
    private final ArrayList<FlexTableView> dependencies;

    /**
     * The difference between the first row index of this view and the first row index of the entire table. This value
     * should be incremented by others views if there are displayed above.
     */
    private int shift;

    /**
     * The column to show the group of rows.
     */
    private final int groupColumnIndex;

    /**
     * The number of fillable columns.
     */
    private final int columnsCount;

    /**
     * An ordered list of the current displayed groups.
     */
    private final ArrayList<RowsGroup<?>> groupsOrderedList;

    /**
     * The key of this map is the unique integer which identifies a group of rows.
     */
    private final HashMap<Integer, RowsGroup<?>> groupsCodesMap;

    /**
     * Listeners.
     */
    private final ArrayList<FlexTableViewListener> listeners;

    /**
     * Initializes this group of rows.
     * 
     * @param table
     *          The parent flex table.
     * @param columnsCount
     *          The total columns count.
     * @param row
     *          The row index.
     */
    public FlexTableView(FlexTable table, int columnsCount, int row) {

        // Checks if the table isn't null.
        if (table == null) {
            throw new NullPointerException("table must not be null");
        }

        // Checks if the row indexes an existing row.
        if (row < 0 || row >= table.getRowCount()) {
            throw new IllegalArgumentException("the flex table does not have a row at index #" + row + ".");
        }

        // Checks if the table contains enough columns.
        if (columnsCount < 2) {
            throw new IllegalArgumentException("the flex table does not contains enought columns (min 2).");
        }

        // Sets the table.
        this.table = table;

        // Initializes the local lists.
        groupsOrderedList = new ArrayList<RowsGroup<?>>();
        groupsCodesMap = new HashMap<Integer, RowsGroup<?>>();
        dependencies = new ArrayList<FlexTableView>();
        listeners = new ArrayList<FlexTableViewListener>();

        // At the beginning, the shift count is equals to the row index.
        shift = row;

        // The first column is used to show the group of rows (rowspan).
        groupColumnIndex = 0;

        // The number of the others columns with can contains widgets.
        this.columnsCount = columnsCount - 1;
    }

    // ------------------------------------------------------------------------
    // -- LISTENERS
    // ------------------------------------------------------------------------

    /**
     * Adds a listener.
     * 
     * @param l
     *          The new listener.
     */
    public void addFlexTableViewListener(FlexTableViewListener l) {
        this.listeners.add(l);
    }

    /**
     * Removes a listener.
     * 
     * @param l
     *          The old listener.
     */
    public void removeFlexTableViewListener(FlexTableViewListener l) {
        this.listeners.remove(l);
    }

    /**
     * Method called when a group is added to this view.
     * 
     * @param group
     *          The new group.
     */
    protected void fireGroupAdded(final RowsGroup<?> group) {
        for (final FlexTableViewListener l : listeners) {
            l.groupAdded(group);
        }
    }

    /**
     * Method called when a row is added to a group.
     * 
     * @param group
     *          The group.
     * @param row
     *          The new row.
     */
    protected void fireRowAdded(RowsGroup<?> group, Row<?> row) {
        for (final FlexTableViewListener l : listeners) {
            l.rowAdded(group, row);
        }
    }

    /**
     * Method called when a row is removed from a group.
     * 
     * @param group
     *          The group.
     * @param row
     *          The old row.
     */
    protected void fireRowRemoved(RowsGroup<?> group, Row<?> row) {
        for (final FlexTableViewListener l : listeners) {
            l.rowRemoved(group, row);
        }
    }

    // ------------------------------------------------------------------------
    // -- DEPENDENCIES
    // ------------------------------------------------------------------------

    /**
     * Adds a dependency to this view.
     * 
     * @param other
     *          The other view.
     */
    public void addDependency(FlexTableView other) {

        // Checks if the other view is correct.
        if (other == null) {
            throw new NullPointerException("other must not be null");
        }

        // Checks if the two views share the same table.
        if (table != other.table) {
            throw new IllegalArgumentException("the other view doesn't share the same table as the current view");
        }

        dependencies.add(other);
    }

    /**
     * Increments the shift count of each dependency.
     */
    private void incrementDependencies() {

        for (final FlexTableView dependency : dependencies) {
            dependency.shift++;
        }
    }

    /**
     * Decrements the shift count of each dependency.
     */
    private void decrementDependencies() {

        for (final FlexTableView dependency : dependencies) {
            dependency.shift--;
        }
    }

    // ------------------------------------------------------------------------
    // -- FLEX TABLE
    // ------------------------------------------------------------------------

    /**
     * Inserts a row in the table. This method considers the shift.
     * 
     * @param beforeRow
     *          The index before which the new row will be inserted.
     * @return The new row index.
     */
    protected int insertTableRow(int beforeRow) {

        // Inserts the new row.
        int row = table.insertRow(beforeRow + shift);

        // Adjusts the row span.
        table.getFlexCellFormatter().setRowSpan(shift, groupColumnIndex,
                table.getFlexCellFormatter().getRowSpan(shift, groupColumnIndex) + 1);

        // Applies the row styles.
        HTMLTableUtils.applyRowStyles(table, row);

        // Impacts the adding of row on dependencies.
        incrementDependencies();

        return row;
    }

    /**
     * Removes a row from the table. This method considers the shift.
     * 
     * @param row
     *          The index of the row to remove.
     */
    protected void removeTableRow(int row) {

        // Removes the row.
        table.removeRow(row + shift);

        // Adjusts the row span.
        table.getFlexCellFormatter().setRowSpan(shift, groupColumnIndex,
                table.getFlexCellFormatter().getRowSpan(shift, groupColumnIndex) - 1);

        // Impacts the removing of row on dependencies.
        decrementDependencies();
    }

    // ------------------------------------------------------------------------
    // -- GROUPS
    // ------------------------------------------------------------------------

    /**
     * Inserts a new group of rows at the last position.
     * 
     * @param group
     *          The group.
     * @throws NullPointerException
     *           If the group is <code>null</code>.
     * @throws IllegalArgumentException
     *           If a group with the same id already exists.
     */
    public void addGroup(final RowsGroup<?> group) {
        insertGroup(groupsOrderedList.size() + 1, group);
    }

    /**
     * Remove a log group when it is empty (should be verified before this action).
     * 
     * @param group
     *          The group to remove.
     */
    public void removeGroup(final RowsGroup<?> group) {

        // Get the index of this group in the ordered group list
        int groupIndexInGroups = groupsOrderedList.indexOf(group);

        // Get the index in the view, the shift is not considered
        int groupIndexInView = computeGroupIndex(groupIndexInGroups + 1);

        // Remove the row from the table,the shift is considered in the removeTableRow method
        this.removeTableRow(groupIndexInView);

        // Remove the group from ordered list and the map
        groupsOrderedList.remove(group);
        groupsCodesMap.remove(group.getId());
    }

    /**
     * Inserts a new group of rows at the given position.
     * 
     * @param position
     *          The position at which the group will be inserted among the groups list (for example, a index equals to
     *          <code>2</code> means that the group will be the second one).
     *          If this index is lower or equal than <code>0</code>, the group will be the first one. An index greater
     *          than the number of group will insert the group at the last position.
     * @param group
     *          The group.
     * @throws NullPointerException
     *           If the group is <code>null</code>.
     * @throws IllegalArgumentException
     *           If a group with the same id already exists.
     */
    public void insertGroup(int position, final RowsGroup<?> group) {

        // Checks if the group is valid.
        if (group == null) {
            throw new NullPointerException("The group must not be null.");
        }

        final int id = group.getId();
        // Checks if the group doesn't exist already.
        if (groupsCodesMap.get(id) != null) {
            throw new IllegalArgumentException("The group with id #" + id + " already exists.");
        }

        // Re-adjusts the position to avoid out of bounds errors.
        if (position <= 0 || groupsOrderedList.isEmpty()) {
            position = 1;
        } else if (position > groupsOrderedList.size()) {
            position = groupsOrderedList.size() + 1;
        }

        if (Log.isDebugEnabled()) {
            Log.debug("[insertGroup] Inserts the new group #" + id + " at position # " + position + ".");
        }

        // Computes new group indexes.
        int row = computeGroupIndex(position);
        row = insertTableRow(row);
        int column = 0;

        // Builds group's widget.
        final Widget widget = group.getWidget();

        // Adds widget and sets row.
        table.setWidget(row, column, widget);
        table.getFlexCellFormatter().setColSpan(row, column, columnsCount + 2);
        HTMLTableUtils.applyCellStyles(table, row, column, false, true);
        table.getFlexCellFormatter().addStyleName(row, column, CSS_GROUP_CELL_STYLE_NAME);
        widget.addStyleName(CSS_GROUP_LABEL_STYLE_NAME);

        // Adds the group locally at the correct position.
        groupsOrderedList.add(position - 1, group);
        groupsCodesMap.put(group.getId(), group);

        // Hides the group header if needed.
        if (!group.isVisible()) {
            widget.setVisible(false);
        }

        fireGroupAdded(group);
    }

    /**
     * Gets the group with the given id if it exists. Returns <code>null</code> otherwise.
     * 
     * @param groupId
     *          The group id.
     * @return The group with this id, <code>null</code> otherwise.
     */
    public RowsGroup<?> getGroup(int groupId) {
        return groupsCodesMap.get(groupId);
    }

    /**
     * Gets the index at which a group must be inserted to be at the given position. This index <strong>does'nt</strong>
     * consider the shift.
     * Use the {@link FlexTableView#insertTableRow(int)} method to insert a row considering the shift.
     * 
     * @param position
     *          The position at which the group will be inserted among the groups list (for example, a index equals to
     *          <code>2</code> means that the group will be the second one).
     *          If this index is lower or equal than <code>0</code>, the group will be the first one. An index greater
     *          than the number of group will insert the group at the last position.
     * @see FlexTableView#insertTableRow(int)
     */
    protected int computeGroupIndex(int position) {

        // Default index (no group already displayed).
        int index = 1;

        // Browses the list of existing groups until the desired position is reached.
        RowsGroup<?> group;
        for (int i = 0; i < position - 1 && i < groupsOrderedList.size(); i++) {

            // For each group, increments the index with the number of elements it contains.
            group = groupsOrderedList.get(i);
            index += 1 + group.getRowsCount();
        }

        return index;
    }

    /**
     * Gets the position of a group. The position is included in the interval [1;GROUPS_COUNT]. If the group doesn't
     * exist, an exception is thrown.
     * 
     * @param group
     *          The group.
     * @return The group position in the interval [1;GROUPS_COUNT].
     * @throws NullPointerException
     *           If the group is <code>null</code>.
     * @throws IllegalArgumentException
     *           If the group doesn't exist.
     */
    protected int getGroupPosition(final RowsGroup<?> group) {

        // Checks if the group is valid.
        if (group == null) {
            throw new NullPointerException("The group must not be null.");
        }

        // Gets the group index in the ordered list.
        final int position = groupsOrderedList.indexOf(group);

        // The group doesn't exist.
        if (position == -1) {
            throw new IllegalArgumentException("The group with id #" + group.getId() + " doesn't exist.");
        }

        return position + 1;
    }

    /**
     * Gets the row index of a group. If the group doesn't exist, an exception is thrown.
     * 
     * @param group
     *          The group.
     * @return The row index of this group.
     * @throws NullPointerException
     *           If the group is <code>null</code>.
     * @throws IllegalArgumentException
     *           If the group doesn't exist.
     */
    protected int getGroupRowIndex(final RowsGroup<?> group) {

        // Checks if the group is valid.
        if (group == null) {
            throw new NullPointerException("The group must not be null.");
        }

        // Default index (no group already displayed).
        int index = 0;

        // Browses the list of existing groups until the searched group is reached.
        for (final RowsGroup<?> g : groupsOrderedList) {

            index++;

            if (g.equals(group)) {
                return index;
            }

            // For each group, increments the index with the number of elements it contains.
            index += g.getRowsCount();
        }

        // The group hasn't been found.
        throw new IllegalArgumentException("The group with id #" + group.getId() + " doesn't exist.");
    }

    /**
     * Gets the number of groups in this view.
     * 
     * @return The number of groups in this view.
     */
    public int getGroupsCount() {
        return groupsOrderedList.size();
    }

    /**
     * Refreshes the group widget.
     * 
     * @param group
     *          The group.
     */
    public void refreshGroupWidget(final RowsGroup<?> group) {

        // Checks if the group is valid.
        if (group == null) {
            throw new NullPointerException("The group must not be null.");
        }

        if (Log.isDebugEnabled()) {
            Log.debug("[refreshGroupWidget] Refreshes the group #" + group.getId() + ".");
        }

        // Computes new group indexes.
        int row = getGroupRowIndex(group) + shift;
        int column = 0;

        // Builds group's widget.
        final Widget widget = group.getWidget();

        // Sets the new widget.
        table.setWidget(row, column, widget);

        // Applies style names.
        HTMLTableUtils.applyCellStyles(table, row, column, false, true);
        widget.addStyleName(CSS_GROUP_LABEL_STYLE_NAME);
    }

    // ------------------------------------------------------------------------
    // -- ROWS (IN GROUP)
    // ------------------------------------------------------------------------

    /**
     * Inserts a row in the given group at the last position.
     * 
     * @param <T>
     *          The type of the user object contained in this row.
     * @param groupId
     *          The id of the group in which the row will be inserted.
     * @param row
     *          The row.
     * @throws NullPointerException
     *           If the row is <code>null</code>.
     * @throws IllegalArgumentException
     *           If there isn't a group with the given id.
     */
    public <T> void addRow(final int groupId, final Row<T> row) {

        // By default the row is inserted at the end of the group.
        // (sets the position to the infinite value to avoid group searching which is done by the sub method).
        insertRow(Integer.MAX_VALUE, groupId, row);
    }

    /**
     * Inserts a row in the given group at the given position.
     * 
     * @param <T>
     *          The type of the user object contained in this row.
     * @param position
     *          The row position in its group (for example, a index equals to <code>2</code> means that the row will be
     *          the second one in its group).
     *          If this index is lower or equal than <code>0</code>, the row will be the first one. An index greater than
     *          the number of rows in this group will insert the row at the last position.
     * @param groupId
     *          The id of the group in which the row will be inserted.
     * @param row
     *          The row.
     * @throws NullPointerException
     *           If the row is <code>null</code>.
     * @throws IllegalArgumentException
     *           If there isn't a group with the given id.
     */
    @SuppressWarnings("unchecked")
    public <T> void insertRow(int position, final int groupId, final Row<T> row) {

        // Checks if the row is valid.
        if (row == null) {
            throw new NullPointerException("The row must not be null.");
        }

        // Checks if the group code is valid.
        final RowsGroup<?> group;
        if ((group = groupsCodesMap.get(groupId)) == null) {
            throw new IllegalArgumentException("The group #" + groupId + " does'nt exist.");
        }

        // Re-adjusts the position to avoid out of bounds errors.
        if (position <= 0 || group.getRowsCount() == 0) {
            position = 1;
        } else if (position > group.getRowsCount()) {
            position = group.getRowsCount() + 1;
        }

        if (Log.isDebugEnabled()) {
            Log.debug("[insertRow] Inserts the new row #" + row.getId() + " in group #" + groupId + " at position #"
                    + position + ".");
        }

        // Computes new row indexes.
        int rowIndex = computeRowIndex(group, position);
        rowIndex = insertTableRow(rowIndex);

        // Indexes of the columns which manage merging.
        final List<Integer> merge = new ArrayList<Integer>();
        for (int index : group.getMergedColumnIndexes()) {
            merge.add(index);
        }

        int column = 0;
        int colSpan = 0;
        // Adds each column widget.
        for (int j = 0; j < columnsCount; j++) {

            // Gets the widget at this column index.
            final Widget w = row.getWidgetAt(j);

            if (w == null) {
                colSpan++;
                column--;
            } else {

                table.setWidget(rowIndex, column, w);

                // If there is any col span to perform.
                if (colSpan != 0) {
                    table.getFlexCellFormatter().setColSpan(rowIndex, column, colSpan + 1);
                }

                HTMLTableUtils.applyCellStyles(table, rowIndex, column, false, false);

                // Reinit the col span.
                colSpan = 0;
            }

            // Checks if this column can be merged.
            if (merge.contains(j)) {

                // Gets the top row if any.
                final Row<?> topRow;
                if ((topRow = group.getRowAtPosition(position - 1)) != null) {

                    // If the rows properties are similar, removes the widget.
                    if (row.isSimilar(j, row.getUserObject(), ((Row<T>) topRow).getUserObject())) {
                        table.setWidget(rowIndex, column, new Label(""));
                        table.getFlexCellFormatter().addStyleName(rowIndex, column, CSS_MERGED_ROWS_STYLE_NAME);
                    }
                }
            }

            column++;
        }

        // Adds the row locally.
        group.addRow(row, position);

        try {
            // Refreshes the direct bottom row of the just inserted row.
            refreshMergedRow(group, position + 1);
        }
        // The row doesn't exist, nothing to do.
        catch (IndexOutOfBoundsException e) {
            // Digests exception.
        }

        fireRowAdded(group, row);
    }

    /**
     * Removes the given row from the given group.
     * 
     * @param group
     *          The group in which the row is inserted.
     * @param rowId
     *          The row id.
     * @throws NullPointerException
     *           If the group is <code>null</code>.
     * @throws IllegalArgumentException
     *           If the row doesn't exist.
     */
    public void removeRow(final RowsGroup<?> group, final int rowId) {

        // Checks if the group is valid.
        if (group == null) {
            throw new IllegalArgumentException("The group must not be null.");
        }

        // Checks if the row exists in this group.
        final Row<?> row;
        if ((row = group.getRow(rowId)) == null) {
            throw new IllegalArgumentException(
                    "The row with id #" + rowId + " does'nt exist in group #" + group.getId() + ".");
        }

        // Saves the old position of the removed row.
        final int oldPosition = group.getRowPosition(row);

        // Gets the row index.
        final int index = getRowIndex(row);

        // Removes the row in the table.
        removeTableRow(index);

        // Removes the row locally.
        group.removeRow(row);

        try {
            // Refreshes the direct bottom row of the just removed row.
            refreshMergedRow(group, oldPosition);
        }
        // The row doesn't exist, nothing to do.
        catch (IndexOutOfBoundsException e) {
            // Digests exception.
        }

        fireRowRemoved(group, row);
    }

    /**
     * Refreshes a row styles and widgets considering the merged columns indexes.
     * 
     * @param group
     *          The group in which the row is inserted.
     * @param position
     *          The position of the row to refresh.
     * @throws IndexOutOfBoundsException
     *           If there is no row at the given position.
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    private void refreshMergedRow(RowsGroup<?> group, int position) throws IndexOutOfBoundsException {

        // If the row was the last one, nothing to do.
        if (group.getRowsCount() == 0) {
            return;
        }

        // Gets the row to refresh.
        final Row row = group.getRowAtPosition(position);

        // Computes this row index.
        int rowIndex = computeRowIndex(group, position) + shift;

        // Indexes of the columns which manage merging.
        final List<Integer> merge = new ArrayList<Integer>();
        for (int index : group.getMergedColumnIndexes()) {
            merge.add(index);
        }

        int column = 0;
        // Adds each column widget.
        for (int j = 0; j < columnsCount; j++) {

            // Gets the widget at this column index.
            final Widget w = row.getWidgetAt(j);

            if (w == null) {
                column--;
            }

            // Checks if this column can be merged.
            if (merge.contains(j)) {

                // Gets the top row if any.
                final Row topRow;
                if ((topRow = group.getRowAtPosition(position - 1)) != null) {

                    // If the rows properties are similar, removes the widget.
                    if (row.isSimilar(j, row.getUserObject(), topRow.getUserObject())) {
                        table.setWidget(rowIndex, column, new Label(""));
                        table.getFlexCellFormatter().addStyleName(rowIndex, column, CSS_MERGED_ROWS_STYLE_NAME);
                    } else {
                        table.setWidget(rowIndex, column, w);
                        HTMLTableUtils.applyCellStyles(table, rowIndex, column, false, false);
                        table.getFlexCellFormatter().removeStyleName(rowIndex, column, CSS_MERGED_ROWS_STYLE_NAME);
                    }
                } else {
                    table.setWidget(rowIndex, column, w);
                    HTMLTableUtils.applyCellStyles(table, rowIndex, column, false, false);
                    table.getFlexCellFormatter().removeStyleName(rowIndex, column, CSS_MERGED_ROWS_STYLE_NAME);
                }
            }

            column++;
        }
    }

    /**
     * Moves a row inside its group.
     * 
     * @param group
     *          The group in which the row is inserted.
     * @param rowId
     *          The id of the row to move.
     * @param move
     *          The number of moves to execute. If this count is higher than the available moves inside the row's group,
     *          the excess is ignored.
     *          A null integer has no effect.
     *          A positive integer will move the row upward.
     *          A negative integer will move the row downward.
     * @throws NullPointerException
     *           If the group is <code>null</code>.
     * @throws IllegalArgumentException
     *           If the row doesn't exist.
     */
    public void moveRow(final RowsGroup<?> group, final int rowId, int move) {

        // No move.
        if (move == 0) {
            return;
        }

        // Checks if the group is valid.
        if (group == null) {
            throw new IllegalArgumentException("The group must not be null.");
        }

        // The group contains no or only one row, nothing to do.
        final int rowsCount = group.getRowsCount();
        if (group.getRowsCount() <= 1) {
            return;
        }

        // Checks if the row exists in this group.
        final Row<?> row;
        if ((row = group.getRow(rowId)) == null) {
            throw new IllegalArgumentException(
                    "The row #" + rowId + " does'nt exist in group #" + group.getId() + ".");
        }

        // Gets the row position in its group.
        final int rowPosition = row.getParent().getRowPosition(row);

        // Checks if the row can be moved.
        if (move > 0) {

            // The row is already the first one, nothing to do.
            if (rowPosition == 1) {
                return;
            }
        } else {

            // The row is already the last one, nothing to do.
            if (rowPosition == rowsCount) {
                return;
            }
        }

        // Re-adjusts the moves count to avoid out of bounds errors.
        if (move > 0) {

            final int avalaibleMovesCount = rowPosition - 1;
            if (move > avalaibleMovesCount) {
                move = avalaibleMovesCount;
            }
        } else {

            final int avalaibleMovesCount = rowsCount - rowPosition;
            if (Math.abs(move) > avalaibleMovesCount) {
                move = -avalaibleMovesCount;
            }
        }

        // Removes the row.
        removeRow(group, rowId);

        // Re-inserts it at its new position.
        insertRow(rowPosition - move, group.getId(), row);
    }

    /**
     * Returns if a row can be moved for the given moves count.
     * 
     * @param group
     *          The group in which the row is inserted.
     * @param rowId
     *          The id of the row to move.
     * @param move
     *          The number of moves to execute. If this count is higher than the available moves inside the row's group,
     *          the excess is ignored.
     *          A null integer has no effect.
     *          A positive integer will move the row upward.
     *          A negative integer will move the row downward.
     * @return If the row can be moved for the given moves count.
     * @throws NullPointerException
     *           If the group is <code>null</code>.
     * @throws IllegalArgumentException
     *           If the row doesn't exist.
     */
    public boolean canBeMoved(final RowsGroup<?> group, final int rowId, int move) {

        // No move.
        if (move == 0) {
            return false;
        }

        // Checks if the group is valid.
        if (group == null) {
            throw new IllegalArgumentException("The group must not be null.");
        }

        // The group contains no or only one row, nothing to do.
        final int rowsCount = group.getRowsCount();
        if (group.getRowsCount() <= 1) {
            return false;
        }

        // Checks if the row exists in this group.
        final Row<?> row;
        if ((row = group.getRow(rowId)) == null) {
            throw new IllegalArgumentException(
                    "The row #" + rowId + " does'nt exist in group #" + group.getId() + ".");
        }

        // Gets the row position in its group.
        final int rowPosition = row.getParent().getRowPosition(row);

        // Checks if the row can be moved.
        if (move > 0) {

            // The row is already the first one, nothing to do.
            if (rowPosition == 1) {
                return false;
            }
        } else {

            // The row is already the last one, nothing to do.
            if (rowPosition == rowsCount) {
                return false;
            }
        }

        return true;
    }

    /**
     * Gets the index at which a row must be inserted to be contained in the given group at the given position. This index
     * <strong>does'nt</strong> consider the shift.
     * Use the {@link FlexTableView#insertTableRow(int)} method to insert a row considering the shift.
     * 
     * @param group
     *          The group in which the row will be inserted.
     * @param position
     *          The row position in its group (for example, a index equals to <code>2</code> means that the row will be
     *          the second one in its group).
     *          If this index is lower or equal than <code>0</code>, the row will be the first one. An index greater than
     *          the number of rows in this group will insert the row at the last position.
     * @throws NullPointerException
     *           If the group is <code>null</code>.
     * @throws IllegalArgumentException
     *           If the group doesn't exist.
     * @see FlexTableView#insertTableRow(int)
     */
    protected int computeRowIndex(final RowsGroup<?> group, int position) {

        // Computes group row index.
        final int groupRowIndex = getGroupRowIndex(group);

        // Re-adjusts the position to avoid out of bounds errors.
        if (position <= 0 || group.getRowsCount() == 0) {
            position = 1;
        } else if (position > group.getRowsCount()) {
            position = group.getRowsCount() + 1;
        }

        // The row position is added to the group row index.
        return groupRowIndex + position;
    }

    /**
     * Gets the row index of a row.
     * 
     * @param row
     *          The row.
     * @throws NullPointerException
     *           If the row is <code>null</code>.
     * @throws IllegalArgumentException
     *           If this row doesn't exist.
     */
    protected int getRowIndex(final Row<?> row) {

        // Checks if the row is valid.
        if (row == null) {
            throw new NullPointerException("The row must not be null.");
        }

        // Gets the row group.
        final RowsGroup<?> parent = row.getParent();

        // Computes group row index.
        final int groupRowIndex = getGroupRowIndex(parent);

        // Gets the row position.
        final int rowPosition = parent.getRowPosition(row);

        return groupRowIndex + rowPosition;
    }

    /**
     * Gets the number of rows in this view.
     * 
     * @return The number of rows in this view.
     */
    public int getRowsCount() {

        int count = 0;
        for (final RowsGroup<?> group : groupsOrderedList) {
            count += group.getRowsCount();
        }

        return count;
    }
}