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

io.guise.framework.coupler.AbstractCardCoupler Maven / Gradle / Ivy

There is a newer version: 0.5.3
Show newest version
/*
 * Copyright © 2005-2008 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 io.guise.framework.coupler;

import static com.globalmentor.java.Classes.*;

import static java.util.Arrays.*;

import java.beans.PropertyVetoException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import static java.util.Collections.*;

import com.globalmentor.beans.*;
import com.globalmentor.java.Objects;
import com.globalmentor.model.TaskState;

import io.guise.framework.component.*;
import io.guise.framework.component.layout.*;
import io.guise.framework.event.*;
import io.guise.framework.model.*;

/**
 * Abstract coupler to one or more cards in a {@link CardControl}. This coupler is only functional when the given card is contained within a {@link CardControl}
 * . This coupler can behave as if a single card or multiple cards are connected, firing both the {@link #CARD_PROPERTY} and {@link #CARDS_PROPERTY} property
 * change events when cards are changed. If the card change results in the same card in the first position in the list, the {@link #CARD_PROPERTY} is not fired.
 * @author Garret Wilson
 */
public class AbstractCardCoupler extends GuiseBoundPropertyObject //TODO listen for the card control changing; do something to keep card couplers from leaking memory
{

	/** The bound property of the connected card. */
	public static final String CARD_PROPERTY = getPropertyName(AbstractCardCoupler.class, "card");
	/** The bound property of the connected cards. */
	public static final String CARDS_PROPERTY = getPropertyName(AbstractCardCoupler.class, "cards");

	/**
	 * The flag indicating if we are currently updaing the selected state, so that we can prevent selecting an incorrect card if another card is being selected;
	 * access to this variable should be synchronized on this.
	 */
	private boolean updatingSelected = false;

	/** The property change listener to listen for the selected card changing. */
	private final GenericPropertyChangeListener selectedCardChangeListener = new AbstractGenericPropertyChangeListener() {

		@Override
		public void propertyChange(final GenericPropertyChangeEvent propertyChangeEvent) { //if the selected card changes
			updateSelected(); //update the selected status
		}

	};

	/** The property change listener to listen for the card displayed status changing and change the action accordingly. */
	private final GenericPropertyChangeListener displayedChangeListener = new AbstractGenericPropertyChangeListener() {

		@Override
		public void propertyChange(final GenericPropertyChangeEvent propertyChangeEvent) { //if the displayed status changes
			updateDisplayed(); //update the displayed status based upon all the cards
		}

	};

	/** The property change listener to listen for the card enabled status changing and reflect that value in the action. */
	private final GenericPropertyChangeListener enabledChangeListener = new AbstractGenericPropertyChangeListener() {

		@Override
		public void propertyChange(final GenericPropertyChangeEvent propertyChangeEvent) { //if the enabled status changes
			updateEnabled(); //update the enabled status based upon all the cards
		}

	};

	/** The property change listener to listen for the card task status changing and reflect that value in the action. */
	private final GenericPropertyChangeListener taskStateChangeListener = new AbstractGenericPropertyChangeListener() {

		@Override
		public void propertyChange(final GenericPropertyChangeEvent propertyChangeEvent) { //if the task state changes
			updateTaskState(); //update the task state based upon all the cards
		}

	};

	/** The property change listener to listen for card constraints changing. */
	private final GenericPropertyChangeListener constraintsChangeListener = new AbstractGenericPropertyChangeListener() {

		@Override
		public void propertyChange(final GenericPropertyChangeEvent propertyChangeEvent) { //if the constrants change
			final Constraints oldCardConstraints = propertyChangeEvent.getOldValue(); //get the old card constraints
			if(oldCardConstraints != null) { //if there were old card constraints
				uninstallCardConstraints(oldCardConstraints); //uninstall the old card constraints
			}
			final Constraints newCardConstraints = propertyChangeEvent.getNewValue(); //get the new card constraints
			if(newCardConstraints != null) { //if there are new card constraints
				installCardConstraints(newCardConstraints); //install the old card constraints
			}
		}

	};

	/** The thread-safe list of connected cards. */
	private List cards = emptyList();

	/** @return The connected cards. */
	public List getCards() {
		return new ArrayList(cards);
	} //return a copy of the cards list 

