PageRenderTime 47ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/src/com/mintdigital/hemlock/controls/HemlockScrollBar.as

http://github.com/mintdigital/hemlock
ActionScript | 406 lines | 294 code | 64 blank | 48 comment | 37 complexity | 7f5d335ead57ebd605d164bfc05c923d MD5 | raw file
  1. package com.mintdigital.hemlock.controls{
  2. import com.mintdigital.hemlock.HemlockEnvironment;
  3. import com.mintdigital.hemlock.Logger;
  4. import com.mintdigital.hemlock.utils.HashUtils;
  5. import flash.display.DisplayObject;
  6. import flash.display.DisplayObjectContainer;
  7. import flash.display.Sprite;
  8. import flash.events.Event;
  9. import flash.events.MouseEvent;
  10. import flash.geom.Rectangle;
  11. import flash.text.TextField;
  12. public class HemlockScrollBar extends HemlockControl{
  13. public static const HORIZONTAL:String = 'h';
  14. public static const VERTICAL:String = 'v';
  15. public static const NAME_SUFFIX:String = '_scrollbar';
  16. public var views:Object = {};
  17. private var _content:DisplayObject;
  18. private var _contentSize:Number;
  19. private var positionProperty:String; // 'y' if vertical, 'x' if horizontal
  20. private var sizeProperty:String; // 'height' if vertical, 'width' if horizontal
  21. private var contentIsTextField:Boolean;
  22. private var _percentage:Number = 0.0;
  23. private var isVertical:Boolean;
  24. protected static var _defaultOptions:Object = {
  25. direction: VERTICAL,
  26. thickness: skin.SCROLL_BAR_THICKNESS || 20,
  27. // Width if vertical, height if horizontal
  28. minThumbSize: skin.SCROLL_BAR_LENGTH || 15,
  29. // Height if vertical, width if horizontal
  30. colors: {
  31. bg: 0xCCCCCC, // Set null to be transparent
  32. thumb: 0xEEEEEE
  33. }
  34. };
  35. public function HemlockScrollBar(content:DisplayObject, options:Object = null){
  36. _content = content;
  37. contentIsTextField = (_content is TextField);
  38. options = HashUtils.merge(_defaultOptions, options);
  39. switch(options.direction){
  40. case VERTICAL:
  41. options.width = options.thickness;
  42. options.height = _content.height;
  43. isVertical = true;
  44. positionProperty= 'y';
  45. sizeProperty = 'height';
  46. break;
  47. case HORIZONTAL:
  48. options.width = _content.width;
  49. options.height = options.thickness;
  50. isVertical = false;
  51. positionProperty= 'x';
  52. sizeProperty = 'width';
  53. break;
  54. default:
  55. Logger.fatal('HemlockScrollBar::HemlockScrollBar() : Invalid direction: ' + options.direction);
  56. break;
  57. }
  58. if(!options.name){
  59. options.name = content.name + NAME_SUFFIX;
  60. }
  61. super(options.name, '', options);
  62. createViews();
  63. registerListeners();
  64. startListeners();
  65. }
  66. public function createViews():void{
  67. // http://www.actionscript.org/forums/archive/index.php3/t-88342.html
  68. if(!contentIsTextField){
  69. // Crop content
  70. content.scrollRect = new Rectangle(0, 0, content.width, content.height);
  71. // Assumes that content currently has intended dimensions
  72. // TODO: Accept options contentWidth and contentHeight
  73. // - Use these options when setting scrollbar dimensions
  74. }
  75. if(isVertical){
  76. var sizes:Object = {
  77. bg: { width: options.width, height: options.height },
  78. thumb: { width: options.width, height: 50 }
  79. };
  80. var useCustomColors:Boolean = (options.colors != defaultOptions.colors);
  81. // Create background
  82. if(skin.ScrollBarVerticalBG && !useCustomColors){
  83. views.bg = new skin.ScrollBarVerticalBG();
  84. sizes.bg.width = skin.SCROLL_BAR_THICKNESS;
  85. }else{
  86. // TODO: Create rounded ends
  87. // - Update setSize() and updateThumbSize() also
  88. views.bg = new Sprite();
  89. with(views.bg.graphics){
  90. if(options.colors && options.colors.bg){
  91. beginFill(options.colors.bg);
  92. }else{
  93. beginFill(0, 0); // Prop open
  94. }
  95. drawRect(0, 0, sizes.bg.width, sizes.bg.height);
  96. endFill();
  97. }
  98. }
  99. views.bg.width = sizes.bg.width;
  100. views.bg.height = sizes.bg.height;
  101. // Create scroll thumb
  102. if(skin.ScrollBarVerticalThumb && !useCustomColors){
  103. views.thumb = new skin.ScrollBarVerticalThumb();
  104. sizes.thumb.width = skin.SCROLL_BAR_THICKNESS;
  105. }else{
  106. // TODO: Create rounded ends
  107. // - Update updateThumbSize() also
  108. views.thumb = new Sprite();
  109. with(views.thumb.graphics){
  110. if(options.colors && options.colors.thumb){
  111. beginFill(options.colors.thumb);
  112. }else{
  113. beginFill(0, 0); // Prop open
  114. }
  115. drawRect(0, 0, sizes.thumb.width, sizes.thumb.height);
  116. endFill();
  117. }
  118. }
  119. views.thumb.width = sizes.thumb.width;
  120. views.thumb.height = sizes.thumb.height;
  121. // Create scroll thumb grip
  122. if(skin.ScrollBarVerticalThumbGrip && !useCustomColors){
  123. // TODO: Use updateThumbGrip() instead?
  124. views.thumbGrip = new skin.ScrollBarVerticalThumbGrip();
  125. sizes.thumbGrip = {
  126. width: skin.SCROLL_BAR_THUMB_GRIP_THICKNESS,
  127. height: skin.SCROLL_BAR_THUMB_GRIP_LENGTH
  128. };
  129. // views.thumb.addChild(views.thumbGrip);
  130. with(views.thumbGrip){
  131. x = (sizes.thumb.width - sizes.thumbGrip.width) * 0.5;
  132. y = (sizes.thumb.height - sizes.thumbGrip.height) * 0.5;
  133. width = sizes.thumbGrip.width;
  134. height = sizes.thumbGrip.height;
  135. }
  136. }
  137. }else{
  138. // Create background
  139. // FIXME: Implement
  140. // Create scroll thumb
  141. // FIXME: Implement
  142. }
  143. // Wrap up
  144. addChild(views.bg);
  145. addChild(views.thumb);
  146. if(views.thumbGrip){ addChild(views.thumbGrip); }
  147. setSize(options.width, options.height);
  148. }
  149. override public function registerListeners():void{
  150. registerListener(this, Event.ADDED_TO_STAGE, onAddToStage);
  151. registerListener(this, Event.REMOVED_FROM_STAGE, onRemoveFromStage);
  152. registerListener(views.thumb, MouseEvent.MOUSE_DOWN, onScrollThumbMouseDown);
  153. registerListener(this, MouseEvent.MOUSE_WHEEL, onMouseWheel);
  154. registerListener(content, MouseEvent.MOUSE_WHEEL, onMouseWheel);
  155. registerListener(views.bg, MouseEvent.CLICK, onBGClick);
  156. registerListener(content, Event.CHANGE, onContentChange);
  157. }
  158. override public function setSize(width:Number, height:Number):void{
  159. views.bg.width = width;
  160. views.bg.height = height;
  161. super.setSize(width, height);
  162. updateThumbSize();
  163. }
  164. //--------------------------------------
  165. // Event handlers
  166. //--------------------------------------
  167. private function onAddToStage(event:Event):void{
  168. registerListener(stage, MouseEvent.MOUSE_UP, onScrollThumbMouseUp);
  169. startListeners();
  170. }
  171. private function onRemoveFromStage(event:Event):void{
  172. stopListeners();
  173. }
  174. private function onScrollThumbMouseDown(event:MouseEvent):void{
  175. stage.addEventListener(MouseEvent.MOUSE_MOVE, onScrollThumbMouseMove);
  176. // This should presumably only run after the scrollbar has
  177. // added to the stage.
  178. var bounds:Rectangle = isVertical
  179. ? new Rectangle(0, 0, 0, height - views.thumb.height)
  180. : new Rectangle(0, 0, width - views.thumb.width, 0);
  181. views.thumb.startDrag(false, bounds);
  182. }
  183. private function onScrollThumbMouseMove(event:MouseEvent):void{
  184. event.updateAfterEvent();
  185. scrollContent(
  186. views.thumb[positionProperty]
  187. / (this[sizeProperty] - views.thumb[sizeProperty])
  188. );
  189. updateThumbGrip();
  190. }
  191. private function onScrollThumbMouseUp(event:MouseEvent):void{
  192. stage.removeEventListener(MouseEvent.MOUSE_MOVE, onScrollThumbMouseMove);
  193. views.thumb.stopDrag();
  194. }
  195. private function onMouseWheel(event:MouseEvent):void{
  196. var newPercentage:Number,
  197. deltaPixels:Number = event.delta * 2;
  198. // Higher multiplier => more sensitive to scrolling
  199. if(contentIsTextField){
  200. newPercentage =
  201. (views.thumb[positionProperty] - deltaPixels)
  202. / (this[sizeProperty] - views.thumb[sizeProperty]);
  203. }else{
  204. newPercentage =
  205. ((content.scrollRect[sizeProperty] * percentage) - deltaPixels)
  206. / content.scrollRect[sizeProperty];
  207. }
  208. scrollContent(newPercentage);
  209. updateThumbPosition();
  210. }
  211. private function onBGClick(event:MouseEvent):void{
  212. // Scrolls content by one page.
  213. var newPercentage:Number = percentage,
  214. pagePercentage:Number,
  215. clickCoord:Number = event[isVertical ? 'localY' : 'localX'];
  216. if(contentIsTextField){
  217. var textField:TextField = content as TextField;
  218. pagePercentage =
  219. textField[sizeProperty]
  220. / textField[isVertical ? 'textHeight' : 'textWidth'];
  221. }else{
  222. pagePercentage = content.scrollRect[sizeProperty] / contentSize;
  223. }
  224. if(clickCoord < views.thumb[positionProperty]){
  225. newPercentage = percentage - pagePercentage;
  226. }else if(clickCoord > views.thumb[positionProperty] + views.thumb[sizeProperty]){
  227. newPercentage = percentage + pagePercentage;
  228. }
  229. scrollContent(newPercentage);
  230. updateThumbPosition();
  231. }
  232. private function onContentChange(event:Event):void{
  233. _contentSize = -1;
  234. updateThumbSize();
  235. }
  236. //--------------------------------------
  237. // Internal helpers
  238. //--------------------------------------
  239. private function updateThumbSize():void{
  240. var thumbSize:Number = 0;
  241. if(contentIsTextField){
  242. thumbSize =
  243. this[sizeProperty] * contentSize
  244. / (content as TextField)[isVertical ? 'textHeight' : 'textWidth'];
  245. }else{
  246. thumbSize = this[sizeProperty] * content.scrollRect[sizeProperty] / contentSize;
  247. }
  248. visible = (thumbSize < contentSize);
  249. startListeners();
  250. // HemlockSprite listeners are stopped if not visible, but the
  251. // scrollbar's listeners should stay enabled to listen for
  252. // content events, e.g., Event.CHANGE.
  253. if(visible){
  254. if(thumbSize > contentSize) { thumbSize = contentSize; }
  255. if(thumbSize < options.minThumbSize) { thumbSize = options.minThumbSize; }
  256. views.thumb[sizeProperty] = thumbSize;
  257. updateThumbPosition();
  258. }
  259. }
  260. private function updateThumbPosition():void{
  261. var thumbPosition:Number;
  262. if(contentIsTextField){
  263. var textField:TextField = content as TextField;
  264. if(isVertical){
  265. if(textField.scrollV + 1 == textField.maxScrollV){
  266. percentage = 1;
  267. }else{
  268. percentage = (textField.scrollV - 1) / (textField.maxScrollV - 1);
  269. }
  270. }else{
  271. percentage = (textField.scrollH - 1) / (textField.maxScrollH - 1);
  272. }
  273. }
  274. thumbPosition = (content[sizeProperty] - views.thumb[sizeProperty]) * percentage;
  275. if(thumbPosition + views.thumb[sizeProperty] > content[sizeProperty]){
  276. views.thumb[sizeProperty] = content[sizeProperty] - thumbPosition;
  277. }
  278. views.thumb[positionProperty] = thumbPosition;
  279. updateThumbGrip();
  280. }
  281. private function updateThumbGrip():void{
  282. // Updates the thumb grip's size and position.
  283. if(!views.thumbGrip){ return; }
  284. views.thumbGrip.x = views.thumb.x + ((views.thumb.width - views.thumbGrip.width) * 0.5);
  285. views.thumbGrip.y = views.thumb.y + ((views.thumb.height - views.thumbGrip.height) * 0.5);
  286. views.thumbGrip[isVertical ? 'width' : 'height'] = skin.SCROLL_BAR_THUMB_GRIP_THICKNESS;
  287. views.thumbGrip[isVertical ? 'height' : 'width'] = skin.SCROLL_BAR_THUMB_GRIP_LENGTH;
  288. }
  289. private function scrollContent(newPercentage:Number):void{
  290. // `newPercentage` is the target percentage distance of the scroll
  291. // thumb from the top/left of the scrollbar. This should be
  292. // between 0.0 and 1.0 inclusive.
  293. percentage = newPercentage;
  294. if(contentIsTextField){
  295. var textField:TextField = content as TextField;
  296. if(isVertical){
  297. textField.scrollV = Math.round(textField.maxScrollV * percentage);
  298. }else{
  299. textField.scrollH = Math.round(textField.maxScrollH * percentage);
  300. }
  301. }else{
  302. var rect:Rectangle = content.scrollRect;
  303. rect[positionProperty] = (contentSize - content.scrollRect[sizeProperty]) * percentage;
  304. content.scrollRect = rect;
  305. }
  306. }
  307. //--------------------------------------
  308. // Properties
  309. //--------------------------------------
  310. public function get content():DisplayObject { return _content; }
  311. public function set content(value:DisplayObject):void { _content = value; }
  312. public function get contentSize():Number{
  313. // Returns the current height (if vertical) or width (if
  314. // horizontal) of `content`, depending on what type of
  315. // DisplayObject it is.
  316. if(!_contentSize || _contentSize < 0){
  317. if(content is DisplayObjectContainer){
  318. // Get distance to end of farthest child
  319. var doc:DisplayObjectContainer = (content as DisplayObjectContainer);
  320. for(var i:uint = 0, max:uint = doc.numChildren; i < max; i++){
  321. var child:DisplayObject = doc.getChildAt(i);
  322. _contentSize = Math.max(_contentSize, child[positionProperty] + child[sizeProperty]);
  323. }
  324. }else{
  325. // Get natural width/height
  326. _contentSize = content[sizeProperty];
  327. }
  328. }
  329. return Number(_contentSize);
  330. }
  331. public function get percentage():Number{
  332. // Current percentage distance of scroll thumb from top/left.
  333. // 0.0 to 1.0 inclusive.
  334. return _percentage;
  335. }
  336. public function set percentage(value:Number):void{
  337. _percentage = Number(isNaN(value) ? 0 : value < 0 ? 0 : value > 1 ? 1 : value);
  338. }
  339. public static function get defaultOptions():Object { return _defaultOptions; }
  340. public static function set defaultOptions(value:Object):void { _defaultOptions = value; }
  341. }
  342. }