blob: 7607661a1c699953542ad01d10f761b2b40945a4 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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
{
import flash.display.Sprite;
import flash.events.KeyboardEvent;
import flash.ui.Keyboard;
import flashx.textLayout.compose.TextFlowLine;
import flashx.textLayout.container.ContainerController;
import flashx.textLayout.container.ScrollPolicy;
import flashx.textLayout.edit.EditManager;
import flashx.textLayout.edit.SelectionManager;
import flashx.textLayout.elements.TextFlow;
import flashx.textLayout.events.CompositionCompleteEvent;
import flashx.textLayout.events.SelectionEvent;
import flashx.textLayout.events.StatusChangeEvent;
import flashx.textLayout.formats.TextLayoutFormat;
public class PaginationWidget extends Sprite
{
// width and height this widget should use
private var _width:int;
private var _height:int;
// current textflow, the list of pages and current page and display position
private var _textFlow:TextFlow;
private var _pageList:Array;
private var _curPage:int;
private var _curPosition:int; // really the first visible character - during resize keep this in view
// some configuration values - ContainerFormat for all containers and constraints on container width
private var _containerFormat:TextLayoutFormat;
private const _minContainerWidth:int = 100;
private const _maxContainerWidth:int = 10000;
// derived values - based on width/height compute these values
private var _containerHeight:int;
private var _containerWidth:int;
private var _containersToShow:int;
private var _containerMargin:Number;
public function PaginationWidget()
{
_curPage = -1;
_curPosition = 0;
_pageList = new Array();
// all containers formatted this way
_containerFormat = new TextLayoutFormat();
_containerFormat.columnCount = 1;
_containerFormat.paddingTop = 10;
_containerFormat.paddingBottom = 10;
_containerFormat.paddingLeft = 10;
_containerFormat.paddingRight = 10;
_containersToShow = 0;
this.focusRect = false;
}
/** Sets a new width and height into the widget.
* Uses simple heuristics to decide how big the containers are and how many are visible.
* Don't resize the containers on every size change - instead wait for a larger change
*/
public function setSize(w:int,h:int):void
{
if (w == _width && h == _height)
return;
_width = w;
_height = h;
var newContainerMargin:int = 25;
// width <= 250 one column
// width <= 500 two columns
// width <= 1000 three columns
// width > 1000 four colunmns
var newContainersToShow:int = 0;
if (_width <= 300)
newContainersToShow = 1;
else if (_width <= 550)
newContainersToShow = 2;
else if (_width <= 1050)
newContainersToShow = 3;
else
newContainersToShow = 4;
var newContainerHeight:int = _height;
var newContainerWidth:int = Math.max((_width-2*newContainerMargin)/newContainersToShow,_minContainerWidth);
// only change if things go out of view or height changes by more than one line - call it 12
// this is a heuristic that can be easily refined. the goal is to not reflow the text every time things change just a little to give much smoother performance
if (newContainersToShow != _containersToShow || Math.abs(_containerWidth-newContainerWidth)>36 || Math.abs(newContainerHeight-_containerHeight) > 12 || (_containerMargin + _containerWidth * _containersToShow) > _width)
{
_containerWidth = newContainerWidth;
_containerHeight = newContainerHeight;
_containersToShow = newContainersToShow;
_containerMargin = newContainerMargin;
if (_textFlow)
{
recomputeContainers();
goToCurrentPosition(true);
}
}
else
{
// decided not to recompose but lets redo the margins so things look nice
newContainerMargin = Math.max((_width - _containersToShow * _containerWidth) / 2.0,0);
if (newContainerMargin != _containerMargin)
{
var savePage:int = _curPage;
_containerMargin = newContainerMargin;
goToPage(-1,false);
goToPage(savePage,false);
}
}
}
private var inRecomputeContainers:Boolean = false;
/** The worker function. Reflows based on the parameters computed in setSize */
private function recomputeContainers():void
{
var idx:int; // scratch
inRecomputeContainers = true;
// clear list of pages
_pageList.splice(0);
// resize existing containers
for (idx = 0; idx < _textFlow.flowComposer.numControllers; idx++)
{
_textFlow.flowComposer.getControllerAt(idx).setCompositionSize(_containerWidth,_containerHeight);
}
var controller:ContainerController;
for (;;)
{
// compose the current chain of continers
if (_textFlow.flowComposer.numControllers)
{
_textFlow.flowComposer.compose();
// add just the containers with content to pageList. Stop at first empty container or when all text is placed
while (_pageList.length < _textFlow.flowComposer.numControllers)
{
controller = _textFlow.flowComposer.getControllerAt(_pageList.length);
_pageList.push(Sprite(controller.container));
if (controller.textLength == 0 || controller.absoluteStart + controller.textLength >= _textFlow.textLength)
{
// all the text has fit into the containers. now display the textlines and done
_textFlow.flowComposer.updateAllControllers();
inRecomputeContainers = false;
return;
}
}
}
// create new containers in batches - 10 at a time
for (idx = 0; idx < 10; idx++)
{
controller = new MyDisplayObjectContainerController(new Sprite(),_containerWidth,_containerHeight, this);
controller.horizontalScrollPolicy = ScrollPolicy.OFF;
controller.verticalScrollPolicy = ScrollPolicy.OFF;
controller.format = _containerFormat;
_textFlow.flowComposer.addController(controller);
}
}
}
/** The TextFlow to display */
public function get textFlow():TextFlow
{ return _textFlow; }
public function set textFlow(newFlow:TextFlow):void
{
// clear any old flow if present
if (_textFlow)
{
_textFlow.interactionManager = null;
goToPage(-1, false);
_textFlow.flowComposer.removeAllControllers();
_textFlow.removeEventListener(StatusChangeEvent.INLINE_GRAPHIC_STATUS_CHANGE,graphicStatusChangeEvent);
_textFlow.removeEventListener(SelectionEvent.SELECTION_CHANGE,selectionChangeEvent);
_textFlow.removeEventListener(CompositionCompleteEvent.COMPOSITION_COMPLETE,compositionDoneEvent);
_textFlow = null;
}
_textFlow = newFlow;
if (_textFlow)
{
// Disable the interactionManager
// _textFlow.interactionManager = new EditManager();
// _textFlow.interactionManager.selectRange(0,0);
// setup event listener ILG loaded
_textFlow.addEventListener(StatusChangeEvent.INLINE_GRAPHIC_STATUS_CHANGE,graphicStatusChangeEvent);
_textFlow.addEventListener(SelectionEvent.SELECTION_CHANGE,selectionChangeEvent);
_textFlow.addEventListener(CompositionCompleteEvent.COMPOSITION_COMPLETE,compositionDoneEvent);
_textFlow.interactionManager = new SelectionManager();
recomputeContainers();
goToPage(0);
}
}
/** Receives an event any time an ILG with a computed size finishes loading. */
private function graphicStatusChangeEvent(evt:StatusChangeEvent):void
{
// recompose if the evt is from an element in this textFlow
if (_textFlow && evt.element.getTextFlow() == _textFlow)
{
recomputeContainers();
goToCurrentPosition();
}
}
private function selectionChangeEvent(e:SelectionEvent):void
{
goToCurrentPosition();
}
private function compositionDoneEvent(evt:CompositionCompleteEvent):void
{
if (inRecomputeContainers)
return;
// is the entire flow in a container
var lastLine:TextFlowLine = _textFlow.flowComposer.getLineAt(_textFlow.flowComposer.numLines-1);
if (lastLine.controller == null || _textFlow.flowComposer.findControllerIndexAtPosition(lastLine.absoluteStart) != _pageList.length-1)
{
recomputeContainers();
goToCurrentPosition();
}
}
/** Go to the first page of the current textFlow. */
public function firstPage():void
{
if (_curPage != -1 &&_pageList.length)
goToPage(0);
}
/** Go to the last page of the current textFlow. */
public function lastPage():void
{
if (_curPage != -1 &&_pageList.length)
goToPage(_pageList.length-1);
}
/** Go to the next page of the current textFlow. */
public function nextPage():void
{
if (_curPage != -1)
goToPage(_curPage+_containersToShow);
}
/** Go to the previous page of the current textFlow. */
public function prevPage():void
{
if (_curPage != -1)
goToPage(Math.max(0,_curPage-_containersToShow));
}
private function goToCurrentPosition(alwaysgo:Boolean = false):void
{
var activePosition:int = _textFlow.interactionManager ? _textFlow.interactionManager.activePosition : _curPosition;
var pageToShow:int = _textFlow.flowComposer.findControllerIndexAtPosition(activePosition,activePosition == _textFlow.textLength);
pageToShow = Math.max(0,Math.min(pageToShow,_pageList.length-_containersToShow));
// if its already visible do nothing
if (alwaysgo || _curPage == -1 || _curPage > pageToShow || _curPage+_containersToShow <= pageToShow)
{
goToPage(-1,false);
goToPage(pageToShow,false);
if (_textFlow.interactionManager)
_textFlow.interactionManager.refreshSelection();
}
}
/** Go to a specific page.
* @param pageNum - page to go to
* @param updateCurPosition - remember first character so that on resize that character stays in view.
*/
public function goToPage(pageNum:int,updateCurPosition:Boolean = true):void
{
if (pageNum >= _pageList.length)
pageNum = _pageList.length-1;
if (pageNum != _curPage)
{
while (numChildren)
removeChildAt(0);
_curPage = pageNum;
if (_curPage != -1)
{
// now add in the correct number of pages
var pageAfter:int = Math.min(_pageList.length,_curPage+this._containersToShow);
var xpos:Number = this._containerMargin;
for (var idx:int = _curPage; idx < pageAfter; idx++)
{
var pageToShow:Sprite = _pageList[idx];
pageToShow.x = xpos;
addChild(pageToShow);
xpos += _containerWidth;
}
}
}
// focus on the first page
this.stage.focus = _curPage == -1 ? null : _pageList[_curPage];
if (updateCurPosition)
_curPosition = _curPage == -1 ? 0 : _textFlow.flowComposer.getControllerAt(_curPage).absoluteStart;
}
/** KeyDown helper function for keyboard navigation.
* @returns true --> keyboard event handled here. */
public function processKeyDownEvent(e:KeyboardEvent):Boolean
{
if (e.charCode == 0 && !e.shiftKey)
{
// the keycodes for navigating within a TextFlow
switch(e.keyCode)
{
case Keyboard.LEFT:
case Keyboard.UP:
case Keyboard.PAGE_UP:
prevPage();
return true;
case Keyboard.RIGHT:
case Keyboard.DOWN:
case Keyboard.PAGE_DOWN:
nextPage();
return true;
case Keyboard.HOME:
firstPage();
return true;
case Keyboard.END:
lastPage();
return true;
}
}
return false;
}
}
}
import flash.display.Sprite;
import flash.events.KeyboardEvent;
import flashx.textLayout.container.ContainerController;
/** overrides processKeyDownEvent to add keyboard navigation */
class MyDisplayObjectContainerController extends ContainerController
{
private var _widget:PaginationWidget;
public function MyDisplayObjectContainerController(cont:Sprite,compositionWidth:Number,compositionHeight:Number,widget:PaginationWidget)
{
super(cont,compositionWidth,compositionHeight);
_widget = widget;
}
public override function keyDownHandler(e:KeyboardEvent):void
{
if (_widget.processKeyDownEvent(e))
{
e.preventDefault();
return;
}
super.keyDownHandler(e);
}
}