	/** @return The first connected card, or null if there are no connected cards. */
	public Component getCard() {
		return !cards.isEmpty() ? cards.get(0) : null;
	} //return the first card, if there is one 

	/**
	 * Sets the connected card. This is a bound property.
	 * @param newCard The new card to be connected.
	 * @see #CARD_PROPERTY
	 */
	public void setCard(final Component newCard) {
		final List newCards = new ArrayList(); //create a new list
		newCards.add(newCard); //add the card to the list
		setCards(newCards); //set the cards
	}

	/**
	 * Sets the connected cards. This is a bound property.
	 * @param newCards The new cards to be connected.
	 * @throws NullPointerException if the given cards is null.
	 * @see #CARDS_PROPERTY
	 */
	public void setCards(final List newCards) {
		if(!Objects.equals(cards, newCards)) { //if the value is really changing
			final List oldCards = cards; //get the old value
			for(final Component oldCard : oldCards) { //for each old card
				oldCard.removePropertyChangeListener(Component.CONSTRAINTS_PROPERTY, constraintsChangeListener); //stop listening for the component changing constraints
				final Constraints oldCardConstraints = oldCard.getConstraints(); //get the old constraints
				if(oldCardConstraints != null) { //if there were old card constraints
					uninstallCardConstraints(oldCardConstraints); //uninstall the old card constraints
				}
			}
			cards = new CopyOnWriteArrayList(newCards); //actually change the value, making a thread-safe copy of the new list
			for(final Component newCard : newCards) { //for each new card
				newCard.addPropertyChangeListener(Component.CONSTRAINTS_PROPERTY, constraintsChangeListener); //listen for the component changing constraints
				final Constraints newCardConstraints = newCard.getConstraints(); //get the new constraints
				if(newCardConstraints != null) { //if there are new card constraints
					installCardConstraints(newCardConstraints); //install the old card constraints
				}
			}
			final CompositeComponent newParent = !newCards.isEmpty() ? newCards.get(0).getParent() : null; //get the first new card's parent, if there is a new card TODO make sure all parents are the same
			final CardControl newCardControl = newParent instanceof CardControl ? (CardControl)newParent : null; //get the new card control, if any
			setCardControl(newCardControl); //change the card control if needed
			final Component oldCard = !oldCards.isEmpty() ? oldCards.get(0) : null; //get the old card, if any
			final Component newCard = !newCards.isEmpty() ? newCards.get(0) : null; //get the new card, if any
			firePropertyChange(CARDS_PROPERTY, oldCards, newCards); //indicate that the cards value changed
			firePropertyChange(CARD_PROPERTY, oldCard, newCard); //indicate that the card value changed
			updateSelected(); //update the control selection based upon the selected card
			updateDisplayed(); //update the displayed status based upon the selected card
			updateEnabled(); //update the enabled status based upon the selected card
			updateTaskState(); //update the task state based upon the selected card
		}
	}

	/** A convenience reference to the connected card's parent card control, if any. */
	private CardControl cardControl = null;

	/** @return A convenience reference to the connected card's parent card control, if any. */
	protected CardControl getCardControl() {
		return cardControl;
	}

	/**
	 * Sets the convenience reference to the card control.
	 * @param newCardControl The new card control, or null if the card does not have a parent card control.
	 */
	private void setCardControl(final CardControl newCardControl) {
		if(cardControl != newCardControl) { //if the value is really changing
			final CardControl oldCardControl = cardControl; //get the old value
			if(oldCardControl != null) { //if there was an old card control
				oldCardControl.removePropertyChangeListener(CardControl.VALUE_PROPERTY, selectedCardChangeListener); //stop listening for selected card changes in the old card control
			}
			cardControl = newCardControl; //update the card control reference in case it changed				
			if(newCardControl != null) { //if there is a new card control
				newCardControl.addPropertyChangeListener(CardControl.VALUE_PROPERTY, selectedCardChangeListener); //listen for selected card changes in the new card control
			}
		}
	}

	/**
	 * Installs appropriate listeners in a card's constraints.
	 * @param constraints The card constraints being installed.
	 */
	protected void installCardConstraints(final Constraints constraints) {
		if(constraints instanceof Displayable) { //if the new constraints are displayable
			constraints.addPropertyChangeListener(Displayable.DISPLAYED_PROPERTY, displayedChangeListener); //listen for changes in displayed status 
		}
		if(constraints instanceof Enableable) { //if the new constraints are enableable
			constraints.addPropertyChangeListener(Enableable.ENABLED_PROPERTY, enabledChangeListener); //listen for changes in enabled status 
		}
		if(constraints instanceof TaskCardConstraints) { //if the new constraints are task card constraints
			constraints.addPropertyChangeListener(TaskCardConstraints.TASK_STATE_PROPERTY, taskStateChangeListener); //listen for changes in task state 
		}
	}

	/**
	 * Uninstalls appropriate listeners from a card's constraints.
	 * @param constraints The card constraints being uninstalled.
	 */
	protected void uninstallCardConstraints(final Constraints constraints) {
		if(constraints instanceof Displayable) { //if the old constraints were displayable
			constraints.removePropertyChangeListener(Displayable.DISPLAYED_PROPERTY, displayedChangeListener); //stop listening for changes in displayed status
		}
		if(constraints instanceof Enableable) { //if the old constraints were enableable
			constraints.removePropertyChangeListener(Enableable.ENABLED_PROPERTY, enabledChangeListener); //stop listening for changes in enabled status
		}
		if(constraints instanceof TaskCardConstraints) { //if the old constraints were task card constraints
			constraints.removePropertyChangeListener(TaskCardConstraints.TASK_STATE_PROPERTY, taskStateChangeListener); //stop listening for changes in task state
		}
	}

	/**
	 * Card constructor.
	 * @param cards The new cards to connect, if any.
	 */
	public AbstractCardCoupler(final Component... cards) {
		setCards(asList(cards)); //set the cards
	}

	/**
	 * Selects the first connected card that is displayed and enabled. If no card is connected or the card has no parent card control, no action occurs. If a
	 * selectable card is already selected, no action occurs. This method calls {@link #isCardSelectable(Component)} and
	 * {@link #selectCard(CardControl, Component)}.
	 * @throws PropertyVetoException if the appropriate card could not be selected.
	 */
	protected void selectCard() throws PropertyVetoException {
		//TODO del Log.trace("ready to select new card for tab");
		synchronized(this) { //prevent race conditions accessing updatingSelected
			if(!updatingSelected) { //if we're not already updating the selected state (if we are, then this change could be in response to one of our cards being selected, and if we select a new card we could select the wrong one)
				final CardControl cardControl = getCardControl(); //get the connected card card control
				if(cardControl != null) { //there a card control is connected
					final Component selectedCard = cardControl.getValue(); //get the current selected card value
					/*TODO del
					Log.trace("seleted card is index", cardControl.indexOf(selectedCard), "is one of our cards?", cards.contains(selectedCard));
					if(!cards.contains(selectedCard))
					{
						for(final Component card:cards) {	//for each card
							Log.trace("we have card:", cardControl.indexOf(card));
						}
					}
					*/
					if(cards.contains(selectedCard) && isCardSelectable(selectedCard)) { //if a selectable card is already selected (without this check, moving to another tab that changes a card panel that is vetoed and changes back to the original tab, which would try to select the first in a series of cards, would create an endless loop)
						return; //an appropriate card is already selected; take no action
					}
					for(final Component card : cards) { //for each card
						if(isCardSelectable(card)) { //if this card can be selected
							selectCard(cardControl, card); //select the card
							break; //only select the first card
						}
					}
				}
			}
		}
	}

	/**
	 * Determines whether the given card is selectable. This method ensures the card is enabled (if enableable) and displayed (if displayable).
	 * @param card The card to check.
	 * @return true if the card can be selected.
	 */
	protected boolean isCardSelectable(final Component card) {
		//TODO del Log.trace("is card selectable?");
		final Constraints constraints = card.getConstraints(); //get the card constraints, if any
		if(!(constraints instanceof Enableable) || ((Enableable)constraints).isEnabled()) { //if the constraints indicate enabled
			if(!(constraints instanceof Displayable) || ((Displayable)constraints).isDisplayed()) { //if the constraints indicate displayed
				return true; //indicate that the card can be selected
			} else {
				//			TODO del 				Log.trace("card not displayed");
			}
		} else {
			//		TODO del 			Log.trace("card not enabled");
		}
		return false; //if the card doesn't pass the tests, it can't be selected
	}

	/**
	 * Selects the specified card.
	 * @param cardControl The card control to use in selected the card.
	 * @param card The card to select.
	 * @throws PropertyVetoException if the provided card could not be selected.
	 */
	protected void selectCard(final CardControl cardControl, final Component card) throws PropertyVetoException {
		cardControl.setValue(card); //select the card
	}

	/**
	 * Performs any needed updates based upon the displayed status of the constraints of the connected cards. The new displayed status will be considered
	 * true unless the constraints of all connected cards implement {@link Displayable} and return false for
	 * {@link Displayable#isDisplayed()}. This method calls {@link #updateDisplayed(boolean)} with the result.
	 */
	protected void updateDisplayed() {
		boolean displayed = false; //start by assuming the new status is undisplayed
		for(final Component card : cards) { //for each card
			final Constraints constraints = card.getConstraints(); //get the card constraints, if any
			if(!(constraints instanceof Displayable) || ((Displayable)constraints).isDisplayed()) { //if the constraints indicate displayed
				displayed = true; //the new status is displayed
				break; //only one displayed status is needed
			}
		}
		updateDisplayed(displayed); //update the displayed status
	}

	/**
	 * Updates the current displayed status. This implementation does nothing.
	 * @param displayed The new displayed status.
	 */
	protected void updateDisplayed(final boolean displayed) {
	}

	/**
	 * Performs any needed updates based upon the enabled status of the constraints of the connected cards. The new enabled status will be considered
	 * true unless the constraints of all connected cards implement {@link Enableable} and return false for
	 * {@link Enableable#isEnabled()}. This method calls {@link #updateEnabled(boolean)} with the result.
	 */
	protected void updateEnabled() {
		boolean enabled = false; //start by assuming the new status is disabled
		for(final Component card : cards) { //for each card
			final Constraints constraints = card.getConstraints(); //get the card constraints, if any
			if(!(constraints instanceof Enableable) || ((Enableable)constraints).isEnabled()) { //if the constraints indicate enabled
				enabled = true; //the new status is enabled
				break; //only one enabled status is needed
			}
		}
		updateEnabled(enabled); //update the enabled status
	}

	/**
	 * Updates the current enabled status. This implementation does nothing.
	 * @param enabled The new enabled status.
	 */
	protected void updateEnabled(final boolean enabled) {
	}

	/**
	 * Performs any needed updates based upon the current task status of the constraints of the connected cards. This implementation uses the first available task
	 * status from the connected cards. This method calls {@link #updateTaskState(TaskState)}.
	 */
	protected void updateTaskState() {
		TaskState taskState = null;
		for(final Component card : cards) { //for each card
			final Constraints constraints = card.getConstraints(); //get the card constraints, if any
			if(constraints instanceof TaskCardConstraints) { //if the constraints indicate task state
				taskState = ((TaskCardConstraints)constraints).getTaskState(); //get the task state from the constraints
				break; //use the first available task state
			}
		}
		updateTaskState(taskState); //update the task state
	}

	/**
	 * Updates the current task state. This implementation does nothing.
	 * @param taskState The new task state, or null if there is no task state.
	 */
	protected void updateTaskState(final TaskState taskState) {
	}

	/**
	 * Performs any needed updates based upon the currently selected card. This implementation determines the selected status based upon whether the currently
	 * selected card in the connected card control is one of the connected cards. This method calls {@link #updateSelected(boolean)}.
	 */
	protected void updateSelected() {
		synchronized(this) { //prevent race conditions accessing updatingSelected
			if(!updatingSelected) { //if we're not already updating the selected state
				updatingSelected = true; //show that we're updating our selected state
				try {
					boolean selected = false; //start by assuming no connected card is selected
					final CardControl cardControl = getCardControl(); //get the card control
					if(cardControl != null) { //there is a parent card control
						final Component selectedCard = cardControl.getSelectedValue(); //get the selected card
						for(final Component card : cards) { //for each card
							if(card == selectedCard) { //if this connected cards is selected
								selected = true; //show that a connected card is selected
								break; //it only takes one selected card do set the selected status
							}
						}
					}
					updateSelected(selected); //update the selected status
				} finally {
					updatingSelected = false; //always show that we are done updating the selected state
				}
			}
		}
	}

	/**
	 * Updates the current selected status. This implementation does nothing.
	 * @param selected The new selected status.
	 */
	protected void updateSelected(final boolean selected) {
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy