All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.globalmentor.swing.text.ContainerBoxView Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 1996-2009 GlobalMentor, Inc. 
 *
 * Licensed 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 com.globalmentor.swing.text;

import java.awt.Insets;
import java.util.*;

import javax.swing.text.*;
import static com.globalmentor.java.Objects.*;
import static com.globalmentor.swing.text.Views.*;

import com.globalmentor.awt.Inset;
import com.globalmentor.log.Log;

/**
 * A view that contains other views, constrained in a box. No assumptions are made about the order, size, or offsets of the child views, unlike
 * BoxView. This class consequently determines view index based upon document position without relying on underlying element order and offsets.
 * Beginning and ending offsets are determined based upon the contained views. This view knows how to break into fragments along the tiling axis.
 * @author Garret Wilson
 */
public class ContainerBoxView extends BoxView implements Inset {

	/** The shared default break strategy for container views. */
	public static final ViewBreakStrategy DEFAULT_BREAK_STRATEGY = new ContainerBreakStrategy();

	/** The stategy for breaking this view into fragments. */
	private ViewBreakStrategy breakStrategy = DEFAULT_BREAK_STRATEGY;

	/** @return The stategy for breaking this view into fragments. */
	protected ViewBreakStrategy getBreakStrategy() {
		return breakStrategy;
	}

	/**
	 * Sets the stategy for breaking this view into fragments.
	 * @param strategy The strategy to use for creating view fragments.
	 */
	protected void setBreakStrategy(final ViewBreakStrategy strategy) {
		breakStrategy = strategy;
	}

	/** Shared constant empty array of views. */
	protected static final View[] NO_VIEWS = new View[0];

	/** true if the cache is based upon recent information, even though there may not be a cached value available. */
	private boolean cacheValid = false;

	/** Invalidates the cached values so that they can be recalculated when needed. */
	protected void invalidateCache() {
		cacheValid = false;
	}

	/** The last known element starting offset; used to ensure that changed content hasn't changed our cached starting or ending offset values. */
	private int lastElementStartOffset;

	/** The last known element ending offset; used to ensure that changed content hasn't changed our cached starting or ending offset values. */
	private int lastElementEndOffset;

	/** The cached starting offset, or -1 if there is no cached value. */
	private int startOffset = -1;

	/** The cached ending offset, or -1 if there is no cached value. */
	private int endOffset = -1;

	/**
	 * Constructor.
	 * @param element The element this view is responsible for.
	 * @param axis Either View.X_AXIS or View.Y_AXIS
	 */
	public ContainerBoxView(final Element element, final int axis) {
		super(element, axis); //construct the parent class
	}

	/** @return The insets of the view. */
	public Insets getInsets() {
		return new Insets(getTopInset(), getLeftInset(), getBottomInset(), getRightInset()); //return the insets of the view
	}

	/**
	 * Replaces child views, which can function as an insert or a remove. This version invalidates the cached values so that they can be recalculated when needed.
	 * @param offset The starting index (0<=offset<=getViewCount()) into the child views to insert the new views.
	 * @param length The number of existing child views (0<=length<=getViewCount()-offset) to remove.
	 * @param views The child views to add, or null if no children are being added.
	 * @see #invalidateCache()
	 */
	public void replace(final int offset, final int length, final View[] views) {
		super.replace(offset, length, views); //do the default replacement
		invalidateCache(); //invalidate the cache
	}

	/**
	 * @return true if the cache is based upon recent information, even though there may not be a cached value available. This implementation not only
	 *         checks the cache valid flag, but also verifies the underlying element's starting and ending offset to make sure the content hasn't changed.
	 */
	protected boolean isCacheValid() {
		final Element element = getElement(); //get the underlying element
		return cacheValid && lastElementStartOffset == element.getStartOffset() && lastElementEndOffset == element.getEndOffset(); //make sure the cache is valid and the content hasn't changed
	}

	/**
	 * Checks the cache and updates it if necessary.
	 * @see #updateCache()
	 */
	protected void verifyCache() {
		if(!isCacheValid()) { //if the cache is not valid
			updateCache(); //update the cache
		}
	}

