| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // 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); |
| } |
| } |