	/** Updates the cached values, including beginning and ending offset. */
	protected void updateCache() {
		final int viewCount = getViewCount(); //find out how many view are contained in this view
		if(viewCount > 0) { //if we have child views
			startOffset = Integer.MAX_VALUE; //we'll start out with a high number, and we'll end up with the lowest starting offset of all the views
			endOffset = 0; //start out with a low ending offset, and we'll wind up with the largest ending offset
			for(int viewIndex = viewCount - 1; viewIndex >= 0; --viewIndex) { //look at each child view in the container view
				final View childView = getView(viewIndex); //get a reference to this child view
				final int childStartOffset = childView.getStartOffset(); //get the child's starting offset
				if(childStartOffset < startOffset) { //if this child start offset is lower than what we've alredy found
					startOffset = childStartOffset; //update our starting offset
				}
				final int childEndOffset = childView.getEndOffset(); //get the child's ending offset
				if(childEndOffset > endOffset) { //if this child end offset is higher than what we've alredy found
					endOffset = childEndOffset; //update our ending offset
				}
			}
		} else { //if we don't have any child views
			startOffset = endOffset = -1; //show that there are no cached values; the beginning and ending offsets of the underlying element will have to be returned
		}
		final Element element = getElement(); //get the underlying element
		lastElementStartOffset = getElement().getStartOffset(); //update our record of the underlying element's offsets to see if the content changes
		lastElementEndOffset = getElement().getEndOffset();
		cacheValid = true; //show that the cached values are now valid
	}

	/**
	 * Fetches the portion of the model for which this view is responsible. This version uses the cached starting offset, if available; otherwise, the default
	 * starting offset is returned.
	 * @return The starting offset into the model (>=0).
	 */
	public int getStartOffset() {
		verifyCache(); //make sure the cache is valid
		return startOffset >= 0 ? startOffset : super.getStartOffset(); //return the cached value or the default if there is no cached value
	}

	/**
	 * Fetches the portion of the model for which this view is responsible. This version uses the cached ending offset, if available; otherwise, the default
	 * ending offset is returned.
	 * @return The ending offset into the model (>=0).
	 */
	public int getEndOffset() {
		verifyCache(); //make sure the cache is valid
		return endOffset >= 0 ? endOffset : super.getEndOffset(); //return the cached value or the default if there is no cached value
	}

	/**
	 * Returns the index of the child at the given model position in the container. This implementation queries each view directly and does not assume that there
	 * is a direct correspondence between each child view and the underlying element hierarchy.
	 * @param pos The position (>=0) in the model.
	 * @return The index of the view representing the given position, or -1 if there is no view on this container which represents that position.
	 */
	protected int getViewIndexAtPosition(final int pos) {
		return ContainerView.getViewIndexAtPosition(this, pos); //delegate to the utility method
	}

	/**
	 * A break strategy for views that contain other views and may be parceled up into fragments. If a view and its fragment both support managed components, this
	 * strategy transfers managed components to the fragment.
	 * @author Garret Wilson
	 */
	public static class ContainerBreakStrategy implements ViewBreakStrategy {

		/**
		 * Breaks a view on the given axis at the given length. This is implemented to attempt to break on the largest number of child views that can fit within the
		 * given length.
		 * @param view The view to break.
		 * @param axis The axis to break along, either View.X_AXIS or View.Y_AXIS.
		 * @param offset The location in the model where the fragment should start its representation (>=0).
		 * @param pos the position along the axis that the broken view would occupy (>=0).
		 * @param length The distance along the axis where a potential break is desired (>=0).
		 * @param fragmentViewFactory The source of fragment views.
		 * @return The fragment of the view that represents the given span, if the view can be broken. If the view doesn't support breaking behavior, the view
		 *         itself is returned.
		 * @see View#breakView
		 */
		public View breakView(final BoxView view, final int axis, final int offset, final float pos, final float length,
				final FragmentViewFactory fragmentViewFactory) {
			//TODO del Log.trace("Inside XMLBlockView.breakView axis: ", axis, "p0:", p0, "pos:", pos, "len:", len, "name:", XMLStyleUtilities.getXMLElementName(getAttributes()));	//TODO del
			if(axis == view.getAxis() && length < view.getPreferredSpan(axis)) { //if they want to break along our tiling axis and they want less of us than we prefer, we'll try to break
				final int childViewCount = view.getViewCount(); //get the number of child views we have
				if(childViewCount > 0) { //if we have child views
					final List childViewList = new ArrayList(); //create a new list for accumulating child views
					final boolean isViewFragment = view instanceof FragmentView; //see if the view we are breaking is itself a fragment
					final boolean viewRepresentsFirst = !isViewFragment || ((FragmentView)view).isFirstFragment(); //see if the view represents the first fragment of the original view (or the view is the original view)
					final boolean viewRepresentsLast = !isViewFragment || ((FragmentView)view).isLastFragment(); //see if the view represents the last fragment of the original view (or the view is the original view)
					boolean isFirstFragment = false; //start out assuming this is not the first fragment
					boolean isLastFragment = false; //start out assuming this is not the last fragment
					float totalSpan = 0; //we'll use this to accumulate the size of each view to be included
					int startOffset = offset; //we'll continually update this as we create new child view fragments
					int childIndex; //start looking at the first child to find one that can be included in our break
					for(childIndex = 0; childIndex < childViewCount && view.getView(childIndex).getEndOffset() <= startOffset; ++childIndex)
						; //find the first child that ends after our first model location
					for(; childIndex < childViewCount && totalSpan < length; ++childIndex) { //look at each child view at and including the first child we found that will go inside this fragment, and keep adding children until we find enough views to fill up the space or we run out of views
						final View childView = view.getView(childIndex); //get a reference to this child view; we may change this variable if we have to break one of the child views
						final float childPreferredSpan = childView.getPreferredSpan(axis); //get the child's preferred span along the axis
						Log.trace("looking to include child view: ", childView.getClass().getName()); //TODO del
						Log.trace("child view preferred span: ", childPreferredSpan); //TODO del
						final float remainingSpan = length - totalSpan; //calculate the span we have left
						final View newChildView; //we'll determine which child to add
						final float newChildPreferredSpan; //we'll determine the size of the new child
						if(childPreferredSpan > remainingSpan) { //if this view is too big to fit into our space
							//TODO old comment: we really only need to add something if this is the root XMLBlockView -- but how do we know that?
							final boolean mustBreak = childViewList.size() == 0; //if we haven't fit any views into our fragment, we must have at least one, even if it doesn't fit
							final boolean canBreak = mustBreak || childView.getBreakWeight(axis, pos, remainingSpan) > BadBreakWeight; //see if this view can be broken to be made to fit
							if(canBreak) { //if we can break
								newChildView = childView.breakView(axis, Math.max(childView.getStartOffset(), startOffset), pos, remainingSpan); //break the view to fit, taking into account that the view might have started before our break point
								newChildPreferredSpan = newChildView.getPreferredSpan(axis); //get the new child's preferred span along the axis
								if(!mustBreak && newChildPreferredSpan > remainingSpan) { //if this view is still too big to fit into our space, and we don't have to break
									break; //stop trying to fit things
								}
							} else { //if this view can't break and we're not required to break
								break; //stop trying to fit things
							}
						} else { //if we can take the whole view
							newChildView = childView; //take the whole view as it is
							newChildPreferredSpan = childPreferredSpan; //the view prefers as much as it originally did
						}
						childViewList.add(newChildView); //add the new child view, may could have been chopped up into a fragment itself
						totalSpan += newChildPreferredSpan; //show that we've used up more space
						if(viewRepresentsFirst && childIndex == 0) { //if we just added the first child and we would know if it really is the first
							isFirstFragment = true; //show that the new fragment will be the first fragment
						}
						if(viewRepresentsLast && childIndex == childViewCount - 1) { //if we just added the last child and we would know if it really is the last
							isLastFragment = true; //show that the new fragment will be the last fragment
						}
						if(newChildView != childView) { //if we were not able to return the whole view
							break; //stop trying to add more child views; more views may fit (especially hidden views), but they would be skipping content that we've already lost by breaking this child view
						}
					}
					final View fragmentView = createFragmentView(fragmentViewFactory, view, childViewList.toArray(new View[childViewList.size()]), isFirstFragment,
							isLastFragment); //create a fragment view with the collected children
					fragmentComponents(view, fragmentView); //fragment managed components if necessary
					return fragmentView; //return the fragment view we created
				}
			}
			return view; //if they want to break along another axis or we weren't able to break, return our entire view
		}

		/**
		 * Creates a view that represents a portion of the element. If the view doesn't support fragmenting, the view itself will be returned. This implementation
		 * returns a new view that contains the required child views.
		 * @param view The view to break.
		 * @param p0 The starting offset (>=0). This should be a value greater or equal to the element starting offset and less than the element ending offset.
		 * @param p1 The ending offset (>p0). This should be a value less than or equal to the elements end offset and greater than the elements starting offset.
		 * @param fragmentViewFactory The source of fragment views.
		 * @return The view fragment, or the view itself if the view doesn't support breaking into fragments.
		 * @see View#createFragment
		 */
		public View createFragment(final BoxView view, int p0, int p1, final FragmentViewFactory fragmentViewFactory) {
			if(p0 <= view.getStartOffset() && p1 >= view.getEndOffset()) //if the range they want encompasses all of our view
				return view; //return the whole view; there's no use to try to break it up
			else { //if the range they want only includes part of our view
				final List childViewList = new ArrayList(); //create a new list for accumulating child views
				final boolean isViewFragment = view instanceof FragmentView; //see if the view we are breaking is itself a fragment
				final boolean viewRepresentsFirst = !isViewFragment || ((FragmentView)view).isFirstFragment(); //see if the view represents the first fragment of the original view (or the view is the original view)
				final boolean viewRepresentsLast = !isViewFragment || ((FragmentView)view).isLastFragment(); //see if the view represents the last fragment of the original view (or the view is the original view)
				final int childViewCount = view.getViewCount(); //find out how many child views there are
				//see if we'll include the first child in any form; if so, we're the first fragment
				final boolean isFirstFragment = viewRepresentsFirst && childViewCount > 0 && p0 <= view.getView(0).getStartOffset();
				//see if we'll include the last child in any form; if so, we're the last fragment
				final boolean isLastFragment = viewRepresentsLast && childViewCount > 0 && p1 >= view.getView(childViewCount - 1).getEndOffset();
				for(int i = 0; i < childViewCount; ++i) { //look at each child view
					final View childView = view.getView(i); //get a reference to this child view
					final int childViewStartOffset = childView.getStartOffset(); //get the child view's starting offset
					final int childViewEndOffset = childView.getEndOffset(); //get the child view's ending offset
					if(childViewEndOffset > p0 && childViewStartOffset < p1) { //if this view is within our range
						final int startPos = Math.max(p0, childViewStartOffset); //find out where we want to start, staying within this child view
						final int endPos = Math.min(p1, childViewEndOffset); //find out where we want to end, staying within this child view
						childViewList.add(childView.createFragment(startPos, endPos)); //add a portion (or all) of this child to our list of views
					}
				}
				final View fragmentView = createFragmentView(fragmentViewFactory, view, childViewList.toArray(new View[childViewList.size()]), isFirstFragment,
						isLastFragment); //create a fragment view with the collected children
				fragmentComponents(view, fragmentView); //fragment managed components if necessary
				return fragmentView; //return the fragment view we created
			}
		}

		/**
		 * Creates a fragment view into which pieces of this view will be placed. The fragment view will be given the correct parent.
		 * @param fragmentViewFactory The source of fragment views.
		 * @param view The view to break.
		 * @param childViews the child views to include in the fragment
		 * @param isFirstFragment Whether this fragment holds the first part of the original view.
		 * @param isLastFragment Whether this fragment holds the last part of the original view.
		 */
		protected View createFragmentView(final FragmentViewFactory fragmentViewFactory, final View parentView, final View[] childViews,
				final boolean isFirstFragment, final boolean isLastFragment) {
			final View fragmentView = fragmentViewFactory.createFragmentView(isFirstFragment, isLastFragment); //create a fragment of the view
			fragmentView.setParent(parentView); //give the fragment the correct parent
			fragmentView.replace(0, fragmentView.getViewCount(), childViews); //add the child views to the fragment
			//TODO reparenting when fragmenting seems to fix the problems that have been encountered, but what other break strategies? what about unfragmented views whose children have been fragmented and then unfragmented, yet the parent views never see this method so they can never have their hierarchies reparented?
			reparentHierarchy(fragmentView); //reparent the entire hierarchy of the fragment view, because some of the children's children may have been fragmented and then unfragmented
			return fragmentView; //return the fragment view we created
		}

		/**
		 * Fragments the managed components of a view, if supported, into its fragment view.
		 * @param fromView The view being fragmented.
		 * @param toFragmentView The fragment view into which the original view is being fragmented.
		 * @throws ClassCastException if toFragmentView is not a FragmentView.
		 */
		protected void fragmentComponents(final BoxView fromView, final View toFragmentView) {
			final FragmentView fragmentView = checkType(toFragmentView, FragmentView.class); //get the destination view as a fragment view
			try { //TODO later modify this code to see if the component locations fall within our span
				if(fragmentView instanceof ViewComponentManageable && fromView instanceof ViewComponentManageable) { //if the original view and the fragment both support managed components
					final boolean isFirstFragment = fragmentView.isFirstFragment(); //see if this is the first fragment
					final boolean isLastFragment = fragmentView.isLastFragment(); //see if this is the last fragment
					final ViewComponentManager componentManager = ((ViewComponentManageable)fromView).getComponentManager(); //get the original view's component manager
					final ViewComponentManager fragmentComponentManager = ((ViewComponentManageable)fragmentView).getComponentManager(); //get the fragment view's component manager
					for(final ViewComponentManager.ComponentInfo componentInfo : componentManager.getComponentInfos()) { //for each component information
						final boolean transferComponent; //we'll determine whether we should transfer this component
						final ViewComponentManager.Position componentPosition = componentInfo.getPosition(); //get the component position
						if(componentPosition instanceof ViewComponentManager.RegionPosition) { //if the position of the component is in a region
							//get the position as a region position
							final ViewComponentManager.RegionPosition regionPosition = (ViewComponentManager.RegionPosition)componentPosition;
							//get the location of the relevant axis TODO i18n check orientation
							final ViewComponentManager.AxisLocation axisLocation = fromView.getAxis() == View.X_AXIS ? regionPosition.getLocationX() : regionPosition
									.getLocationY();
							final ViewComponentManager.AxisLocation.Region region = axisLocation.getRegion(); //see which region the component is in
							switch(region) { //determine whether to transfer the component based upon its region
								case BEFORE:
									transferComponent = isFirstFragment; //include all "before" components only in the first fragment
									break;
								case MIDDLE:
									transferComponent = isFirstFragment; //include all "middle" components only in the first fragment
									break;
								case AFTER:
									transferComponent = isLastFragment; //include all "after" components only in the last fragment
									break;
								default:
									throw new AssertionError("Unrecognized region " + region);
							}
						} else { //if the component is not in a specified region
							transferComponent = isFirstFragment; //include all non-region components only in the first fragment
						}
						if(transferComponent) { //if we should transfer this component
							fragmentComponentManager.add((ViewComponentManager.ComponentInfo)componentInfo.clone()); //add a clone of the component information								
						}
					}
				}
			} catch(final CloneNotSupportedException cloneNotSupportedException) { //cloning should always be supported for the objects we use
				throw new AssertionError(cloneNotSupportedException);
			}
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy