You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Escalator.java 156KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993
  1. /*
  2. * Copyright 2000-2013 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client.ui.grid;
  17. import java.util.ArrayList;
  18. import java.util.Collections;
  19. import java.util.HashMap;
  20. import java.util.LinkedList;
  21. import java.util.List;
  22. import java.util.ListIterator;
  23. import java.util.Map;
  24. import java.util.logging.Level;
  25. import java.util.logging.Logger;
  26. import com.google.gwt.animation.client.AnimationScheduler;
  27. import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
  28. import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
  29. import com.google.gwt.core.client.Duration;
  30. import com.google.gwt.core.client.JavaScriptObject;
  31. import com.google.gwt.dom.client.Document;
  32. import com.google.gwt.dom.client.Element;
  33. import com.google.gwt.dom.client.NativeEvent;
  34. import com.google.gwt.dom.client.Node;
  35. import com.google.gwt.dom.client.NodeList;
  36. import com.google.gwt.dom.client.Style;
  37. import com.google.gwt.dom.client.Style.Display;
  38. import com.google.gwt.dom.client.Style.Unit;
  39. import com.google.gwt.event.shared.HandlerRegistration;
  40. import com.google.gwt.logging.client.LogConfiguration;
  41. import com.google.gwt.user.client.DOM;
  42. import com.google.gwt.user.client.Window;
  43. import com.google.gwt.user.client.ui.UIObject;
  44. import com.google.gwt.user.client.ui.Widget;
  45. import com.vaadin.client.Profiler;
  46. import com.vaadin.client.Util;
  47. import com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle;
  48. import com.vaadin.client.ui.grid.PositionFunction.AbsolutePosition;
  49. import com.vaadin.client.ui.grid.PositionFunction.Translate3DPosition;
  50. import com.vaadin.client.ui.grid.PositionFunction.TranslatePosition;
  51. import com.vaadin.client.ui.grid.PositionFunction.WebkitTranslate3DPosition;
  52. import com.vaadin.client.ui.grid.ScrollbarBundle.HorizontalScrollbarBundle;
  53. import com.vaadin.client.ui.grid.ScrollbarBundle.VerticalScrollbarBundle;
  54. import com.vaadin.shared.ui.grid.Range;
  55. import com.vaadin.shared.ui.grid.ScrollDestination;
  56. import com.vaadin.shared.util.SharedUtil;
  57. /*-
  58. Maintenance Notes! Reading these might save your day.
  59. == Row Container Structure
  60. AbstractRowContainer
  61. |-- AbstractStaticRowContainer
  62. | |-- HeaderRowContainer
  63. | `-- FooterContainer
  64. `-- BodyRowContainer
  65. AbstractRowContainer is intended to contain all common logic
  66. between RowContainers. It manages the bookkeeping of row
  67. count, makes sure that all individual cells are rendered
  68. the same way, and so on.
  69. AbstractStaticRowContainer has some special logic that is
  70. required by all RowContainers that don't scroll (hence the
  71. word "static"). HeaderRowContainer and FooterRowContainer
  72. are pretty thin special cases of a StaticRowContainer
  73. (mostly relating to positioning of the root element).
  74. BodyRowContainer could also be split into an additional
  75. "AbstractScrollingRowContainer", but I felt that no more
  76. inner classes were needed. So it contains both logic
  77. required for making things scroll about, and equivalent
  78. special cases for layouting, as are found in
  79. Header/FooterRowContainers.
  80. == The Three Indices
  81. Each RowContainer can be thought to have three levels of
  82. indices for any given displayed row (but the distinction
  83. matters primarily for the BodyRowContainer, because of the
  84. way it scrolls through data):
  85. - Logical index
  86. - Physical (or DOM) index
  87. - Visual index
  88. LOGICAL INDEX is the index that is linked to the data
  89. source. If you want your data source to represent a SQL
  90. database with 10 000 rows, the 7 000:th row in the SQL has a
  91. logical index of 6 999, since the index is 0-based (unless
  92. that data source does some funky logic).
  93. PHYSICAL INDEX is the index for a row that you see in a
  94. browser's DOM inspector. If your row is the second <tr>
  95. element within a <tbody> tag, it has a physical index of 1
  96. (because of 0-based indices). In Header and
  97. FooterRowContainers, you are safe to assume that the logical
  98. index is the same as the physical index. But because the
  99. BodyRowContainer never displays large data sources entirely
  100. in the DOM, a physical index usually has no apparent direct
  101. relationship with its logical index.
  102. VISUAL INDEX is the index relating to the order that you
  103. see a row in, in the browser, as it is rendered. The
  104. topmost row is 0, the second is 1, and so on. The visual
  105. index is similar to the physical index in the sense that
  106. Header and FooterRowContainers can assume a 1:1
  107. relationship between visual index and logical index. And
  108. again, BodyRowContainer has no such relationship. The
  109. body's visual index has additionally no apparent
  110. relationship with its physical index. Because the <tr> tags
  111. are reused in the body and visually repositioned with CSS
  112. as the user scrolls, the relationship between physical
  113. index and visual index is quickly broken. You can get an
  114. element's visual index via the field
  115. BodyRowContainer.visualRowOrder.
  116. */
  117. /**
  118. * A workaround-class for GWT and JSNI.
  119. * <p>
  120. * GWT is unable to handle some method calls to Java methods in inner-classes
  121. * from within JSNI blocks. Having that inner class implement a non-inner-class
  122. * (or interface), makes it possible for JSNI to indirectly refer to the inner
  123. * class, by invoking methods and fields in the non-inner-class.
  124. *
  125. * @see Escalator.Scroller
  126. */
  127. abstract class JsniWorkaround {
  128. /**
  129. * A JavaScript function that handles the scroll DOM event, and passes it on
  130. * to Java code.
  131. *
  132. * @see #createScrollListenerFunction(Escalator)
  133. * @see Escalator#onScroll(double,double)
  134. * @see Escalator.Scroller#onScroll(double, double)
  135. */
  136. protected final JavaScriptObject scrollListenerFunction;
  137. /**
  138. * A JavaScript function that handles the mousewheel DOM event, and passes
  139. * it on to Java code.
  140. *
  141. * @see #createMousewheelListenerFunction(Escalator)
  142. * @see Escalator#onScroll(double,double)
  143. * @see Escalator.Scroller#onScroll(double, double)
  144. */
  145. protected final JavaScriptObject mousewheelListenerFunction;
  146. /**
  147. * A JavaScript function that handles the touch start DOM event, and passes
  148. * it on to Java code.
  149. *
  150. * @see TouchHandlerBundle#touchStart(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
  151. */
  152. protected JavaScriptObject touchStartFunction;
  153. /**
  154. * A JavaScript function that handles the touch move DOM event, and passes
  155. * it on to Java code.
  156. *
  157. * @see TouchHandlerBundle#touchMove(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
  158. */
  159. protected JavaScriptObject touchMoveFunction;
  160. /**
  161. * A JavaScript function that handles the touch end and cancel DOM events,
  162. * and passes them on to Java code.
  163. *
  164. * @see TouchHandlerBundle#touchEnd(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
  165. */
  166. protected JavaScriptObject touchEndFunction;
  167. protected JsniWorkaround(final Escalator escalator) {
  168. scrollListenerFunction = createScrollListenerFunction(escalator);
  169. mousewheelListenerFunction = createMousewheelListenerFunction(escalator);
  170. final TouchHandlerBundle bundle = new TouchHandlerBundle(escalator);
  171. touchStartFunction = bundle.getTouchStartHandler();
  172. touchMoveFunction = bundle.getTouchMoveHandler();
  173. touchEndFunction = bundle.getTouchEndHandler();
  174. }
  175. /**
  176. * A method that constructs the JavaScript function that will be stored into
  177. * {@link #scrollListenerFunction}.
  178. *
  179. * @param esc
  180. * a reference to the current instance of {@link Escalator}
  181. * @see Escalator#onScroll(double,double)
  182. */
  183. protected abstract JavaScriptObject createScrollListenerFunction(
  184. Escalator esc);
  185. /**
  186. * A method that constructs the JavaScript function that will be stored into
  187. * {@link #mousewheelListenerFunction}.
  188. *
  189. * @param esc
  190. * a reference to the current instance of {@link Escalator}
  191. * @see Escalator#onScroll(double,double)
  192. */
  193. protected abstract JavaScriptObject createMousewheelListenerFunction(
  194. Escalator esc);
  195. }
  196. /**
  197. * A low-level table-like widget that features a scrolling virtual viewport and
  198. * lazily generated rows.
  199. *
  200. * @since 7.2
  201. * @author Vaadin Ltd
  202. */
  203. public class Escalator extends Widget {
  204. // todo comments legend
  205. /*
  206. * [[optimize]]: There's an opportunity to rewrite the code in such a way
  207. * that it _might_ perform better (rememeber to measure, implement,
  208. * re-measure)
  209. */
  210. /*
  211. * [[rowheight]]: This code will require alterations that are relevant for
  212. * being able to support variable row heights. NOTE: these bits can most
  213. * often also be identified by searching for code reading the ROW_HEIGHT_PX
  214. * constant.
  215. */
  216. /*
  217. * [[API]]: Implementing this suggestion would require a change in the
  218. * public API. These suggestions usually don't come lightly.
  219. */
  220. /*
  221. * [[mpixscroll]]: This code will require alterations that are relevant for
  222. * supporting the scrolling through more pixels than some browsers normally
  223. * would support. (i.e. when we support more than "a million" pixels in the
  224. * escalator DOM). NOTE: these bits can most often also be identified by
  225. * searching for code that call scrollElem.getScrollTop();.
  226. */
  227. /**
  228. * A utility class that contains utility methods that are usually called
  229. * from JSNI.
  230. * <p>
  231. * The methods are moved in this class to minimize the amount of JSNI code
  232. * as much as feasible.
  233. */
  234. static class JsniUtil {
  235. public static class TouchHandlerBundle {
  236. /**
  237. * A <a href=
  238. * "http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsOverlay.html"
  239. * >JavaScriptObject overlay</a> for the <a
  240. * href="http://www.w3.org/TR/touch-events/">JavaScript
  241. * TouchEvent</a> object.
  242. * <p>
  243. * This needs to be used in the touch event handlers, since GWT's
  244. * {@link com.google.gwt.event.dom.client.TouchEvent TouchEvent}
  245. * can't be cast from the JSNI call, and the
  246. * {@link com.google.gwt.dom.client.NativeEvent NativeEvent} isn't
  247. * properly populated with the correct values.
  248. */
  249. private final static class CustomTouchEvent extends
  250. JavaScriptObject {
  251. protected CustomTouchEvent() {
  252. }
  253. public native NativeEvent getNativeEvent()
  254. /*-{
  255. return this;
  256. }-*/;
  257. public native int getPageX()
  258. /*-{
  259. return this.targetTouches[0].pageX;
  260. }-*/;
  261. public native int getPageY()
  262. /*-{
  263. return this.targetTouches[0].pageY;
  264. }-*/;
  265. }
  266. private double touches = 0;
  267. private int lastX = 0;
  268. private int lastY = 0;
  269. private double lastTime = 0;
  270. private boolean snappedScrollEnabled = true;
  271. private double deltaX = 0;
  272. private double deltaY = 0;
  273. private final Escalator escalator;
  274. private CustomTouchEvent latestTouchMoveEvent;
  275. private AnimationCallback mover = new AnimationCallback() {
  276. @Override
  277. public void execute(double timestamp) {
  278. if (touches != 1) {
  279. return;
  280. }
  281. final int x = latestTouchMoveEvent.getPageX();
  282. final int y = latestTouchMoveEvent.getPageY();
  283. deltaX = x - lastX;
  284. deltaY = y - lastY;
  285. lastX = x;
  286. lastY = y;
  287. lastTime = timestamp;
  288. // snap the scroll to the major axes, at first.
  289. if (snappedScrollEnabled) {
  290. final double oldDeltaX = deltaX;
  291. final double oldDeltaY = deltaY;
  292. /*
  293. * Scrolling snaps to 40 degrees vs. flick scroll's 30
  294. * degrees, since slow movements have poor resolution -
  295. * it's easy to interpret a slight angle as a steep
  296. * angle, since the sample rate is "unnecessarily" high.
  297. * 40 simply felt better than 30.
  298. */
  299. final double[] snapped = Escalator.snapDeltas(deltaX,
  300. deltaY, RATIO_OF_40_DEGREES);
  301. deltaX = snapped[0];
  302. deltaY = snapped[1];
  303. /*
  304. * if the snap failed once, let's follow the pointer
  305. * from now on.
  306. */
  307. if (oldDeltaX != 0 && deltaX == oldDeltaX
  308. && oldDeltaY != 0 && deltaY == oldDeltaY) {
  309. snappedScrollEnabled = false;
  310. }
  311. }
  312. moveScrollFromEvent(escalator, -deltaX, -deltaY,
  313. latestTouchMoveEvent.getNativeEvent());
  314. }
  315. };
  316. private AnimationHandle animationHandle;
  317. public TouchHandlerBundle(final Escalator escalator) {
  318. this.escalator = escalator;
  319. }
  320. public native JavaScriptObject getTouchStartHandler()
  321. /*-{
  322. // we need to store "this", since it won't be preserved on call.
  323. var self = this;
  324. return $entry(function (e) {
  325. self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchStart(*)(e);
  326. });
  327. }-*/;
  328. public native JavaScriptObject getTouchMoveHandler()
  329. /*-{
  330. // we need to store "this", since it won't be preserved on call.
  331. var self = this;
  332. return $entry(function (e) {
  333. self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchMove(*)(e);
  334. });
  335. }-*/;
  336. public native JavaScriptObject getTouchEndHandler()
  337. /*-{
  338. // we need to store "this", since it won't be preserved on call.
  339. var self = this;
  340. return $entry(function (e) {
  341. self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchEnd(*)(e);
  342. });
  343. }-*/;
  344. public void touchStart(final CustomTouchEvent event) {
  345. touches++;
  346. if (touches != 1) {
  347. return;
  348. }
  349. escalator.scroller.cancelFlickScroll();
  350. lastX = event.getPageX();
  351. lastY = event.getPageY();
  352. snappedScrollEnabled = true;
  353. }
  354. public void touchMove(final CustomTouchEvent event) {
  355. /*
  356. * since we only use the getPageX/Y, and calculate the diff
  357. * within the handler, we don't need to calculate any
  358. * intermediate deltas.
  359. */
  360. latestTouchMoveEvent = event;
  361. if (animationHandle != null) {
  362. animationHandle.cancel();
  363. }
  364. animationHandle = AnimationScheduler.get()
  365. .requestAnimationFrame(mover, escalator.bodyElem);
  366. event.getNativeEvent().preventDefault();
  367. mover.execute(Duration.currentTimeMillis());
  368. }
  369. public void touchEnd(@SuppressWarnings("unused")
  370. final CustomTouchEvent event) {
  371. touches--;
  372. if (touches == 0) {
  373. escalator.scroller.handleFlickScroll(deltaX, deltaY,
  374. lastTime);
  375. }
  376. }
  377. }
  378. public static void moveScrollFromEvent(final Escalator escalator,
  379. final double deltaX, final double deltaY,
  380. final NativeEvent event) {
  381. if (!Double.isNaN(deltaX)) {
  382. escalator.horizontalScrollbar.setScrollPosByDelta((int) deltaX);
  383. }
  384. if (!Double.isNaN(deltaY)) {
  385. escalator.verticalScrollbar.setScrollPosByDelta((int) deltaY);
  386. }
  387. /*
  388. * TODO: only prevent if not scrolled to end/bottom. Or no? UX team
  389. * needs to decide.
  390. */
  391. final boolean warrantedYScroll = deltaY != 0
  392. && escalator.verticalScrollbar.showsScrollHandle();
  393. final boolean warrantedXScroll = deltaX != 0
  394. && escalator.horizontalScrollbar.showsScrollHandle();
  395. if (warrantedYScroll || warrantedXScroll) {
  396. event.preventDefault();
  397. }
  398. }
  399. }
  400. /**
  401. * The animation callback that handles the animation of a touch-scrolling
  402. * flick with inertia.
  403. */
  404. private class FlickScrollAnimator implements AnimationCallback {
  405. private static final double MIN_MAGNITUDE = 0.005;
  406. private static final double MAX_SPEED = 7;
  407. private double velX;
  408. private double velY;
  409. private double prevTime = 0;
  410. private int millisLeft;
  411. private double xFric;
  412. private double yFric;
  413. private boolean cancelled = false;
  414. /**
  415. * Creates a new animation callback to handle touch-scrolling flick with
  416. * inertia.
  417. *
  418. * @param deltaX
  419. * the last scrolling delta in the x-axis in a touchmove
  420. * @param deltaY
  421. * the last scrolling delta in the y-axis in a touchmove
  422. * @param lastTime
  423. * the timestamp of the last touchmove
  424. */
  425. public FlickScrollAnimator(final double deltaX, final double deltaY,
  426. final double lastTime) {
  427. final double currentTimeMillis = Duration.currentTimeMillis();
  428. velX = Math.max(Math.min(deltaX / (currentTimeMillis - lastTime),
  429. MAX_SPEED), -MAX_SPEED);
  430. velY = Math.max(Math.min(deltaY / (currentTimeMillis - lastTime),
  431. MAX_SPEED), -MAX_SPEED);
  432. prevTime = lastTime;
  433. /*
  434. * If we're scrolling mainly in one of the four major directions,
  435. * and only a teeny bit to any other side, snap the scroll to that
  436. * major direction instead.
  437. */
  438. final double[] snapDeltas = Escalator.snapDeltas(velX, velY,
  439. RATIO_OF_30_DEGREES);
  440. velX = snapDeltas[0];
  441. velY = snapDeltas[1];
  442. if (velX * velX + velY * velY > MIN_MAGNITUDE) {
  443. millisLeft = 1500;
  444. xFric = velX / millisLeft;
  445. yFric = velY / millisLeft;
  446. } else {
  447. millisLeft = 0;
  448. }
  449. }
  450. @Override
  451. public void execute(final double timestamp) {
  452. if (millisLeft <= 0 || cancelled) {
  453. scroller.currentFlickScroller = null;
  454. return;
  455. }
  456. final int lastLeft = tBodyScrollLeft;
  457. final int lastTop = tBodyScrollTop;
  458. final double timeDiff = timestamp - prevTime;
  459. setScrollLeft((int) (tBodyScrollLeft - velX * timeDiff));
  460. velX -= xFric * timeDiff;
  461. setScrollTop(tBodyScrollTop - velY * timeDiff);
  462. velY -= yFric * timeDiff;
  463. cancelBecauseOfEdgeOrCornerMaybe(lastLeft, lastTop);
  464. prevTime = timestamp;
  465. millisLeft -= timeDiff;
  466. AnimationScheduler.get().requestAnimationFrame(this);
  467. }
  468. private void cancelBecauseOfEdgeOrCornerMaybe(final int lastLeft,
  469. final int lastTop) {
  470. if (lastLeft == horizontalScrollbar.getScrollPos()
  471. && lastTop == verticalScrollbar.getScrollPos()) {
  472. cancel();
  473. }
  474. }
  475. public void cancel() {
  476. cancelled = true;
  477. }
  478. }
  479. /**
  480. * ScrollDestination case-specific handling logic.
  481. */
  482. private static double getScrollPos(final ScrollDestination destination,
  483. final double targetStartPx, final double targetEndPx,
  484. final double viewportStartPx, final double viewportEndPx,
  485. final int padding) {
  486. final double viewportLength = viewportEndPx - viewportStartPx;
  487. switch (destination) {
  488. /*
  489. * Scroll as little as possible to show the target element. If the
  490. * element fits into view, this works as START or END depending on the
  491. * current scroll position. If the element does not fit into view, this
  492. * works as START.
  493. */
  494. case ANY: {
  495. final double startScrollPos = targetStartPx - padding;
  496. final double endScrollPos = targetEndPx + padding - viewportLength;
  497. if (startScrollPos < viewportStartPx) {
  498. return startScrollPos;
  499. } else if (targetEndPx + padding > viewportEndPx) {
  500. return endScrollPos;
  501. } else {
  502. // NOOP, it's already visible
  503. return viewportStartPx;
  504. }
  505. }
  506. /*
  507. * Scrolls so that the element is shown at the end of the viewport. The
  508. * viewport will, however, not scroll before its first element.
  509. */
  510. case END: {
  511. return targetEndPx + padding - viewportLength;
  512. }
  513. /*
  514. * Scrolls so that the element is shown in the middle of the viewport.
  515. * The viewport will, however, not scroll beyond its contents, given
  516. * more elements than what the viewport is able to show at once. Under
  517. * no circumstances will the viewport scroll before its first element.
  518. */
  519. case MIDDLE: {
  520. final double targetMiddle = targetStartPx
  521. + (targetEndPx - targetStartPx) / 2;
  522. return targetMiddle - viewportLength / 2;
  523. }
  524. /*
  525. * Scrolls so that the element is shown at the start of the viewport.
  526. * The viewport will, however, not scroll beyond its contents.
  527. */
  528. case START: {
  529. return targetStartPx - padding;
  530. }
  531. /*
  532. * Throw an error if we're here. This can only mean that
  533. * ScrollDestination has been carelessly amended..
  534. */
  535. default: {
  536. throw new IllegalArgumentException(
  537. "Internal: ScrollDestination has been modified, "
  538. + "but Escalator.getScrollPos has not been updated "
  539. + "to match new values.");
  540. }
  541. }
  542. }
  543. /** An inner class that handles all logic related to scrolling. */
  544. private class Scroller extends JsniWorkaround {
  545. private double lastScrollTop = 0;
  546. private double lastScrollLeft = 0;
  547. /**
  548. * The current flick scroll animator. This is <code>null</code> if the
  549. * view isn't animating a flick scroll at the moment.
  550. */
  551. private FlickScrollAnimator currentFlickScroller;
  552. public Scroller() {
  553. super(Escalator.this);
  554. }
  555. @Override
  556. protected native JavaScriptObject createScrollListenerFunction(
  557. Escalator esc)
  558. /*-{
  559. var vScroll = esc.@com.vaadin.client.ui.grid.Escalator::verticalScrollbar;
  560. var vScrollElem = vScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::getElement()();
  561. var hScroll = esc.@com.vaadin.client.ui.grid.Escalator::horizontalScrollbar;
  562. var hScrollElem = hScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::getElement()();
  563. return $entry(function(e) {
  564. var target = e.target || e.srcElement; // IE8 uses e.scrElement
  565. // in case the scroll event was native (i.e. scrollbars were dragged, or
  566. // the scrollTop/Left was manually modified), the bundles have old cache
  567. // values. We need to make sure that the caches are kept up to date.
  568. if (target === vScrollElem) {
  569. vScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::updateScrollPosFromDom()();
  570. } else if (target === hScrollElem) {
  571. hScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::updateScrollPosFromDom()();
  572. } else {
  573. $wnd.console.error("unexpected scroll target: "+target);
  574. }
  575. esc.@com.vaadin.client.ui.grid.Escalator::onScroll()();
  576. });
  577. }-*/;
  578. @Override
  579. protected native JavaScriptObject createMousewheelListenerFunction(
  580. Escalator esc)
  581. /*-{
  582. return $entry(function(e) {
  583. var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
  584. var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
  585. // IE8 has only delta y
  586. if (isNaN(deltaY)) {
  587. deltaY = -0.5*e.wheelDelta;
  588. }
  589. @com.vaadin.client.ui.grid.Escalator.JsniUtil::moveScrollFromEvent(*)(esc, deltaX, deltaY, e);
  590. });
  591. }-*/;
  592. /**
  593. * Recalculates the virtual viewport represented by the scrollbars, so
  594. * that the sizes of the scroll handles appear correct in the browser
  595. */
  596. public void recalculateScrollbarsForVirtualViewport() {
  597. int scrollContentHeight = body.calculateEstimatedTotalRowHeight();
  598. int scrollContentWidth = columnConfiguration.calculateRowWidth();
  599. double tableWrapperHeight = heightOfEscalator;
  600. double tableWrapperWidth = widthOfEscalator;
  601. boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
  602. - header.heightOfSection - footer.heightOfSection;
  603. boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth;
  604. // One dimension got scrollbars, but not the other. Recheck time!
  605. if (verticalScrollNeeded != horizontalScrollNeeded) {
  606. if (!verticalScrollNeeded && horizontalScrollNeeded) {
  607. verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
  608. - header.heightOfSection
  609. - footer.heightOfSection
  610. - horizontalScrollbar.getScrollbarThickness();
  611. } else {
  612. horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth
  613. - verticalScrollbar.getScrollbarThickness();
  614. }
  615. }
  616. // let's fix the table wrapper size, since it's now stable.
  617. if (verticalScrollNeeded) {
  618. tableWrapperWidth -= verticalScrollbar.getScrollbarThickness();
  619. }
  620. if (horizontalScrollNeeded) {
  621. tableWrapperHeight -= horizontalScrollbar
  622. .getScrollbarThickness();
  623. }
  624. tableWrapper.getStyle().setHeight(tableWrapperHeight, Unit.PX);
  625. tableWrapper.getStyle().setWidth(tableWrapperWidth, Unit.PX);
  626. verticalScrollbar.setOffsetSize((int) (tableWrapperHeight
  627. - footer.heightOfSection - header.heightOfSection));
  628. verticalScrollbar.setScrollSize(scrollContentHeight);
  629. /*
  630. * If decreasing the amount of frozen columns, and scrolled to the
  631. * right, the scroll position might reset. So we need to remember
  632. * the scroll position, and re-apply it once the scrollbar size has
  633. * been adjusted.
  634. */
  635. int prevScrollPos = horizontalScrollbar.getScrollPos();
  636. int unfrozenPixels = columnConfiguration
  637. .getCalculatedColumnsWidth(Range.between(
  638. columnConfiguration.getFrozenColumnCount(),
  639. columnConfiguration.getColumnCount()));
  640. int frozenPixels = scrollContentWidth - unfrozenPixels;
  641. double hScrollOffsetWidth = tableWrapperWidth - frozenPixels;
  642. horizontalScrollbar.setOffsetSize((int) hScrollOffsetWidth);
  643. horizontalScrollbar.setScrollSize(unfrozenPixels);
  644. horizontalScrollbar.getElement().getStyle()
  645. .setLeft(frozenPixels, Unit.PX);
  646. horizontalScrollbar.setScrollPos(prevScrollPos);
  647. }
  648. /**
  649. * Logical scrolling event handler for the entire widget.
  650. *
  651. * @param scrollLeft
  652. * the current number of pixels that the user has scrolled
  653. * from left
  654. * @param scrollTop
  655. * the current number of pixels that the user has scrolled
  656. * from the top
  657. */
  658. public void onScroll() {
  659. if (internalScrollEventCalls > 0) {
  660. internalScrollEventCalls--;
  661. return;
  662. }
  663. final int scrollLeft = horizontalScrollbar.getScrollPos();
  664. final int scrollTop = verticalScrollbar.getScrollPos();
  665. if (lastScrollLeft != scrollLeft) {
  666. for (int i = 0; i < columnConfiguration.frozenColumns; i++) {
  667. header.updateFreezePosition(i, scrollLeft);
  668. body.updateFreezePosition(i, scrollLeft);
  669. footer.updateFreezePosition(i, scrollLeft);
  670. }
  671. position.set(headElem, -scrollLeft, 0);
  672. /*
  673. * TODO [[optimize]]: cache this value in case the instanceof
  674. * check has undesirable overhead. This could also be a
  675. * candidate for some deferred binding magic so that e.g.
  676. * AbsolutePosition is not even considered in permutations that
  677. * we know support something better. That would let the compiler
  678. * completely remove the entire condition since it knows that
  679. * the if will never be true.
  680. */
  681. if (position instanceof AbsolutePosition) {
  682. /*
  683. * we don't want to put "top: 0" on the footer, since it'll
  684. * render wrong, as we already have
  685. * "bottom: $footer-height".
  686. */
  687. footElem.getStyle().setLeft(-scrollLeft, Unit.PX);
  688. } else {
  689. position.set(footElem, -scrollLeft, 0);
  690. }
  691. lastScrollLeft = scrollLeft;
  692. }
  693. body.setBodyScrollPosition(scrollLeft, scrollTop);
  694. lastScrollTop = scrollTop;
  695. body.updateEscalatorRowsOnScroll();
  696. /*
  697. * TODO [[optimize]]: Might avoid a reflow by first calculating new
  698. * scrolltop and scrolleft, then doing the escalator magic based on
  699. * those numbers and only updating the positions after that.
  700. */
  701. }
  702. public native void attachScrollListener(Element element)
  703. /*
  704. * Attaching events with JSNI instead of the GWT event mechanism because
  705. * GWT didn't provide enough details in events, or triggering the event
  706. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  707. * and skill, it could be done with better success. JavaScript overlay
  708. * types might work. This might also get rid of the JsniWorkaround
  709. * class.
  710. */
  711. /*-{
  712. if (element.addEventListener) {
  713. element.addEventListener("scroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
  714. } else {
  715. element.attachEvent("onscroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
  716. }
  717. }-*/;
  718. public native void detachScrollListener(Element element)
  719. /*
  720. * Attaching events with JSNI instead of the GWT event mechanism because
  721. * GWT didn't provide enough details in events, or triggering the event
  722. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  723. * and skill, it could be done with better success. JavaScript overlay
  724. * types might work. This might also get rid of the JsniWorkaround
  725. * class.
  726. */
  727. /*-{
  728. if (element.addEventListener) {
  729. element.removeEventListener("scroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
  730. } else {
  731. element.detachEvent("onscroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
  732. }
  733. }-*/;
  734. public native void attachMousewheelListener(Element element)
  735. /*
  736. * Attaching events with JSNI instead of the GWT event mechanism because
  737. * GWT didn't provide enough details in events, or triggering the event
  738. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  739. * and skill, it could be done with better success. JavaScript overlay
  740. * types might work. This might also get rid of the JsniWorkaround
  741. * class.
  742. */
  743. /*-{
  744. if (element.addEventListener) {
  745. // firefox likes "wheel", while others use "mousewheel"
  746. var eventName = element.onwheel===undefined?"mousewheel":"wheel";
  747. element.addEventListener(eventName, this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
  748. } else {
  749. // IE8
  750. element.attachEvent("onmousewheel", this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
  751. }
  752. }-*/;
  753. public native void detachMousewheelListener(Element element)
  754. /*
  755. * Detaching events with JSNI instead of the GWT event mechanism because
  756. * GWT didn't provide enough details in events, or triggering the event
  757. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  758. * and skill, it could be done with better success. JavaScript overlay
  759. * types might work. This might also get rid of the JsniWorkaround
  760. * class.
  761. */
  762. /*-{
  763. if (element.addEventListener) {
  764. // firefox likes "wheel", while others use "mousewheel"
  765. var eventName = element.onwheel===undefined?"mousewheel":"wheel";
  766. element.removeEventListener(eventName, this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
  767. } else {
  768. // IE8
  769. element.detachEvent("onmousewheel", this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
  770. }
  771. }-*/;
  772. public native void attachTouchListeners(Element element)
  773. /*
  774. * Detaching events with JSNI instead of the GWT event mechanism because
  775. * GWT didn't provide enough details in events, or triggering the event
  776. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  777. * and skill, it could be done with better success. JavaScript overlay
  778. * types might work. This might also get rid of the JsniWorkaround
  779. * class.
  780. */
  781. /*-{
  782. if (element.addEventListener) {
  783. element.addEventListener("touchstart", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchStartFunction);
  784. element.addEventListener("touchmove", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchMoveFunction);
  785. element.addEventListener("touchend", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction);
  786. element.addEventListener("touchcancel", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction);
  787. } else {
  788. // this would be IE8, but we don't support it with touch
  789. }
  790. }-*/;
  791. public native void detachTouchListeners(Element element)
  792. /*
  793. * Detaching events with JSNI instead of the GWT event mechanism because
  794. * GWT didn't provide enough details in events, or triggering the event
  795. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  796. * and skill, it could be done with better success. JavaScript overlay
  797. * types might work. This might also get rid of the JsniWorkaround
  798. * class.
  799. */
  800. /*-{
  801. if (element.removeEventListener) {
  802. element.removeEventListener("touchstart", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchStartFunction);
  803. element.removeEventListener("touchmove", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchMoveFunction);
  804. element.removeEventListener("touchend", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction);
  805. element.removeEventListener("touchcancel", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction);
  806. } else {
  807. // this would be IE8, but we don't support it with touch
  808. }
  809. }-*/;
  810. private void cancelFlickScroll() {
  811. if (currentFlickScroller != null) {
  812. currentFlickScroller.cancel();
  813. }
  814. }
  815. /**
  816. * Handles a touch-based flick scroll.
  817. *
  818. * @param deltaX
  819. * the last scrolling delta in the x-axis in a touchmove
  820. * @param deltaY
  821. * the last scrolling delta in the y-axis in a touchmove
  822. * @param lastTime
  823. * the timestamp of the last touchmove
  824. */
  825. public void handleFlickScroll(double deltaX, double deltaY,
  826. double lastTime) {
  827. currentFlickScroller = new FlickScrollAnimator(deltaX, deltaY,
  828. lastTime);
  829. AnimationScheduler.get()
  830. .requestAnimationFrame(currentFlickScroller);
  831. }
  832. public void scrollToColumn(final int columnIndex,
  833. final ScrollDestination destination, final int padding) {
  834. assert columnIndex >= columnConfiguration.frozenColumns : "Can't scroll to a frozen column";
  835. /*
  836. * To cope with frozen columns, we just pretend those columns are
  837. * not there at all when calculating the position of the target
  838. * column and the boundaries of the viewport. The resulting
  839. * scrollLeft will be correct without compensation since the DOM
  840. * structure effectively means that scrollLeft also ignores the
  841. * frozen columns.
  842. */
  843. final int frozenPixels = columnConfiguration
  844. .getCalculatedColumnsWidth(Range.withLength(0,
  845. columnConfiguration.frozenColumns));
  846. final int targetStartPx = columnConfiguration
  847. .getCalculatedColumnsWidth(Range.withLength(0, columnIndex))
  848. - frozenPixels;
  849. final int targetEndPx = targetStartPx
  850. + columnConfiguration.getColumnWidthActual(columnIndex);
  851. final int viewportStartPx = getScrollLeft();
  852. int viewportEndPx = viewportStartPx + getElement().getOffsetWidth()
  853. - frozenPixels;
  854. if (verticalScrollbar.showsScrollHandle()) {
  855. viewportEndPx -= Util.getNativeScrollbarSize();
  856. }
  857. final double scrollLeft = getScrollPos(destination, targetStartPx,
  858. targetEndPx, viewportStartPx, viewportEndPx, padding);
  859. /*
  860. * note that it doesn't matter if the scroll would go beyond the
  861. * content, since the browser will adjust for that, and everything
  862. * fall into line accordingly.
  863. */
  864. setScrollLeft((int) scrollLeft);
  865. }
  866. public void scrollToRow(final int rowIndex,
  867. final ScrollDestination destination, final int padding) {
  868. /*
  869. * FIXME [[rowheight]]: coded to work only with default row heights
  870. * - will not work with variable row heights
  871. */
  872. final int targetStartPx = body.getDefaultRowHeight() * rowIndex;
  873. final int targetEndPx = targetStartPx + body.getDefaultRowHeight();
  874. final double viewportStartPx = getScrollTop();
  875. final double viewportEndPx = viewportStartPx
  876. + body.calculateHeight();
  877. final double scrollTop = getScrollPos(destination, targetStartPx,
  878. targetEndPx, viewportStartPx, viewportEndPx, padding);
  879. /*
  880. * note that it doesn't matter if the scroll would go beyond the
  881. * content, since the browser will adjust for that, and everything
  882. * falls into line accordingly.
  883. */
  884. setScrollTop(scrollTop);
  885. }
  886. }
  887. private abstract class AbstractRowContainer implements RowContainer {
  888. private static final int INITIAL_DEFAULT_ROW_HEIGHT = 20;
  889. private EscalatorUpdater updater = EscalatorUpdater.NULL;
  890. private int rows;
  891. /**
  892. * The table section element ({@code <thead>}, {@code <tbody>} or
  893. * {@code <tfoot>}) the rows (i.e. {@code <tr>} tags) are contained in.
  894. */
  895. protected final Element root;
  896. /** The height of the combined rows in the DOM. */
  897. protected double heightOfSection = -1;
  898. /**
  899. * The primary style name of the escalator. Most commonly provided by
  900. * Escalator as "v-escalator".
  901. */
  902. private String primaryStyleName = null;
  903. /**
  904. * A map containing cached values of an element's current top position.
  905. * <p>
  906. * Don't use this field directly, because it will not take proper care
  907. * of all the bookkeeping required.
  908. *
  909. * @deprecated Use {@link #setRowPosition(Element, int, int)},
  910. * {@link #getRowTop(Element)} and
  911. * {@link #removeRowPosition(Element)} instead.
  912. */
  913. @Deprecated
  914. private final Map<Element, Integer> rowTopPositionMap = new HashMap<Element, Integer>();
  915. private int defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT;
  916. public AbstractRowContainer(final Element rowContainerElement) {
  917. root = rowContainerElement;
  918. }
  919. /**
  920. * Gets the tag name of an element to represent a cell in a row.
  921. * <p>
  922. * Usually {@code "th"} or {@code "td"}.
  923. * <p>
  924. * <em>Note:</em> To actually <em>create</em> such an element, use
  925. * {@link #createCellElement()} instead.
  926. *
  927. * @return the tag name for the element to represent cells as
  928. * @see #createCellElement()
  929. */
  930. protected abstract String getCellElementTagName();
  931. @Override
  932. public EscalatorUpdater getEscalatorUpdater() {
  933. return updater;
  934. }
  935. /**
  936. * {@inheritDoc}
  937. * <p>
  938. * <em>Implementation detail:</em> This method does no DOM modifications
  939. * (i.e. is very cheap to call) if there is no data for rows or columns
  940. * when this method is called.
  941. *
  942. * @see #hasColumnAndRowData()
  943. */
  944. @Override
  945. public void setEscalatorUpdater(final EscalatorUpdater escalatorUpdater) {
  946. if (escalatorUpdater == null) {
  947. throw new IllegalArgumentException(
  948. "escalator updater cannot be null");
  949. }
  950. updater = escalatorUpdater;
  951. if (hasColumnAndRowData() && getRowCount() > 0) {
  952. refreshRows(0, getRowCount());
  953. }
  954. }
  955. /**
  956. * {@inheritDoc}
  957. * <p>
  958. * <em>Implementation detail:</em> This method does no DOM modifications
  959. * (i.e. is very cheap to call) if there are no rows in the DOM when
  960. * this method is called.
  961. *
  962. * @see #hasSomethingInDom()
  963. */
  964. @Override
  965. public void removeRows(final int index, final int numberOfRows) {
  966. assertArgumentsAreValidAndWithinRange(index, numberOfRows);
  967. rows -= numberOfRows;
  968. if (!isAttached()) {
  969. return;
  970. }
  971. if (hasSomethingInDom()) {
  972. paintRemoveRows(index, numberOfRows);
  973. }
  974. }
  975. protected abstract void paintRemoveRows(final int index,
  976. final int numberOfRows);
  977. private void assertArgumentsAreValidAndWithinRange(final int index,
  978. final int numberOfRows) throws IllegalArgumentException,
  979. IndexOutOfBoundsException {
  980. if (numberOfRows < 1) {
  981. throw new IllegalArgumentException(
  982. "Number of rows must be 1 or greater (was "
  983. + numberOfRows + ")");
  984. }
  985. if (index < 0 || index + numberOfRows > getRowCount()) {
  986. throw new IndexOutOfBoundsException("The given "
  987. + "row range (" + index + ".." + (index + numberOfRows)
  988. + ") was outside of the current number of rows ("
  989. + getRowCount() + ")");
  990. }
  991. }
  992. @Override
  993. public int getRowCount() {
  994. return rows;
  995. }
  996. /**
  997. * {@inheritDoc}
  998. * <p>
  999. * <em>Implementation detail:</em> This method does no DOM modifications
  1000. * (i.e. is very cheap to call) if there is no data for columns when
  1001. * this method is called.
  1002. *
  1003. * @see #hasColumnAndRowData()
  1004. */
  1005. @Override
  1006. public void insertRows(final int index, final int numberOfRows) {
  1007. if (index < 0 || index > getRowCount()) {
  1008. throw new IndexOutOfBoundsException("The given index (" + index
  1009. + ") was outside of the current number of rows (0.."
  1010. + getRowCount() + ")");
  1011. }
  1012. if (numberOfRows < 1) {
  1013. throw new IllegalArgumentException(
  1014. "Number of rows must be 1 or greater (was "
  1015. + numberOfRows + ")");
  1016. }
  1017. rows += numberOfRows;
  1018. /*
  1019. * only add items in the DOM if the widget itself is attached to the
  1020. * DOM. We can't calculate sizes otherwise.
  1021. */
  1022. if (isAttached()) {
  1023. paintInsertRows(index, numberOfRows);
  1024. }
  1025. }
  1026. /**
  1027. * Actually add rows into the DOM, now that everything can be
  1028. * calculated.
  1029. *
  1030. * @param visualIndex
  1031. * the DOM index to add rows into
  1032. * @param numberOfRows
  1033. * the number of rows to insert
  1034. * @return a list of the added row elements
  1035. */
  1036. protected List<Element> paintInsertRows(final int visualIndex,
  1037. final int numberOfRows) {
  1038. assert isAttached() : "Can't paint rows if Escalator is not attached";
  1039. final List<Element> addedRows = new ArrayList<Element>();
  1040. if (numberOfRows < 1) {
  1041. return addedRows;
  1042. }
  1043. Node referenceRow;
  1044. if (root.getChildCount() != 0 && visualIndex != 0) {
  1045. // get the row node we're inserting stuff after
  1046. referenceRow = root.getChild(visualIndex - 1);
  1047. } else {
  1048. // index is 0, so just prepend.
  1049. referenceRow = null;
  1050. }
  1051. for (int row = visualIndex; row < visualIndex + numberOfRows; row++) {
  1052. final int rowHeight = getDefaultRowHeight();
  1053. final Element tr = DOM.createTR();
  1054. addedRows.add(tr);
  1055. tr.addClassName(getStylePrimaryName() + "-row");
  1056. referenceRow = insertAfterReferenceAndUpdateIt(root, tr,
  1057. referenceRow);
  1058. for (int col = 0; col < columnConfiguration.getColumnCount(); col++) {
  1059. final int colWidth = columnConfiguration
  1060. .getColumnWidthActual(col);
  1061. final Element cellElem = createCellElement(rowHeight,
  1062. colWidth);
  1063. tr.appendChild(cellElem);
  1064. // Set stylename and position if new cell is frozen
  1065. if (col < columnConfiguration.frozenColumns) {
  1066. cellElem.addClassName("frozen");
  1067. position.set(cellElem, scroller.lastScrollLeft, 0);
  1068. }
  1069. }
  1070. refreshRow(tr, row);
  1071. }
  1072. reapplyRowWidths();
  1073. recalculateSectionHeight();
  1074. return addedRows;
  1075. }
  1076. private Node insertAfterReferenceAndUpdateIt(final Element parent,
  1077. final Element elem, final Node referenceNode) {
  1078. if (referenceNode != null) {
  1079. parent.insertAfter(elem, referenceNode);
  1080. } else {
  1081. /*
  1082. * referencenode being null means we have offset 0, i.e. make it
  1083. * the first row
  1084. */
  1085. /*
  1086. * TODO [[optimize]]: Is insertFirst or append faster for an
  1087. * empty root?
  1088. */
  1089. parent.insertFirst(elem);
  1090. }
  1091. return elem;
  1092. }
  1093. abstract protected void recalculateSectionHeight();
  1094. /**
  1095. * Returns the estimated height of all rows in the row container.
  1096. * <p>
  1097. * The estimate is promised to be correct as long as there are no rows
  1098. * with calculated heights.
  1099. */
  1100. protected int calculateEstimatedTotalRowHeight() {
  1101. return getDefaultRowHeight() * getRowCount();
  1102. }
  1103. /**
  1104. * {@inheritDoc}
  1105. * <p>
  1106. * <em>Implementation detail:</em> This method does no DOM modifications
  1107. * (i.e. is very cheap to call) if there is no data for columns when
  1108. * this method is called.
  1109. *
  1110. * @see #hasColumnAndRowData()
  1111. */
  1112. @Override
  1113. public void refreshRows(final int index, final int numberOfRows) {
  1114. Profiler.enter("Escalator.AbstractRowContainer.refreshRows");
  1115. assertArgumentsAreValidAndWithinRange(index, numberOfRows);
  1116. if (!isAttached()) {
  1117. return;
  1118. }
  1119. /*
  1120. * TODO [[rowheight]]: even if no rows are evaluated in the current
  1121. * viewport, the heights of some unrendered rows might change in a
  1122. * refresh. This would cause the scrollbar to be adjusted (in
  1123. * scrollHeight and/or scrollTop). Do we want to take this into
  1124. * account?
  1125. */
  1126. if (hasColumnAndRowData()) {
  1127. /*
  1128. * TODO [[rowheight]]: nudge rows down with
  1129. * refreshRowPositions() as needed
  1130. */
  1131. for (int row = index; row < index + numberOfRows; row++) {
  1132. final Node tr = getTrByVisualIndex(row);
  1133. refreshRow(tr, row);
  1134. }
  1135. }
  1136. Profiler.leave("Escalator.AbstractRowContainer.refreshRows");
  1137. }
  1138. void refreshRow(final Node tr, final int logicalRowIndex) {
  1139. flyweightRow.setup((Element) tr, logicalRowIndex,
  1140. columnConfiguration.getCalculatedColumnWidths());
  1141. updater.updateCells(flyweightRow, flyweightRow.getCells());
  1142. /*
  1143. * the "assert" guarantees that this code is run only during
  1144. * development/debugging.
  1145. */
  1146. assert flyweightRow.teardown();
  1147. }
  1148. /**
  1149. * Create and setup an empty cell element.
  1150. *
  1151. * @param width
  1152. * the width of the cell, in pixels
  1153. * @param height
  1154. * the height of the cell, in pixels
  1155. *
  1156. * @return a set-up empty cell element
  1157. */
  1158. @SuppressWarnings("hiding")
  1159. public Element createCellElement(final int height, final int width) {
  1160. final Element cellElem = DOM.createElement(getCellElementTagName());
  1161. cellElem.getStyle().setHeight(height, Unit.PX);
  1162. cellElem.getStyle().setWidth(width, Unit.PX);
  1163. cellElem.addClassName(getStylePrimaryName() + "-cell");
  1164. return cellElem;
  1165. }
  1166. /**
  1167. * Gets the child element that is visually at a certain index
  1168. *
  1169. * @param index
  1170. * the index of the element to retrieve
  1171. * @return the element at position {@code index}
  1172. * @throws IndexOutOfBoundsException
  1173. * if {@code index} is not valid within {@link #root}
  1174. */
  1175. abstract protected Element getTrByVisualIndex(int index)
  1176. throws IndexOutOfBoundsException;
  1177. protected void paintRemoveColumns(final int offset,
  1178. final int numberOfColumns,
  1179. final List<ColumnConfigurationImpl.Column> removedColumns) {
  1180. final NodeList<Node> childNodes = root.getChildNodes();
  1181. for (int visualRowIndex = 0; visualRowIndex < childNodes
  1182. .getLength(); visualRowIndex++) {
  1183. final Node tr = childNodes.getItem(visualRowIndex);
  1184. for (int column = 0; column < numberOfColumns; column++) {
  1185. Element cellElement = tr.getChild(offset).cast();
  1186. detachPossibleWidgetFromCell(cellElement);
  1187. cellElement.removeFromParent();
  1188. }
  1189. }
  1190. reapplyRowWidths();
  1191. final int firstRemovedColumnLeft = columnConfiguration
  1192. .getCalculatedColumnsWidth(Range.withLength(0, offset));
  1193. final boolean columnsWereRemovedFromLeftOfTheViewport = scroller.lastScrollLeft > firstRemovedColumnLeft;
  1194. if (columnsWereRemovedFromLeftOfTheViewport) {
  1195. int removedColumnsPxAmount = 0;
  1196. for (ColumnConfigurationImpl.Column removedColumn : removedColumns) {
  1197. removedColumnsPxAmount += removedColumn
  1198. .getCalculatedWidth();
  1199. }
  1200. final int leftByDiff = (int) (scroller.lastScrollLeft - removedColumnsPxAmount);
  1201. final int newScrollLeft = Math.max(firstRemovedColumnLeft,
  1202. leftByDiff);
  1203. horizontalScrollbar.setScrollPos(newScrollLeft);
  1204. }
  1205. // this needs to be after the scroll position adjustment above.
  1206. scroller.recalculateScrollbarsForVirtualViewport();
  1207. /*
  1208. * Because we might remove columns where affected by colspans, it's
  1209. * easiest to simply redraw everything when columns are modified.
  1210. *
  1211. * Yes, this is a TODO [[optimize]].
  1212. */
  1213. if (getRowCount() > 0
  1214. && getColumnConfiguration().getColumnCount() > 0) {
  1215. refreshRows(0, getRowCount());
  1216. }
  1217. }
  1218. void detachPossibleWidgetFromCell(Node cellNode) {
  1219. // Detach possible widget
  1220. Widget widget = getWidgetFromCell(cellNode);
  1221. if (widget != null) {
  1222. // Orphan.
  1223. setParent(widget, null);
  1224. // Physical detach.
  1225. cellNode.removeChild(widget.getElement());
  1226. }
  1227. }
  1228. protected void paintInsertColumns(final int offset,
  1229. final int numberOfColumns, boolean frozen) {
  1230. final NodeList<Node> childNodes = root.getChildNodes();
  1231. for (int row = 0; row < childNodes.getLength(); row++) {
  1232. final int rowHeight = getDefaultRowHeight();
  1233. final Element tr = getTrByVisualIndex(row);
  1234. Node referenceCell;
  1235. if (offset != 0) {
  1236. referenceCell = tr.getChild(offset - 1);
  1237. } else {
  1238. referenceCell = null;
  1239. }
  1240. for (int col = offset; col < offset + numberOfColumns; col++) {
  1241. final int colWidth = columnConfiguration
  1242. .getColumnWidthActual(col);
  1243. final Element cellElem = createCellElement(rowHeight,
  1244. colWidth);
  1245. referenceCell = insertAfterReferenceAndUpdateIt(tr,
  1246. cellElem, referenceCell);
  1247. }
  1248. }
  1249. reapplyRowWidths();
  1250. if (frozen) {
  1251. for (int col = offset; col < offset + numberOfColumns; col++) {
  1252. setColumnFrozen(col, true);
  1253. }
  1254. }
  1255. // this needs to be before the scrollbar adjustment.
  1256. scroller.recalculateScrollbarsForVirtualViewport();
  1257. int pixelsToInsertedColumn = columnConfiguration
  1258. .getCalculatedColumnsWidth(Range.withLength(0, offset));
  1259. final boolean columnsWereAddedToTheLeftOfViewport = scroller.lastScrollLeft > pixelsToInsertedColumn;
  1260. if (columnsWereAddedToTheLeftOfViewport) {
  1261. int insertedColumnsWidth = columnConfiguration
  1262. .getCalculatedColumnsWidth(Range.withLength(offset,
  1263. numberOfColumns));
  1264. horizontalScrollbar
  1265. .setScrollPos((int) (scroller.lastScrollLeft + insertedColumnsWidth));
  1266. }
  1267. /*
  1268. * Because we might insert columns where affected by colspans, it's
  1269. * easiest to simply redraw everything when columns are modified.
  1270. *
  1271. * Yes, this is a TODO [[optimize]].
  1272. */
  1273. if (getRowCount() > 0
  1274. && getColumnConfiguration().getColumnCount() > 1) {
  1275. refreshRows(0, getRowCount());
  1276. }
  1277. }
  1278. public void setColumnFrozen(int column, boolean frozen) {
  1279. final NodeList<Node> childNodes = root.getChildNodes();
  1280. for (int row = 0; row < childNodes.getLength(); row++) {
  1281. final Element tr = childNodes.getItem(row).cast();
  1282. Element cell = (Element) tr.getChild(column);
  1283. if (frozen) {
  1284. cell.addClassName("frozen");
  1285. } else {
  1286. cell.removeClassName("frozen");
  1287. position.reset(cell);
  1288. }
  1289. }
  1290. if (frozen) {
  1291. updateFreezePosition(column, scroller.lastScrollLeft);
  1292. }
  1293. }
  1294. public void updateFreezePosition(int column, double scrollLeft) {
  1295. final NodeList<Node> childNodes = root.getChildNodes();
  1296. for (int row = 0; row < childNodes.getLength(); row++) {
  1297. final Element tr = childNodes.getItem(row).cast();
  1298. Element cell = (Element) tr.getChild(column);
  1299. position.set(cell, scrollLeft, 0);
  1300. }
  1301. }
  1302. /**
  1303. * Iterates through all the cells in a column and returns the width of
  1304. * the widest element in this RowContainer.
  1305. *
  1306. * @param index
  1307. * the index of the column to inspect
  1308. * @return the pixel width of the widest element in the indicated column
  1309. */
  1310. public int calculateMaxColWidth(int index) {
  1311. Element row = root.getFirstChildElement();
  1312. int maxWidth = 0;
  1313. while (row != null) {
  1314. final Element cell = (Element) row.getChild(index);
  1315. final boolean isVisible = !cell.getStyle().getDisplay()
  1316. .equals(Display.NONE.getCssName());
  1317. if (isVisible) {
  1318. maxWidth = Math.max(maxWidth, cell.getScrollWidth());
  1319. }
  1320. row = row.getNextSiblingElement();
  1321. }
  1322. return maxWidth;
  1323. }
  1324. /**
  1325. * Reapplies all the cells' widths according to the calculated widths in
  1326. * the column configuration.
  1327. */
  1328. public void reapplyColumnWidths() {
  1329. Element row = root.getFirstChildElement();
  1330. while (row != null) {
  1331. Element cell = row.getFirstChildElement();
  1332. int columnIndex = 0;
  1333. while (cell != null) {
  1334. @SuppressWarnings("hiding")
  1335. final int width = getCalculatedColumnWidthWithColspan(cell,
  1336. columnIndex);
  1337. /*
  1338. * TODO Should Escalator implement ProvidesResize at some
  1339. * point, this is where we need to do that.
  1340. */
  1341. cell.getStyle().setWidth(width, Unit.PX);
  1342. cell = cell.getNextSiblingElement();
  1343. columnIndex++;
  1344. }
  1345. row = row.getNextSiblingElement();
  1346. }
  1347. reapplyRowWidths();
  1348. }
  1349. private int getCalculatedColumnWidthWithColspan(final Element cell,
  1350. final int columnIndex) {
  1351. final int colspan = cell.getPropertyInt(FlyweightCell.COLSPAN_ATTR);
  1352. Range spannedColumns = Range.withLength(columnIndex, colspan);
  1353. /*
  1354. * Since browsers don't explode with overflowing colspans, escalator
  1355. * shouldn't either.
  1356. */
  1357. if (spannedColumns.getEnd() > columnConfiguration.getColumnCount()) {
  1358. spannedColumns = Range.between(columnIndex,
  1359. columnConfiguration.getColumnCount());
  1360. }
  1361. return columnConfiguration
  1362. .getCalculatedColumnsWidth(spannedColumns);
  1363. }
  1364. /**
  1365. * Applies the total length of the columns to each row element.
  1366. * <p>
  1367. * <em>Note:</em> In contrast to {@link #reapplyColumnWidths()}, this
  1368. * method only modifies the width of the {@code <tr>} element, not the
  1369. * cells within.
  1370. */
  1371. protected void reapplyRowWidths() {
  1372. int rowWidth = columnConfiguration.calculateRowWidth();
  1373. com.google.gwt.dom.client.Element row = root.getFirstChildElement();
  1374. while (row != null) {
  1375. row.getStyle().setWidth(rowWidth, Unit.PX);
  1376. row = row.getNextSiblingElement();
  1377. }
  1378. }
  1379. /**
  1380. * The primary style name for the container.
  1381. *
  1382. * @param primaryStyleName
  1383. * the style name to use as prefix for all row and cell style
  1384. * names.
  1385. */
  1386. protected void setStylePrimaryName(String primaryStyleName) {
  1387. String oldStyle = getStylePrimaryName();
  1388. if (SharedUtil.equals(oldStyle, primaryStyleName)) {
  1389. return;
  1390. }
  1391. this.primaryStyleName = primaryStyleName;
  1392. // Update already rendered rows and cells
  1393. Node row = root.getFirstChild();
  1394. while (row != null) {
  1395. Element rowElement = row.cast();
  1396. UIObject.setStylePrimaryName(rowElement, primaryStyleName
  1397. + "-row");
  1398. Node cell = row.getFirstChild();
  1399. while (cell != null) {
  1400. Element cellElement = cell.cast();
  1401. UIObject.setStylePrimaryName(cellElement, primaryStyleName
  1402. + "-cell");
  1403. cell = cell.getNextSibling();
  1404. }
  1405. row = row.getNextSibling();
  1406. }
  1407. }
  1408. /**
  1409. * Returns the primary style name of the container.
  1410. *
  1411. * @return The primary style name or <code>null</code> if not set.
  1412. */
  1413. protected String getStylePrimaryName() {
  1414. return primaryStyleName;
  1415. }
  1416. @Override
  1417. public void setDefaultRowHeight(int px) throws IllegalArgumentException {
  1418. if (px < 1) {
  1419. throw new IllegalArgumentException("Height must be positive. "
  1420. + px + " was given.");
  1421. }
  1422. defaultRowHeight = px;
  1423. reapplyDefaultRowHeights();
  1424. }
  1425. @Override
  1426. public int getDefaultRowHeight() {
  1427. return defaultRowHeight;
  1428. }
  1429. /**
  1430. * The default height of rows has (most probably) changed.
  1431. * <p>
  1432. * Make sure that the displayed rows with a default height are updated
  1433. * in height and top position.
  1434. * <p>
  1435. * <em>Note:</em>This implementation should not call
  1436. * {@link Escalator#recalculateElementSizes()} - it is done by the
  1437. * discretion of the caller of this method.
  1438. */
  1439. protected abstract void reapplyDefaultRowHeights();
  1440. protected void reapplyRowHeight(final Element tr, final int heightPx) {
  1441. Element cellElem = tr.getFirstChildElement().cast();
  1442. while (cellElem != null) {
  1443. cellElem.getStyle().setHeight(heightPx, Unit.PX);
  1444. cellElem = cellElem.getNextSiblingElement();
  1445. }
  1446. /*
  1447. * no need to apply height to tr-element, it'll be resized
  1448. * implicitly.
  1449. */
  1450. }
  1451. @SuppressWarnings("boxing")
  1452. protected void setRowPosition(final Element tr, final int x, final int y) {
  1453. position.set(tr, x, y);
  1454. rowTopPositionMap.put(tr, y);
  1455. }
  1456. @SuppressWarnings("boxing")
  1457. protected int getRowTop(final Element tr) {
  1458. return rowTopPositionMap.get(tr);
  1459. }
  1460. protected void removeRowPosition(Element tr) {
  1461. rowTopPositionMap.remove(tr);
  1462. }
  1463. }
  1464. private abstract class AbstractStaticRowContainer extends
  1465. AbstractRowContainer {
  1466. public AbstractStaticRowContainer(final Element headElement) {
  1467. super(headElement);
  1468. }
  1469. @Override
  1470. protected void paintRemoveRows(final int index, final int numberOfRows) {
  1471. for (int i = index; i < index + numberOfRows; i++) {
  1472. final Element tr = (Element) root.getChild(index);
  1473. for (int c = 0; c < tr.getChildCount(); c++) {
  1474. detachPossibleWidgetFromCell((Element) tr.getChild(c)
  1475. .cast());
  1476. }
  1477. tr.removeFromParent();
  1478. }
  1479. recalculateSectionHeight();
  1480. }
  1481. @Override
  1482. protected Element getTrByVisualIndex(final int index)
  1483. throws IndexOutOfBoundsException {
  1484. if (index >= 0 && index < root.getChildCount()) {
  1485. return (Element) root.getChild(index);
  1486. } else {
  1487. throw new IndexOutOfBoundsException("No such visual index: "
  1488. + index);
  1489. }
  1490. }
  1491. @Override
  1492. public void insertRows(int index, int numberOfRows) {
  1493. super.insertRows(index, numberOfRows);
  1494. recalculateElementSizes();
  1495. }
  1496. @Override
  1497. public void removeRows(int index, int numberOfRows) {
  1498. super.removeRows(index, numberOfRows);
  1499. recalculateElementSizes();
  1500. }
  1501. @Override
  1502. protected void reapplyDefaultRowHeights() {
  1503. if (root.getChildCount() == 0) {
  1504. return;
  1505. }
  1506. Profiler.enter("Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
  1507. Element tr = root.getFirstChildElement().cast();
  1508. while (tr != null) {
  1509. reapplyRowHeight(tr, getDefaultRowHeight());
  1510. tr = tr.getNextSiblingElement();
  1511. }
  1512. /*
  1513. * Because all rows are immediately displayed in the static row
  1514. * containers, the section's overall height has most probably
  1515. * changed.
  1516. */
  1517. recalculateSectionHeight();
  1518. Profiler.leave("Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
  1519. }
  1520. @Override
  1521. protected void recalculateSectionHeight() {
  1522. Profiler.enter("Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
  1523. int newHeight = calculateEstimatedTotalRowHeight();
  1524. if (newHeight != heightOfSection) {
  1525. heightOfSection = newHeight;
  1526. sectionHeightCalculated();
  1527. body.verifyEscalatorCount();
  1528. }
  1529. Profiler.leave("Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
  1530. }
  1531. /**
  1532. * Informs the row container that the height of its respective table
  1533. * section has changed.
  1534. * <p>
  1535. * These calculations might affect some layouting logic, such as the
  1536. * body is being offset by the footer, the footer needs to be readjusted
  1537. * according to its height, and so on.
  1538. * <p>
  1539. * A table section is either header, body or footer.
  1540. */
  1541. protected abstract void sectionHeightCalculated();
  1542. }
  1543. private class HeaderRowContainer extends AbstractStaticRowContainer {
  1544. public HeaderRowContainer(final Element headElement) {
  1545. super(headElement);
  1546. }
  1547. @Override
  1548. protected void sectionHeightCalculated() {
  1549. bodyElem.getStyle().setMarginTop(heightOfSection, Unit.PX);
  1550. verticalScrollbar.getElement().getStyle()
  1551. .setTop(heightOfSection, Unit.PX);
  1552. }
  1553. @Override
  1554. protected String getCellElementTagName() {
  1555. return "th";
  1556. }
  1557. @Override
  1558. public void setStylePrimaryName(String primaryStyleName) {
  1559. super.setStylePrimaryName(primaryStyleName);
  1560. UIObject.setStylePrimaryName(root, primaryStyleName + "-header");
  1561. }
  1562. }
  1563. private class FooterRowContainer extends AbstractStaticRowContainer {
  1564. public FooterRowContainer(final Element footElement) {
  1565. super(footElement);
  1566. }
  1567. @Override
  1568. public void setStylePrimaryName(String primaryStyleName) {
  1569. super.setStylePrimaryName(primaryStyleName);
  1570. UIObject.setStylePrimaryName(root, primaryStyleName + "-footer");
  1571. }
  1572. @Override
  1573. protected String getCellElementTagName() {
  1574. return "td";
  1575. }
  1576. @Override
  1577. protected void sectionHeightCalculated() {
  1578. int vscrollHeight = (int) Math.floor(heightOfEscalator
  1579. - header.heightOfSection - footer.heightOfSection);
  1580. final boolean horizontalScrollbarNeeded = columnConfiguration
  1581. .calculateRowWidth() > widthOfEscalator;
  1582. if (horizontalScrollbarNeeded) {
  1583. vscrollHeight -= horizontalScrollbar.getScrollbarThickness();
  1584. }
  1585. verticalScrollbar.setOffsetSize(vscrollHeight);
  1586. }
  1587. }
  1588. private class BodyRowContainer extends AbstractRowContainer {
  1589. /*
  1590. * TODO [[optimize]]: check whether a native JsArray might be faster
  1591. * than LinkedList
  1592. */
  1593. /**
  1594. * The order in which row elements are rendered visually in the browser,
  1595. * with the help of CSS tricks. Usually has nothing to do with the DOM
  1596. * order.
  1597. */
  1598. private final LinkedList<Element> visualRowOrder = new LinkedList<Element>();
  1599. /**
  1600. * The logical index of the topmost row.
  1601. *
  1602. * @deprecated Use the accessors {@link #setTopRowLogicalIndex(int)},
  1603. * {@link #updateTopRowLogicalIndex(int)} and
  1604. * {@link #getTopRowLogicalIndex()} instead
  1605. */
  1606. @Deprecated
  1607. private int topRowLogicalIndex = 0;
  1608. private void setTopRowLogicalIndex(int topRowLogicalIndex) {
  1609. if (LogConfiguration.loggingIsEnabled(Level.INFO)) {
  1610. Logger.getLogger("Escalator.BodyRowContainer").fine(
  1611. "topRowLogicalIndex: " + this.topRowLogicalIndex
  1612. + " -> " + topRowLogicalIndex);
  1613. }
  1614. assert topRowLogicalIndex >= 0 : "topRowLogicalIndex became negative";
  1615. /*
  1616. * if there's a smart way of evaluating and asserting the max index,
  1617. * this would be a nice place to put it. I haven't found out an
  1618. * effective and generic solution.
  1619. */
  1620. this.topRowLogicalIndex = topRowLogicalIndex;
  1621. }
  1622. private int getTopRowLogicalIndex() {
  1623. return topRowLogicalIndex;
  1624. }
  1625. private void updateTopRowLogicalIndex(int diff) {
  1626. setTopRowLogicalIndex(topRowLogicalIndex + diff);
  1627. }
  1628. public BodyRowContainer(final Element bodyElement) {
  1629. super(bodyElement);
  1630. }
  1631. @Override
  1632. public void setStylePrimaryName(String primaryStyleName) {
  1633. super.setStylePrimaryName(primaryStyleName);
  1634. UIObject.setStylePrimaryName(root, primaryStyleName + "-body");
  1635. }
  1636. public void updateEscalatorRowsOnScroll() {
  1637. if (visualRowOrder.isEmpty()) {
  1638. return;
  1639. }
  1640. boolean rowsWereMoved = false;
  1641. final int topRowPos = getRowTop(visualRowOrder.getFirst());
  1642. // TODO [[mpixscroll]]
  1643. final int scrollTop = tBodyScrollTop;
  1644. final int viewportOffset = topRowPos - scrollTop;
  1645. /*
  1646. * TODO [[optimize]] this if-else can most probably be refactored
  1647. * into a neater block of code
  1648. */
  1649. if (viewportOffset > 0) {
  1650. // there's empty room on top
  1651. /*
  1652. * FIXME [[rowheight]]: coded to work only with default row
  1653. * heights - will not work with variable row heights
  1654. */
  1655. int originalRowsToMove = (int) Math.ceil(viewportOffset
  1656. / (double) getDefaultRowHeight());
  1657. int rowsToMove = Math.min(originalRowsToMove,
  1658. root.getChildCount());
  1659. final int end = root.getChildCount();
  1660. final int start = end - rowsToMove;
  1661. /*
  1662. * FIXME [[rowheight]]: coded to work only with default row
  1663. * heights - will not work with variable row heights
  1664. */
  1665. final int logicalRowIndex = scrollTop / getDefaultRowHeight();
  1666. moveAndUpdateEscalatorRows(Range.between(start, end), 0,
  1667. logicalRowIndex);
  1668. updateTopRowLogicalIndex(-originalRowsToMove);
  1669. }
  1670. else if (viewportOffset + getDefaultRowHeight() <= 0) {
  1671. /*
  1672. * FIXME [[rowheight]]: coded to work only with default row
  1673. * heights - will not work with variable row heights
  1674. */
  1675. /*
  1676. * the viewport has been scrolled more than the topmost visual
  1677. * row.
  1678. */
  1679. /*
  1680. * Using the fact that integer division has implicit
  1681. * floor-function to our advantage here.
  1682. */
  1683. int originalRowsToMove = Math.abs(viewportOffset
  1684. / getDefaultRowHeight());
  1685. int rowsToMove = Math.min(originalRowsToMove,
  1686. root.getChildCount());
  1687. int logicalRowIndex;
  1688. if (rowsToMove < root.getChildCount()) {
  1689. /*
  1690. * We scroll so little that we can just keep adding the rows
  1691. * below the current escalator
  1692. */
  1693. logicalRowIndex = getLogicalRowIndex(visualRowOrder
  1694. .getLast()) + 1;
  1695. } else {
  1696. /*
  1697. * FIXME [[rowheight]]: coded to work only with default row
  1698. * heights - will not work with variable row heights
  1699. */
  1700. /*
  1701. * Since we're moving all escalator rows, we need to
  1702. * calculate the first logical row index from the scroll
  1703. * position.
  1704. */
  1705. logicalRowIndex = scrollTop / getDefaultRowHeight();
  1706. }
  1707. /*
  1708. * Since we're moving the viewport downwards, the visual index
  1709. * is always at the bottom. Note: Due to how
  1710. * moveAndUpdateEscalatorRows works, this will work out even if
  1711. * we move all the rows, and try to place them "at the end".
  1712. */
  1713. final int targetVisualIndex = root.getChildCount();
  1714. // make sure that we don't move rows over the data boundary
  1715. boolean aRowWasLeftBehind = false;
  1716. if (logicalRowIndex + rowsToMove > getRowCount()) {
  1717. /*
  1718. * TODO [[rowheight]]: with constant row heights, there's
  1719. * always exactly one row that will be moved beyond the data
  1720. * source, when viewport is scrolled to the end. This,
  1721. * however, isn't guaranteed anymore once row heights start
  1722. * varying.
  1723. */
  1724. rowsToMove--;
  1725. aRowWasLeftBehind = true;
  1726. }
  1727. moveAndUpdateEscalatorRows(Range.between(0, rowsToMove),
  1728. targetVisualIndex, logicalRowIndex);
  1729. if (aRowWasLeftBehind) {
  1730. /*
  1731. * To keep visualRowOrder as a spatially contiguous block of
  1732. * rows, let's make sure that the one row we didn't move
  1733. * visually still stays with the pack.
  1734. */
  1735. final Range strayRow = Range.withOnly(0);
  1736. /*
  1737. * We cannot trust getLogicalRowIndex, because it hasn't yet
  1738. * been updated. But since we're leaving rows behind, it
  1739. * means we've scrolled to the bottom. So, instead, we
  1740. * simply count backwards from the end.
  1741. */
  1742. final int topLogicalIndex = getRowCount()
  1743. - visualRowOrder.size();
  1744. moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex);
  1745. }
  1746. final int naiveNewLogicalIndex = getTopRowLogicalIndex()
  1747. + originalRowsToMove;
  1748. final int maxLogicalIndex = getRowCount()
  1749. - visualRowOrder.size();
  1750. setTopRowLogicalIndex(Math.min(naiveNewLogicalIndex,
  1751. maxLogicalIndex));
  1752. }
  1753. if (rowsWereMoved) {
  1754. fireRowVisibilityChangeEvent();
  1755. }
  1756. }
  1757. @Override
  1758. protected List<Element> paintInsertRows(final int index,
  1759. final int numberOfRows) {
  1760. if (numberOfRows == 0) {
  1761. return Collections.emptyList();
  1762. }
  1763. /*
  1764. * TODO: this method should probably only add physical rows, and not
  1765. * populate them - let everything be populated as appropriate by the
  1766. * logic that follows.
  1767. *
  1768. * This also would lead to the fact that paintInsertRows wouldn't
  1769. * need to return anything.
  1770. */
  1771. final List<Element> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
  1772. index, numberOfRows);
  1773. /*
  1774. * insertRows will always change the number of rows - update the
  1775. * scrollbar sizes.
  1776. */
  1777. scroller.recalculateScrollbarsForVirtualViewport();
  1778. /*
  1779. * FIXME [[rowheight]]: coded to work only with default row heights
  1780. * - will not work with variable row heights
  1781. */
  1782. final boolean addedRowsAboveCurrentViewport = index
  1783. * getDefaultRowHeight() < getScrollTop();
  1784. final boolean addedRowsBelowCurrentViewport = index
  1785. * getDefaultRowHeight() > getScrollTop()
  1786. + calculateHeight();
  1787. if (addedRowsAboveCurrentViewport) {
  1788. /*
  1789. * We need to tweak the virtual viewport (scroll handle
  1790. * positions, table "scroll position" and row locations), but
  1791. * without re-evaluating any rows.
  1792. */
  1793. /*
  1794. * FIXME [[rowheight]]: coded to work only with default row
  1795. * heights - will not work with variable row heights
  1796. */
  1797. final int yDelta = numberOfRows * getDefaultRowHeight();
  1798. adjustScrollPosIgnoreEvents(yDelta);
  1799. updateTopRowLogicalIndex(numberOfRows);
  1800. }
  1801. else if (addedRowsBelowCurrentViewport) {
  1802. // NOOP, we already recalculated scrollbars.
  1803. }
  1804. else { // some rows were added inside the current viewport
  1805. final int unupdatedLogicalStart = index + addedRows.size();
  1806. final int visualOffset = getLogicalRowIndex(visualRowOrder
  1807. .getFirst());
  1808. /*
  1809. * At this point, we have added new escalator rows, if so
  1810. * needed.
  1811. *
  1812. * If more rows were added than the new escalator rows can
  1813. * account for, we need to start to spin the escalator to update
  1814. * the remaining rows aswell.
  1815. */
  1816. final int rowsStillNeeded = numberOfRows - addedRows.size();
  1817. final Range unupdatedVisual = convertToVisual(Range.withLength(
  1818. unupdatedLogicalStart, rowsStillNeeded));
  1819. final int end = root.getChildCount();
  1820. final int start = end - unupdatedVisual.length();
  1821. final int visualTargetIndex = unupdatedLogicalStart
  1822. - visualOffset;
  1823. moveAndUpdateEscalatorRows(Range.between(start, end),
  1824. visualTargetIndex, unupdatedLogicalStart);
  1825. /*
  1826. * FIXME [[rowheight]]: coded to work only with default row
  1827. * heights - will not work with variable row heights
  1828. */
  1829. // move the surrounding rows to their correct places.
  1830. int rowTop = (unupdatedLogicalStart + (end - start))
  1831. * getDefaultRowHeight();
  1832. final ListIterator<Element> i = visualRowOrder
  1833. .listIterator(visualTargetIndex + (end - start));
  1834. while (i.hasNext()) {
  1835. final Element tr = i.next();
  1836. setRowPosition(tr, 0, rowTop);
  1837. /*
  1838. * FIXME [[rowheight]]: coded to work only with default row
  1839. * heights - will not work with variable row heights
  1840. */
  1841. rowTop += getDefaultRowHeight();
  1842. }
  1843. fireRowVisibilityChangeEvent();
  1844. }
  1845. return addedRows;
  1846. }
  1847. /**
  1848. * Move escalator rows around, and make sure everything gets
  1849. * appropriately repositioned and repainted.
  1850. *
  1851. * @param visualSourceRange
  1852. * the range of rows to move to a new place
  1853. * @param visualTargetIndex
  1854. * the visual index where the rows will be placed to
  1855. * @param logicalTargetIndex
  1856. * the logical index to be assigned to the first moved row
  1857. * @throws IllegalArgumentException
  1858. * if any of <code>visualSourceRange.getStart()</code>,
  1859. * <code>visualTargetIndex</code> or
  1860. * <code>logicalTargetIndex</code> is a negative number; or
  1861. * if <code>visualTargetInfo</code> is greater than the
  1862. * number of escalator rows.
  1863. */
  1864. private void moveAndUpdateEscalatorRows(final Range visualSourceRange,
  1865. final int visualTargetIndex, final int logicalTargetIndex)
  1866. throws IllegalArgumentException {
  1867. if (visualSourceRange.isEmpty()) {
  1868. return;
  1869. }
  1870. if (visualSourceRange.getStart() < 0) {
  1871. throw new IllegalArgumentException(
  1872. "Logical source start must be 0 or greater (was "
  1873. + visualSourceRange.getStart() + ")");
  1874. } else if (logicalTargetIndex < 0) {
  1875. throw new IllegalArgumentException(
  1876. "Logical target must be 0 or greater");
  1877. } else if (visualTargetIndex < 0) {
  1878. throw new IllegalArgumentException(
  1879. "Visual target must be 0 or greater");
  1880. } else if (visualTargetIndex > root.getChildCount()) {
  1881. throw new IllegalArgumentException(
  1882. "Visual target must not be greater than the number of escalator rows");
  1883. } else if (logicalTargetIndex + visualSourceRange.length() > getRowCount()) {
  1884. final int logicalEndIndex = logicalTargetIndex
  1885. + visualSourceRange.length() - 1;
  1886. throw new IllegalArgumentException(
  1887. "Logical target leads to rows outside of the data range ("
  1888. + logicalTargetIndex + ".." + logicalEndIndex
  1889. + ")");
  1890. }
  1891. /*
  1892. * Since we move a range into another range, the indices might move
  1893. * about. Having 10 rows, if we move 0..1 to index 10 (to the end of
  1894. * the collection), the target range will end up being 8..9, instead
  1895. * of 10..11.
  1896. *
  1897. * This applies only if we move elements forward in the collection,
  1898. * not backward.
  1899. */
  1900. final int adjustedVisualTargetIndex;
  1901. if (visualSourceRange.getStart() < visualTargetIndex) {
  1902. adjustedVisualTargetIndex = visualTargetIndex
  1903. - visualSourceRange.length();
  1904. } else {
  1905. adjustedVisualTargetIndex = visualTargetIndex;
  1906. }
  1907. if (visualSourceRange.getStart() != adjustedVisualTargetIndex) {
  1908. /*
  1909. * Reorder the rows to their correct places within
  1910. * visualRowOrder (unless rows are moved back to their original
  1911. * places)
  1912. */
  1913. /*
  1914. * TODO [[optimize]]: move whichever set is smaller: the ones
  1915. * explicitly moved, or the others. So, with 10 escalator rows,
  1916. * if we are asked to move idx[0..8] to the end of the list,
  1917. * it's faster to just move idx[9] to the beginning.
  1918. */
  1919. final List<Element> removedRows = new ArrayList<Element>(
  1920. visualSourceRange.length());
  1921. for (int i = 0; i < visualSourceRange.length(); i++) {
  1922. final Element tr = visualRowOrder.remove(visualSourceRange
  1923. .getStart());
  1924. removedRows.add(tr);
  1925. }
  1926. visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows);
  1927. }
  1928. { // Refresh the contents of the affected rows
  1929. final ListIterator<Element> iter = visualRowOrder
  1930. .listIterator(adjustedVisualTargetIndex);
  1931. for (int logicalIndex = logicalTargetIndex; logicalIndex < logicalTargetIndex
  1932. + visualSourceRange.length(); logicalIndex++) {
  1933. final Element tr = iter.next();
  1934. refreshRow(tr, logicalIndex);
  1935. }
  1936. }
  1937. { // Reposition the rows that were moved
  1938. /*
  1939. * FIXME [[rowheight]]: coded to work only with default row
  1940. * heights - will not work with variable row heights
  1941. */
  1942. int newRowTop = logicalTargetIndex * getDefaultRowHeight();
  1943. final ListIterator<Element> iter = visualRowOrder
  1944. .listIterator(adjustedVisualTargetIndex);
  1945. for (int i = 0; i < visualSourceRange.length(); i++) {
  1946. final Element tr = iter.next();
  1947. setRowPosition(tr, 0, newRowTop);
  1948. /*
  1949. * FIXME [[rowheight]]: coded to work only with default row
  1950. * heights - will not work with variable row heights
  1951. */
  1952. newRowTop += getDefaultRowHeight();
  1953. }
  1954. }
  1955. }
  1956. /**
  1957. * Adjust the scroll position without having the scroll handler have any
  1958. * side-effects.
  1959. * <p>
  1960. * <em>Note:</em> {@link Scroller#onScroll(double, double)}
  1961. * <em>will</em> be triggered, but it will not do anything, with the
  1962. * help of {@link Escalator#internalScrollEventCalls}.
  1963. *
  1964. * @param yDelta
  1965. * the delta of pixels to scrolls. A positive value moves the
  1966. * viewport downwards, while a negative value moves the
  1967. * viewport upwards
  1968. */
  1969. public void adjustScrollPosIgnoreEvents(final int yDelta) {
  1970. if (yDelta == 0) {
  1971. return;
  1972. }
  1973. internalScrollEventCalls++;
  1974. verticalScrollbar.setScrollPosByDelta(yDelta);
  1975. /*
  1976. * FIXME [[rowheight]]: coded to work only with default row heights
  1977. * - will not work with variable row heights
  1978. */
  1979. final int rowTopPos = yDelta - yDelta % getDefaultRowHeight();
  1980. for (final Element tr : visualRowOrder) {
  1981. setRowPosition(tr, 0, getRowTop(tr) + rowTopPos);
  1982. }
  1983. setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + yDelta);
  1984. }
  1985. /**
  1986. * Adds new physical escalator rows to the DOM at the given index if
  1987. * there's still a need for more escalator rows.
  1988. * <p>
  1989. * If Escalator already is at (or beyond) max capacity, this method does
  1990. * nothing to the DOM.
  1991. *
  1992. * @param index
  1993. * the index at which to add new escalator rows.
  1994. * <em>Note:</em>It is assumed that the index is both the
  1995. * visual index and the logical index.
  1996. * @param numberOfRows
  1997. * the number of rows to add at <code>index</code>
  1998. * @return a list of the added rows
  1999. */
  2000. private List<Element> fillAndPopulateEscalatorRowsIfNeeded(
  2001. final int index, final int numberOfRows) {
  2002. final int escalatorRowsStillFit = getMaxEscalatorRowCapacity()
  2003. - root.getChildCount();
  2004. final int escalatorRowsNeeded = Math.min(numberOfRows,
  2005. escalatorRowsStillFit);
  2006. if (escalatorRowsNeeded > 0) {
  2007. final List<Element> addedRows = super.paintInsertRows(index,
  2008. escalatorRowsNeeded);
  2009. visualRowOrder.addAll(index, addedRows);
  2010. /*
  2011. * We need to figure out the top positions for the rows we just
  2012. * added.
  2013. */
  2014. for (int i = 0; i < addedRows.size(); i++) {
  2015. /*
  2016. * FIXME [[rowheight]]: coded to work only with default row
  2017. * heights - will not work with variable row heights
  2018. */
  2019. setRowPosition(addedRows.get(i), 0, (index + i)
  2020. * getDefaultRowHeight());
  2021. }
  2022. /* Move the other rows away from above the added escalator rows */
  2023. for (int i = index + addedRows.size(); i < visualRowOrder
  2024. .size(); i++) {
  2025. final Element tr = visualRowOrder.get(i);
  2026. /*
  2027. * FIXME [[rowheight]]: coded to work only with default row
  2028. * heights - will not work with variable row heights
  2029. */
  2030. setRowPosition(tr, 0, i * getDefaultRowHeight());
  2031. }
  2032. return addedRows;
  2033. } else {
  2034. return new ArrayList<Element>();
  2035. }
  2036. }
  2037. private int getMaxEscalatorRowCapacity() {
  2038. /*
  2039. * FIXME [[rowheight]]: coded to work only with default row heights
  2040. * - will not work with variable row heights
  2041. */
  2042. final int maxEscalatorRowCapacity = (int) Math
  2043. .ceil(calculateHeight() / getDefaultRowHeight()) + 1;
  2044. /*
  2045. * maxEscalatorRowCapacity can become negative if the headers and
  2046. * footers start to overlap. This is a crazy situation, but Vaadin
  2047. * blinks the components a lot, so it's feasible.
  2048. */
  2049. return Math.max(0, maxEscalatorRowCapacity);
  2050. }
  2051. @Override
  2052. protected void paintRemoveRows(final int index, final int numberOfRows) {
  2053. final Range viewportRange = Range.withLength(
  2054. getLogicalRowIndex(visualRowOrder.getFirst()),
  2055. visualRowOrder.size());
  2056. final Range removedRowsRange = Range
  2057. .withLength(index, numberOfRows);
  2058. final Range[] partitions = removedRowsRange
  2059. .partitionWith(viewportRange);
  2060. final Range removedAbove = partitions[0];
  2061. final Range removedLogicalInside = partitions[1];
  2062. final Range removedVisualInside = convertToVisual(removedLogicalInside);
  2063. /*
  2064. * TODO: extract the following if-block to a separate method. I'll
  2065. * leave this be inlined for now, to make linediff-based code
  2066. * reviewing easier. Probably will be moved in the following patch
  2067. * set.
  2068. */
  2069. /*
  2070. * Adjust scroll position in one of two scenarios:
  2071. *
  2072. * 1) Rows were removed above. Then we just need to adjust the
  2073. * scrollbar by the height of the removed rows.
  2074. *
  2075. * 2) There are no logical rows above, and at least the first (if
  2076. * not more) visual row is removed. Then we need to snap the scroll
  2077. * position to the first visible row (i.e. reset scroll position to
  2078. * absolute 0)
  2079. *
  2080. * The logic is optimized in such a way that the
  2081. * adjustScrollPosIgnoreEvents is called only once, to avoid extra
  2082. * reflows, and thus the code might seem a bit obscure.
  2083. */
  2084. final boolean firstVisualRowIsRemoved = !removedVisualInside
  2085. .isEmpty() && removedVisualInside.getStart() == 0;
  2086. if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) {
  2087. /*
  2088. * FIXME [[rowheight]]: coded to work only with default row
  2089. * heights - will not work with variable row heights
  2090. */
  2091. final int yDelta = removedAbove.length()
  2092. * getDefaultRowHeight();
  2093. final int firstLogicalRowHeight = getDefaultRowHeight();
  2094. final boolean removalScrollsToShowFirstLogicalRow = verticalScrollbar
  2095. .getScrollPos() - yDelta < firstLogicalRowHeight;
  2096. if (removedVisualInside.isEmpty()
  2097. && (!removalScrollsToShowFirstLogicalRow || !firstVisualRowIsRemoved)) {
  2098. /*
  2099. * rows were removed from above the viewport, so all we need
  2100. * to do is to adjust the scroll position to account for the
  2101. * removed rows
  2102. */
  2103. adjustScrollPosIgnoreEvents(-yDelta);
  2104. } else if (removalScrollsToShowFirstLogicalRow) {
  2105. /*
  2106. * It seems like we've removed all rows from above, and also
  2107. * into the current viewport. This means we'll need to even
  2108. * out the scroll position to exactly 0 (i.e. adjust by the
  2109. * current negative scrolltop, presto!), so that it isn't
  2110. * aligned funnily
  2111. */
  2112. adjustScrollPosIgnoreEvents(-verticalScrollbar
  2113. .getScrollPos());
  2114. }
  2115. }
  2116. // ranges evaluated, let's do things.
  2117. if (!removedVisualInside.isEmpty()) {
  2118. int escalatorRowCount = bodyElem.getChildCount();
  2119. /*
  2120. * If we're left with less rows than the number of escalators,
  2121. * remove the unused ones.
  2122. */
  2123. final int escalatorRowsToRemove = escalatorRowCount
  2124. - getRowCount();
  2125. if (escalatorRowsToRemove > 0) {
  2126. for (int i = 0; i < escalatorRowsToRemove; i++) {
  2127. final Element tr = visualRowOrder
  2128. .remove(removedVisualInside.getStart());
  2129. for (int c = 0; c < tr.getChildCount(); c++) {
  2130. detachPossibleWidgetFromCell((Element) tr.getChild(
  2131. c).cast());
  2132. }
  2133. tr.removeFromParent();
  2134. removeRowPosition(tr);
  2135. }
  2136. escalatorRowCount -= escalatorRowsToRemove;
  2137. /*
  2138. * Because we're removing escalator rows, we don't have
  2139. * anything to scroll by. Let's make sure the viewport is
  2140. * scrolled to top, to render any rows possibly left above.
  2141. */
  2142. body.setBodyScrollPosition(tBodyScrollLeft, 0);
  2143. /*
  2144. * We might have removed some rows from the middle, so let's
  2145. * make sure we're not left with any holes. Also remember:
  2146. * visualIndex == logicalIndex applies now.
  2147. */
  2148. final int dirtyRowsStart = removedLogicalInside.getStart();
  2149. for (int i = dirtyRowsStart; i < escalatorRowCount; i++) {
  2150. final Element tr = visualRowOrder.get(i);
  2151. /*
  2152. * FIXME [[rowheight]]: coded to work only with default
  2153. * row heights - will not work with variable row heights
  2154. */
  2155. setRowPosition(tr, 0, i * getDefaultRowHeight());
  2156. }
  2157. /*
  2158. * this is how many rows appeared into the viewport from
  2159. * below
  2160. */
  2161. final int rowsToUpdateDataOn = numberOfRows
  2162. - escalatorRowsToRemove;
  2163. final int start = Math.max(0, escalatorRowCount
  2164. - rowsToUpdateDataOn);
  2165. final int end = escalatorRowCount;
  2166. for (int i = start; i < end; i++) {
  2167. final Element tr = visualRowOrder.get(i);
  2168. refreshRow(tr, i);
  2169. }
  2170. }
  2171. else {
  2172. // No escalator rows need to be removed.
  2173. /*
  2174. * Two things (or a combination thereof) can happen:
  2175. *
  2176. * 1) We're scrolled to the bottom, the last rows are
  2177. * removed. SOLUTION: moveAndUpdateEscalatorRows the
  2178. * bottommost rows, and place them at the top to be
  2179. * refreshed.
  2180. *
  2181. * 2) We're scrolled somewhere in the middle, arbitrary rows
  2182. * are removed. SOLUTION: moveAndUpdateEscalatorRows the
  2183. * removed rows, and place them at the bottom to be
  2184. * refreshed.
  2185. *
  2186. * Since a combination can also happen, we need to handle
  2187. * this in a smart way, all while avoiding
  2188. * double-refreshing.
  2189. */
  2190. /*
  2191. * FIXME [[rowheight]]: coded to work only with default row
  2192. * heights - will not work with variable row heights
  2193. */
  2194. final int contentBottom = getRowCount()
  2195. * getDefaultRowHeight();
  2196. final int viewportBottom = (int) (tBodyScrollTop + calculateHeight());
  2197. if (viewportBottom <= contentBottom) {
  2198. /*
  2199. * We're in the middle of the row container, everything
  2200. * is added to the bottom
  2201. */
  2202. paintRemoveRowsAtMiddle(removedLogicalInside,
  2203. removedVisualInside, 0);
  2204. }
  2205. else if (contentBottom
  2206. + (numberOfRows * getDefaultRowHeight())
  2207. - viewportBottom < getDefaultRowHeight()) {
  2208. /*
  2209. * FIXME [[rowheight]]: above coded to work only with
  2210. * default row heights - will not work with variable row
  2211. * heights
  2212. */
  2213. /*
  2214. * We're at the end of the row container, everything is
  2215. * added to the top.
  2216. */
  2217. paintRemoveRowsAtBottom(removedLogicalInside,
  2218. removedVisualInside);
  2219. updateTopRowLogicalIndex(-removedLogicalInside.length());
  2220. }
  2221. else {
  2222. /*
  2223. * We're in a combination, where we need to both scroll
  2224. * up AND show new rows at the bottom.
  2225. *
  2226. * Example: Scrolled down to show the second to last
  2227. * row. Remove two. Viewport scrolls up, revealing the
  2228. * row above row. The last element collapses up and into
  2229. * view.
  2230. *
  2231. * Reminder: this use case handles only the case when
  2232. * there are enough escalator rows to still render a
  2233. * full view. I.e. all escalator rows will _always_ be
  2234. * populated
  2235. */
  2236. /*-
  2237. * 1 1 |1| <- newly rendered
  2238. * |2| |2| |2|
  2239. * |3| ==> |*| ==> |5| <- newly rendered
  2240. * |4| |*|
  2241. * 5 5
  2242. *
  2243. * 1 1 |1| <- newly rendered
  2244. * |2| |*| |4|
  2245. * |3| ==> |*| ==> |5| <- newly rendered
  2246. * |4| |4|
  2247. * 5 5
  2248. */
  2249. /*
  2250. * STEP 1:
  2251. *
  2252. * reorganize deprecated escalator rows to bottom, but
  2253. * don't re-render anything yet
  2254. */
  2255. /*-
  2256. * 1 1 1
  2257. * |2| |*| |4|
  2258. * |3| ==> |*| ==> |*|
  2259. * |4| |4| |*|
  2260. * 5 5 5
  2261. */
  2262. int newTop = getRowTop(visualRowOrder
  2263. .get(removedVisualInside.getStart()));
  2264. for (int i = 0; i < removedVisualInside.length(); i++) {
  2265. final Element tr = visualRowOrder
  2266. .remove(removedVisualInside.getStart());
  2267. visualRowOrder.addLast(tr);
  2268. }
  2269. for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) {
  2270. final Element tr = visualRowOrder.get(i);
  2271. setRowPosition(tr, 0, newTop);
  2272. /*
  2273. * FIXME [[rowheight]]: coded to work only with
  2274. * default row heights - will not work with variable
  2275. * row heights
  2276. */
  2277. newTop += getDefaultRowHeight();
  2278. }
  2279. /*
  2280. * STEP 2:
  2281. *
  2282. * manually scroll
  2283. */
  2284. /*-
  2285. * 1 |1| <-- newly rendered (by scrolling)
  2286. * |4| |4|
  2287. * |*| ==> |*|
  2288. * |*|
  2289. * 5 5
  2290. */
  2291. final double newScrollTop = contentBottom
  2292. - calculateHeight();
  2293. setScrollTop(newScrollTop);
  2294. /*
  2295. * Manually call the scroll handler, so we get immediate
  2296. * effects in the escalator.
  2297. */
  2298. scroller.onScroll();
  2299. internalScrollEventCalls++;
  2300. /*
  2301. * Move the bottommost (n+1:th) escalator row to top,
  2302. * because scrolling up doesn't handle that for us
  2303. * automatically
  2304. */
  2305. moveAndUpdateEscalatorRows(
  2306. Range.withOnly(escalatorRowCount - 1),
  2307. 0,
  2308. getLogicalRowIndex(visualRowOrder.getFirst()) - 1);
  2309. updateTopRowLogicalIndex(-1);
  2310. /*
  2311. * STEP 3:
  2312. *
  2313. * update remaining escalator rows
  2314. */
  2315. /*-
  2316. * |1| |1|
  2317. * |4| ==> |4|
  2318. * |*| |5| <-- newly rendered
  2319. *
  2320. * 5
  2321. */
  2322. /*
  2323. * FIXME [[rowheight]]: coded to work only with default
  2324. * row heights - will not work with variable row heights
  2325. */
  2326. final int rowsScrolled = (int) (Math
  2327. .ceil((viewportBottom - (double) contentBottom)
  2328. / getDefaultRowHeight()));
  2329. final int start = escalatorRowCount
  2330. - (removedVisualInside.length() - rowsScrolled);
  2331. final Range visualRefreshRange = Range.between(start,
  2332. escalatorRowCount);
  2333. final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
  2334. .getFirst()) + start;
  2335. // in-place move simply re-renders the rows.
  2336. moveAndUpdateEscalatorRows(visualRefreshRange, start,
  2337. logicalTargetIndex);
  2338. }
  2339. }
  2340. }
  2341. updateTopRowLogicalIndex(-removedAbove.length());
  2342. /*
  2343. * this needs to be done after the escalator has been shrunk down,
  2344. * or it won't work correctly (due to setScrollTop invocation)
  2345. */
  2346. scroller.recalculateScrollbarsForVirtualViewport();
  2347. fireRowVisibilityChangeEvent();
  2348. }
  2349. private void paintRemoveRowsAtMiddle(final Range removedLogicalInside,
  2350. final Range removedVisualInside, final int logicalOffset) {
  2351. /*-
  2352. * : : :
  2353. * |2| |2| |2|
  2354. * |3| ==> |*| ==> |4|
  2355. * |4| |4| |6| <- newly rendered
  2356. * : : :
  2357. */
  2358. final int escalatorRowCount = visualRowOrder.size();
  2359. final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
  2360. .getLast())
  2361. - (removedVisualInside.length() - 1)
  2362. + logicalOffset;
  2363. moveAndUpdateEscalatorRows(removedVisualInside, escalatorRowCount,
  2364. logicalTargetIndex);
  2365. // move the surrounding rows to their correct places.
  2366. final ListIterator<Element> iterator = visualRowOrder
  2367. .listIterator(removedVisualInside.getStart());
  2368. /*
  2369. * FIXME [[rowheight]]: coded to work only with default row heights
  2370. * - will not work with variable row heights
  2371. */
  2372. int rowTop = (removedLogicalInside.getStart() + logicalOffset)
  2373. * getDefaultRowHeight();
  2374. for (int i = removedVisualInside.getStart(); i < escalatorRowCount
  2375. - removedVisualInside.length(); i++) {
  2376. final Element tr = iterator.next();
  2377. setRowPosition(tr, 0, rowTop);
  2378. /*
  2379. * FIXME [[rowheight]]: coded to work only with default row
  2380. * heights - will not work with variable row heights
  2381. */
  2382. rowTop += getDefaultRowHeight();
  2383. }
  2384. }
  2385. private void paintRemoveRowsAtBottom(final Range removedLogicalInside,
  2386. final Range removedVisualInside) {
  2387. /*-
  2388. * :
  2389. * : : |4| <- newly rendered
  2390. * |5| |5| |5|
  2391. * |6| ==> |*| ==> |7|
  2392. * |7| |7|
  2393. */
  2394. final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
  2395. .getFirst()) - removedVisualInside.length();
  2396. moveAndUpdateEscalatorRows(removedVisualInside, 0,
  2397. logicalTargetIndex);
  2398. // move the surrounding rows to their correct places.
  2399. final ListIterator<Element> iterator = visualRowOrder
  2400. .listIterator(removedVisualInside.getEnd());
  2401. /*
  2402. * FIXME [[rowheight]]: coded to work only with default row heights
  2403. * - will not work with variable row heights
  2404. */
  2405. int rowTop = removedLogicalInside.getStart()
  2406. * getDefaultRowHeight();
  2407. while (iterator.hasNext()) {
  2408. final Element tr = iterator.next();
  2409. setRowPosition(tr, 0, rowTop);
  2410. /*
  2411. * FIXME [[rowheight]]: coded to work only with default row
  2412. * heights - will not work with variable row heights
  2413. */
  2414. rowTop += getDefaultRowHeight();
  2415. }
  2416. }
  2417. private int getLogicalRowIndex(final Element element) {
  2418. assert element.getParentNode() == root : "The given element isn't a row element in the body";
  2419. int internalIndex = visualRowOrder.indexOf(element);
  2420. return getTopRowLogicalIndex() + internalIndex;
  2421. }
  2422. @Override
  2423. protected void recalculateSectionHeight() {
  2424. // NOOP for body, since it doesn't make any sense.
  2425. }
  2426. /**
  2427. * Adjusts the row index and number to be relevant for the current
  2428. * virtual viewport.
  2429. * <p>
  2430. * It converts a logical range of rows index to the matching visual
  2431. * range, truncating the resulting range with the viewport.
  2432. * <p>
  2433. * <ul>
  2434. * <li>Escalator contains logical rows 0..100
  2435. * <li>Current viewport showing logical rows 20..29
  2436. * <li>convertToVisual([20..29]) &rarr; [0..9]
  2437. * <li>convertToVisual([15..24]) &rarr; [0..4]
  2438. * <li>convertToVisual([25..29]) &rarr; [5..9]
  2439. * <li>convertToVisual([26..39]) &rarr; [6..9]
  2440. * <li>convertToVisual([0..5]) &rarr; [0..-1] <em>(empty)</em>
  2441. * <li>convertToVisual([35..1]) &rarr; [0..-1] <em>(empty)</em>
  2442. * <li>convertToVisual([0..100]) &rarr; [0..9]
  2443. * </ul>
  2444. *
  2445. * @return a logical range converted to a visual range, truncated to the
  2446. * current viewport. The first visual row has the index 0.
  2447. */
  2448. private Range convertToVisual(final Range logicalRange) {
  2449. if (logicalRange.isEmpty()) {
  2450. return logicalRange;
  2451. } else if (visualRowOrder.isEmpty()) {
  2452. // empty range
  2453. return Range.withLength(0, 0);
  2454. }
  2455. /*
  2456. * TODO [[rowheight]]: these assumptions will be totally broken with
  2457. * variable row heights.
  2458. */
  2459. final int maxEscalatorRows = getMaxEscalatorRowCapacity();
  2460. final int currentTopRowIndex = getLogicalRowIndex(visualRowOrder
  2461. .getFirst());
  2462. final Range[] partitions = logicalRange.partitionWith(Range
  2463. .withLength(currentTopRowIndex, maxEscalatorRows));
  2464. final Range insideRange = partitions[1];
  2465. return insideRange.offsetBy(-currentTopRowIndex);
  2466. }
  2467. @Override
  2468. protected String getCellElementTagName() {
  2469. return "td";
  2470. }
  2471. /**
  2472. * Calculates the height of the {@code <tbody>} as it should be rendered
  2473. * in the DOM.
  2474. */
  2475. private double calculateHeight() {
  2476. final int tableHeight = tableWrapper.getOffsetHeight();
  2477. final double footerHeight = footer.heightOfSection;
  2478. final double headerHeight = header.heightOfSection;
  2479. return tableHeight - footerHeight - headerHeight;
  2480. }
  2481. @Override
  2482. public void refreshRows(final int index, final int numberOfRows) {
  2483. Profiler.enter("Escalator.BodyRowContainer.refreshRows");
  2484. final Range visualRange = convertToVisual(Range.withLength(index,
  2485. numberOfRows));
  2486. if (!visualRange.isEmpty()) {
  2487. final int firstLogicalRowIndex = getLogicalRowIndex(visualRowOrder
  2488. .getFirst());
  2489. for (int rowNumber = visualRange.getStart(); rowNumber < visualRange
  2490. .getEnd(); rowNumber++) {
  2491. refreshRow(visualRowOrder.get(rowNumber),
  2492. firstLogicalRowIndex + rowNumber);
  2493. }
  2494. }
  2495. Profiler.leave("Escalator.BodyRowContainer.refreshRows");
  2496. }
  2497. @Override
  2498. protected Element getTrByVisualIndex(final int index)
  2499. throws IndexOutOfBoundsException {
  2500. if (index >= 0 && index < visualRowOrder.size()) {
  2501. return visualRowOrder.get(index);
  2502. } else {
  2503. throw new IndexOutOfBoundsException("No such visual index: "
  2504. + index);
  2505. }
  2506. }
  2507. private void setBodyScrollPosition(final int scrollLeft,
  2508. final int scrollTop) {
  2509. tBodyScrollLeft = scrollLeft;
  2510. tBodyScrollTop = scrollTop;
  2511. position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop);
  2512. }
  2513. /**
  2514. * Make sure that there is a correct amount of escalator rows: Add more
  2515. * if needed, or remove any superfluous ones.
  2516. * <p>
  2517. * This method should be called when e.g. the height of the Escalator
  2518. * changes.
  2519. * <p>
  2520. * <em>Note:</em> This method will make sure that the escalator rows are
  2521. * placed in the proper places. By default new rows are added below, but
  2522. * if the content is scrolled down, the rows are populated on top
  2523. * instead.
  2524. */
  2525. public void verifyEscalatorCount() {
  2526. /*
  2527. * This method indeed has a smell very similar to paintRemoveRows
  2528. * and paintInsertRows.
  2529. *
  2530. * Unfortunately, those the code can't trivially be shared, since
  2531. * there are some slight differences in the respective
  2532. * responsibilities. The "paint" methods fake the addition and
  2533. * removal of rows, and make sure to either push existing data out
  2534. * of view, or draw new data into view. Only in some special cases
  2535. * will the DOM element count change.
  2536. *
  2537. * This method, however, has the explicit responsibility to verify
  2538. * that when "something" happens, we still have the correct amount
  2539. * of escalator rows in the DOM, and if not, we make sure to modify
  2540. * that count. Only in some special cases do we need to take into
  2541. * account other things than simply modifying the DOM element count.
  2542. */
  2543. Profiler.enter("Escalator.BodyRowContainer.verifyEscalatorCount");
  2544. if (!isAttached()) {
  2545. return;
  2546. }
  2547. final int maxEscalatorRows = getMaxEscalatorRowCapacity();
  2548. final int neededEscalatorRows = Math.min(maxEscalatorRows,
  2549. body.getRowCount());
  2550. final int neededEscalatorRowsDiff = neededEscalatorRows
  2551. - visualRowOrder.size();
  2552. if (neededEscalatorRowsDiff > 0) {
  2553. // needs more
  2554. /*
  2555. * This is a workaround for the issue where we might be scrolled
  2556. * to the bottom, and the widget expands beyond the content
  2557. * range
  2558. */
  2559. final int index = visualRowOrder.size();
  2560. final int nextLastLogicalIndex;
  2561. if (!visualRowOrder.isEmpty()) {
  2562. nextLastLogicalIndex = getLogicalRowIndex(visualRowOrder
  2563. .getLast()) + 1;
  2564. } else {
  2565. nextLastLogicalIndex = 0;
  2566. }
  2567. final boolean contentWillFit = nextLastLogicalIndex < getRowCount()
  2568. - neededEscalatorRowsDiff;
  2569. if (contentWillFit) {
  2570. final List<Element> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
  2571. index, neededEscalatorRowsDiff);
  2572. /*
  2573. * Since fillAndPopulateEscalatorRowsIfNeeded operates on
  2574. * the assumption that index == visual index == logical
  2575. * index, we thank for the added escalator rows, but since
  2576. * they're painted in the wrong CSS position, we need to
  2577. * move them to their actual locations.
  2578. *
  2579. * Note: this is the second (see body.paintInsertRows)
  2580. * occasion where fillAndPopulateEscalatorRowsIfNeeded would
  2581. * behave "more correctly" if it only would add escalator
  2582. * rows to the DOM and appropriate bookkeping, and not
  2583. * actually populate them :/
  2584. */
  2585. moveAndUpdateEscalatorRows(
  2586. Range.withLength(index, addedRows.size()), index,
  2587. nextLastLogicalIndex);
  2588. } else {
  2589. /*
  2590. * TODO [[optimize]]
  2591. *
  2592. * We're scrolled so far down that all rows can't be simply
  2593. * appended at the end, since we might start displaying
  2594. * escalator rows that don't exist. To avoid the mess that
  2595. * is body.paintRemoveRows, this is a dirty hack that dumbs
  2596. * the problem down to a more basic and already-solved
  2597. * problem:
  2598. *
  2599. * 1) scroll all the way up 2) add the missing escalator
  2600. * rows 3) scroll back to the original position.
  2601. *
  2602. * Letting the browser scroll back to our original position
  2603. * will automatically solve any possible overflow problems,
  2604. * since the browser will not allow us to scroll beyond the
  2605. * actual content.
  2606. */
  2607. final double oldScrollTop = getScrollTop();
  2608. setScrollTop(0);
  2609. scroller.onScroll();
  2610. fillAndPopulateEscalatorRowsIfNeeded(index,
  2611. neededEscalatorRowsDiff);
  2612. setScrollTop(oldScrollTop);
  2613. scroller.onScroll();
  2614. internalScrollEventCalls++;
  2615. }
  2616. }
  2617. else if (neededEscalatorRowsDiff < 0) {
  2618. // needs less
  2619. final ListIterator<Element> iter = visualRowOrder
  2620. .listIterator(visualRowOrder.size());
  2621. for (int i = 0; i < -neededEscalatorRowsDiff; i++) {
  2622. final Element last = iter.previous();
  2623. for (int c = 0; c < last.getChildCount(); c++) {
  2624. detachPossibleWidgetFromCell((Element) last.getChild(c)
  2625. .cast());
  2626. }
  2627. last.removeFromParent();
  2628. iter.remove();
  2629. }
  2630. /*
  2631. * If we were scrolled to the bottom so that we didn't have an
  2632. * extra escalator row at the bottom, we'll probably end up with
  2633. * blank space at the bottom of the escalator, and one extra row
  2634. * above the header.
  2635. *
  2636. * Experimentation idea #1: calculate "scrollbottom" vs content
  2637. * bottom and remove one row from top, rest from bottom. This
  2638. * FAILED, since setHeight has already happened, thus we never
  2639. * will detect ourselves having been scrolled all the way to the
  2640. * bottom.
  2641. */
  2642. if (!visualRowOrder.isEmpty()) {
  2643. final int firstRowTop = getRowTop(visualRowOrder.getFirst());
  2644. /*
  2645. * FIXME [[rowheight]]: coded to work only with default row
  2646. * heights - will not work with variable row heights
  2647. */
  2648. final double firstRowMinTop = tBodyScrollTop
  2649. - getDefaultRowHeight();
  2650. if (firstRowTop < firstRowMinTop) {
  2651. final int newLogicalIndex = getLogicalRowIndex(visualRowOrder
  2652. .getLast()) + 1;
  2653. moveAndUpdateEscalatorRows(Range.withOnly(0),
  2654. visualRowOrder.size(), newLogicalIndex);
  2655. }
  2656. }
  2657. }
  2658. if (neededEscalatorRowsDiff != 0) {
  2659. fireRowVisibilityChangeEvent();
  2660. }
  2661. Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount");
  2662. }
  2663. @Override
  2664. protected void reapplyDefaultRowHeights() {
  2665. if (visualRowOrder.isEmpty()) {
  2666. return;
  2667. }
  2668. /*
  2669. * As an intermediate step between hard-coded row heights to crazily
  2670. * varying row heights, Escalator will support the modification of
  2671. * the default row height (which is applied to all rows).
  2672. *
  2673. * This allows us to do some assumptions and simplifications for
  2674. * now. This code is intended to be quite short-lived, but gives
  2675. * insight into what needs to be done when row heights change in the
  2676. * body, in a general sense.
  2677. *
  2678. * TODO [[rowheight]] remove this comment once row heights may
  2679. * genuinely vary.
  2680. */
  2681. Profiler.enter("Escalator.BodyRowContainer.reapplyDefaultRowHeights");
  2682. /* step 1: resize and reposition rows */
  2683. for (int i = 0; i < visualRowOrder.size(); i++) {
  2684. Element tr = visualRowOrder.get(i);
  2685. reapplyRowHeight(tr, getDefaultRowHeight());
  2686. final int logicalIndex = getTopRowLogicalIndex() + i;
  2687. setRowPosition(tr, 0, logicalIndex * getDefaultRowHeight());
  2688. }
  2689. /*
  2690. * step 2: move scrollbar so that it corresponds to its previous
  2691. * place
  2692. */
  2693. /*
  2694. * This ratio needs to be calculated with the scrollsize (not max
  2695. * scroll position) in order to align the top row with the new
  2696. * scroll position.
  2697. */
  2698. double scrollRatio = (double) verticalScrollbar.getScrollPos()
  2699. / (double) verticalScrollbar.getScrollSize();
  2700. scroller.recalculateScrollbarsForVirtualViewport();
  2701. internalScrollEventCalls++;
  2702. verticalScrollbar.setScrollPos((int) (getDefaultRowHeight()
  2703. * getRowCount() * scrollRatio));
  2704. setBodyScrollPosition(horizontalScrollbar.getScrollPos(),
  2705. verticalScrollbar.getScrollPos());
  2706. scroller.onScroll();
  2707. /* step 3: make sure we have the correct amount of escalator rows. */
  2708. verifyEscalatorCount();
  2709. /*
  2710. * TODO [[rowheight]] This simply doesn't work with variable rows
  2711. * heights.
  2712. */
  2713. setTopRowLogicalIndex(getRowTop(visualRowOrder.getFirst())
  2714. / getDefaultRowHeight());
  2715. Profiler.leave("Escalator.BodyRowContainer.reapplyDefaultRowHeights");
  2716. }
  2717. }
  2718. private class ColumnConfigurationImpl implements ColumnConfiguration {
  2719. public class Column {
  2720. private static final int DEFAULT_COLUMN_WIDTH_PX = 100;
  2721. private int definedWidth = -1;
  2722. private int calculatedWidth = DEFAULT_COLUMN_WIDTH_PX;
  2723. public void setWidth(int px) {
  2724. definedWidth = px;
  2725. calculatedWidth = (px >= 0) ? px : DEFAULT_COLUMN_WIDTH_PX;
  2726. }
  2727. public int getDefinedWidth() {
  2728. return definedWidth;
  2729. }
  2730. public int getCalculatedWidth() {
  2731. return calculatedWidth;
  2732. }
  2733. }
  2734. private final List<Column> columns = new ArrayList<Column>();
  2735. private int frozenColumns = 0;
  2736. /**
  2737. * A cached array of all the calculated column widths.
  2738. *
  2739. * @see #getCalculatedColumnWidths()
  2740. */
  2741. private int[] widthsArray = null;
  2742. /**
  2743. * {@inheritDoc}
  2744. * <p>
  2745. * <em>Implementation detail:</em> This method does no DOM modifications
  2746. * (i.e. is very cheap to call) if there are no rows in the DOM when
  2747. * this method is called.
  2748. *
  2749. * @see #hasSomethingInDom()
  2750. */
  2751. @Override
  2752. public void removeColumns(final int index, final int numberOfColumns) {
  2753. assertArgumentsAreValidAndWithinRange(index, numberOfColumns);
  2754. flyweightRow.removeCells(index, numberOfColumns);
  2755. // Cope with removing frozen columns
  2756. if (index < frozenColumns) {
  2757. if (index + numberOfColumns < frozenColumns) {
  2758. /*
  2759. * Last removed column was frozen, meaning that all removed
  2760. * columns were frozen. Just decrement the number of frozen
  2761. * columns accordingly.
  2762. */
  2763. frozenColumns -= numberOfColumns;
  2764. } else {
  2765. /*
  2766. * If last removed column was not frozen, we have removed
  2767. * columns beyond the frozen range, so all remaining frozen
  2768. * columns are to the left of the removed columns.
  2769. */
  2770. frozenColumns = index;
  2771. }
  2772. }
  2773. List<Column> removedColumns = new ArrayList<Column>();
  2774. for (int i = 0; i < numberOfColumns; i++) {
  2775. removedColumns.add(columns.remove(index));
  2776. }
  2777. if (hasSomethingInDom()) {
  2778. for (final AbstractRowContainer rowContainer : rowContainers) {
  2779. rowContainer.paintRemoveColumns(index, numberOfColumns,
  2780. removedColumns);
  2781. }
  2782. }
  2783. }
  2784. /**
  2785. * Calculate the width of a row, as the sum of columns' widths.
  2786. *
  2787. * @return the width of a row, in pixels
  2788. */
  2789. public int calculateRowWidth() {
  2790. return getCalculatedColumnsWidth(Range.between(0, getColumnCount()));
  2791. }
  2792. private void assertArgumentsAreValidAndWithinRange(final int index,
  2793. final int numberOfColumns) {
  2794. if (numberOfColumns < 1) {
  2795. throw new IllegalArgumentException(
  2796. "Number of columns can't be less than 1 (was "
  2797. + numberOfColumns + ")");
  2798. }
  2799. if (index < 0 || index + numberOfColumns > getColumnCount()) {
  2800. throw new IndexOutOfBoundsException("The given "
  2801. + "column range (" + index + ".."
  2802. + (index + numberOfColumns)
  2803. + ") was outside of the current "
  2804. + "number of columns (" + getColumnCount() + ")");
  2805. }
  2806. }
  2807. /**
  2808. * {@inheritDoc}
  2809. * <p>
  2810. * <em>Implementation detail:</em> This method does no DOM modifications
  2811. * (i.e. is very cheap to call) if there is no data for rows when this
  2812. * method is called.
  2813. *
  2814. * @see #hasColumnAndRowData()
  2815. */
  2816. @Override
  2817. public void insertColumns(final int index, final int numberOfColumns) {
  2818. if (index < 0 || index > getColumnCount()) {
  2819. throw new IndexOutOfBoundsException("The given index(" + index
  2820. + ") was outside of the current number of columns (0.."
  2821. + getColumnCount() + ")");
  2822. }
  2823. if (numberOfColumns < 1) {
  2824. throw new IllegalArgumentException(
  2825. "Number of columns must be 1 or greater (was "
  2826. + numberOfColumns);
  2827. }
  2828. flyweightRow.addCells(index, numberOfColumns);
  2829. for (int i = 0; i < numberOfColumns; i++) {
  2830. columns.add(index, new Column());
  2831. }
  2832. // Either all or none of the new columns are frozen
  2833. boolean frozen = index < frozenColumns;
  2834. if (frozen) {
  2835. frozenColumns += numberOfColumns;
  2836. }
  2837. if (hasColumnAndRowData()) {
  2838. for (final AbstractRowContainer rowContainer : rowContainers) {
  2839. rowContainer.paintInsertColumns(index, numberOfColumns,
  2840. frozen);
  2841. }
  2842. }
  2843. }
  2844. @Override
  2845. public int getColumnCount() {
  2846. return columns.size();
  2847. }
  2848. @Override
  2849. public void setFrozenColumnCount(int count)
  2850. throws IllegalArgumentException {
  2851. if (count < 0 || count > getColumnCount()) {
  2852. throw new IllegalArgumentException(
  2853. "count must be between 0 and the current number of columns ("
  2854. + columns + ")");
  2855. }
  2856. int oldCount = frozenColumns;
  2857. if (count == oldCount) {
  2858. return;
  2859. }
  2860. frozenColumns = count;
  2861. if (hasSomethingInDom()) {
  2862. // Are we freezing or unfreezing?
  2863. boolean frozen = count > oldCount;
  2864. int firstAffectedCol;
  2865. int firstUnaffectedCol;
  2866. if (frozen) {
  2867. firstAffectedCol = oldCount;
  2868. firstUnaffectedCol = count;
  2869. } else {
  2870. firstAffectedCol = count;
  2871. firstUnaffectedCol = oldCount;
  2872. }
  2873. for (int col = firstAffectedCol; col < firstUnaffectedCol; col++) {
  2874. header.setColumnFrozen(col, frozen);
  2875. body.setColumnFrozen(col, frozen);
  2876. footer.setColumnFrozen(col, frozen);
  2877. }
  2878. }
  2879. scroller.recalculateScrollbarsForVirtualViewport();
  2880. }
  2881. @Override
  2882. public int getFrozenColumnCount() {
  2883. return frozenColumns;
  2884. }
  2885. @Override
  2886. public void setColumnWidth(int index, int px)
  2887. throws IllegalArgumentException {
  2888. checkValidColumnIndex(index);
  2889. columns.get(index).setWidth(px);
  2890. widthsArray = null;
  2891. /*
  2892. * TODO [[optimize]]: only modify the elements that are actually
  2893. * modified.
  2894. */
  2895. header.reapplyColumnWidths();
  2896. body.reapplyColumnWidths();
  2897. footer.reapplyColumnWidths();
  2898. recalculateElementSizes();
  2899. }
  2900. private void checkValidColumnIndex(int index)
  2901. throws IllegalArgumentException {
  2902. if (!Range.withLength(0, getColumnCount()).contains(index)) {
  2903. throw new IllegalArgumentException("The given column index ("
  2904. + index + ") does not exist");
  2905. }
  2906. }
  2907. @Override
  2908. public int getColumnWidth(int index) throws IllegalArgumentException {
  2909. checkValidColumnIndex(index);
  2910. return columns.get(index).getDefinedWidth();
  2911. }
  2912. @Override
  2913. public int getColumnWidthActual(int index) {
  2914. return columns.get(index).getCalculatedWidth();
  2915. }
  2916. /**
  2917. * Calculates the width of the columns in a given range.
  2918. *
  2919. * @param columns
  2920. * the columns to calculate
  2921. * @return the total width of the columns in the given
  2922. * <code>columns</code>
  2923. */
  2924. int getCalculatedColumnsWidth(@SuppressWarnings("hiding")
  2925. final Range columns) {
  2926. /*
  2927. * This is an assert instead of an exception, since this is an
  2928. * internal method.
  2929. */
  2930. assert columns.isSubsetOf(Range.between(0, getColumnCount())) : "Range "
  2931. + "was outside of current column range (i.e.: "
  2932. + Range.between(0, getColumnCount())
  2933. + ", but was given :"
  2934. + columns;
  2935. int sum = 0;
  2936. for (int i = columns.getStart(); i < columns.getEnd(); i++) {
  2937. sum += getColumnWidthActual(i);
  2938. }
  2939. return sum;
  2940. }
  2941. void setCalculatedColumnWidth(int index, int width) {
  2942. columns.get(index).calculatedWidth = width;
  2943. widthsArray = null;
  2944. }
  2945. int[] getCalculatedColumnWidths() {
  2946. if (widthsArray == null || widthsArray.length != getColumnCount()) {
  2947. widthsArray = new int[getColumnCount()];
  2948. for (int i = 0; i < columns.size(); i++) {
  2949. widthsArray[i] = columns.get(i).getCalculatedWidth();
  2950. }
  2951. }
  2952. return widthsArray;
  2953. }
  2954. }
  2955. // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y
  2956. /**
  2957. * The solution to
  2958. * <code>|tan<sup>-1</sup>(<i>x</i>)|&times;(180/&pi;)&nbsp;=&nbsp;30</code>
  2959. * .
  2960. * <p>
  2961. * This constant is placed in the Escalator class, instead of an inner
  2962. * class, since even mathematical expressions aren't allowed in non-static
  2963. * inner classes for constants.
  2964. */
  2965. private static final double RATIO_OF_30_DEGREES = 1 / Math.sqrt(3);
  2966. /**
  2967. * The solution to
  2968. * <code>|tan<sup>-1</sup>(<i>x</i>)|&times;(180/&pi;)&nbsp;=&nbsp;40</code>
  2969. * .
  2970. * <p>
  2971. * This constant is placed in the Escalator class, instead of an inner
  2972. * class, since even mathematical expressions aren't allowed in non-static
  2973. * inner classes for constants.
  2974. */
  2975. private static final double RATIO_OF_40_DEGREES = Math.tan(2 * Math.PI / 9);
  2976. private static final String DEFAULT_WIDTH = "400.0px";
  2977. private static final String DEFAULT_HEIGHT = "400.0px";
  2978. private FlyweightRow flyweightRow = new FlyweightRow(this);
  2979. /** The {@code <thead/>} tag. */
  2980. private final Element headElem = DOM.createTHead();
  2981. /** The {@code <tbody/>} tag. */
  2982. private final Element bodyElem = DOM.createTBody();
  2983. /** The {@code <tfoot/>} tag. */
  2984. private final Element footElem = DOM.createTFoot();
  2985. /**
  2986. * TODO: investigate whether this field is now unnecessary, as
  2987. * {@link ScrollbarBundle} now caches its values.
  2988. *
  2989. * @deprecated maybe...
  2990. */
  2991. @Deprecated
  2992. private int tBodyScrollTop = 0;
  2993. /**
  2994. * TODO: investigate whether this field is now unnecessary, as
  2995. * {@link ScrollbarBundle} now caches its values.
  2996. *
  2997. * @deprecated maybe...
  2998. */
  2999. @Deprecated
  3000. private int tBodyScrollLeft = 0;
  3001. private final VerticalScrollbarBundle verticalScrollbar = new VerticalScrollbarBundle();
  3002. private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle();
  3003. private final HeaderRowContainer header = new HeaderRowContainer(headElem);
  3004. private final BodyRowContainer body = new BodyRowContainer(bodyElem);
  3005. private final FooterRowContainer footer = new FooterRowContainer(footElem);
  3006. private final Scroller scroller = new Scroller();
  3007. private final AbstractRowContainer[] rowContainers = new AbstractRowContainer[] {
  3008. header, body, footer };
  3009. private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl();
  3010. private final Element tableWrapper;
  3011. private PositionFunction position;
  3012. private int internalScrollEventCalls = 0;
  3013. /** The cached width of the escalator, in pixels. */
  3014. private double widthOfEscalator;
  3015. /** The cached height of the escalator, in pixels. */
  3016. private double heightOfEscalator;
  3017. private static native double getPreciseWidth(Element element)
  3018. /*-{
  3019. if (element.getBoundingClientRect) {
  3020. var rect = element.getBoundingClientRect();
  3021. return rect.right - rect.left;
  3022. } else {
  3023. return element.offsetWidth;
  3024. }
  3025. }-*/;
  3026. private static native double getPreciseHeight(Element element)
  3027. /*-{
  3028. if (element.getBoundingClientRect) {
  3029. var rect = element.getBoundingClientRect();
  3030. return rect.bottom - rect.top;
  3031. } else {
  3032. return element.offsetHeight;
  3033. }
  3034. }-*/;
  3035. /**
  3036. * Creates a new Escalator widget instance.
  3037. */
  3038. public Escalator() {
  3039. detectAndApplyPositionFunction();
  3040. getLogger().info(
  3041. "Using " + position.getClass().getSimpleName()
  3042. + " for position");
  3043. final Element root = DOM.createDiv();
  3044. setElement(root);
  3045. root.appendChild(verticalScrollbar.getElement());
  3046. root.appendChild(horizontalScrollbar.getElement());
  3047. verticalScrollbar.setScrollbarThickness(Util.getNativeScrollbarSize());
  3048. horizontalScrollbar
  3049. .setScrollbarThickness(Util.getNativeScrollbarSize());
  3050. tableWrapper = DOM.createDiv();
  3051. root.appendChild(tableWrapper);
  3052. final Element table = DOM.createTable();
  3053. tableWrapper.appendChild(table);
  3054. table.appendChild(headElem);
  3055. table.appendChild(bodyElem);
  3056. table.appendChild(footElem);
  3057. setStylePrimaryName("v-escalator");
  3058. // init default dimensions
  3059. setHeight(null);
  3060. setWidth(null);
  3061. }
  3062. @Override
  3063. protected void onLoad() {
  3064. super.onLoad();
  3065. header.paintInsertRows(0, header.getRowCount());
  3066. footer.paintInsertRows(0, footer.getRowCount());
  3067. recalculateElementSizes();
  3068. /*
  3069. * Note: There's no need to explicitly insert rows into the body.
  3070. *
  3071. * recalculateElementSizes will recalculate the height of the body. This
  3072. * has the side-effect that as the body's size grows bigger (i.e. from 0
  3073. * to its actual height), more escalator rows are populated. Those
  3074. * escalator rows are then immediately rendered. This, in effect, is the
  3075. * same thing as inserting those rows.
  3076. *
  3077. * In fact, having an extra paintInsertRows here would lead to duplicate
  3078. * rows.
  3079. */
  3080. scroller.attachScrollListener(verticalScrollbar.getElement());
  3081. scroller.attachScrollListener(horizontalScrollbar.getElement());
  3082. scroller.attachMousewheelListener(getElement());
  3083. scroller.attachTouchListeners(getElement());
  3084. }
  3085. @Override
  3086. protected void onUnload() {
  3087. scroller.detachScrollListener(verticalScrollbar.getElement());
  3088. scroller.detachScrollListener(horizontalScrollbar.getElement());
  3089. scroller.detachMousewheelListener(getElement());
  3090. scroller.detachTouchListeners(getElement());
  3091. header.paintRemoveRows(0, header.getRowCount());
  3092. footer.paintRemoveRows(0, footer.getRowCount());
  3093. body.paintRemoveRows(0, body.getRowCount());
  3094. super.onUnload();
  3095. }
  3096. private void detectAndApplyPositionFunction() {
  3097. /*
  3098. * firefox has a bug in its translate operation, showing white space
  3099. * when adjusting the scrollbar in BodyRowContainer.paintInsertRows
  3100. */
  3101. if (Window.Navigator.getUserAgent().contains("Firefox")) {
  3102. position = new AbsolutePosition();
  3103. return;
  3104. }
  3105. final Style docStyle = Document.get().getBody().getStyle();
  3106. if (hasProperty(docStyle, "transform")) {
  3107. if (hasProperty(docStyle, "transformStyle")) {
  3108. position = new Translate3DPosition();
  3109. } else {
  3110. position = new TranslatePosition();
  3111. }
  3112. } else if (hasProperty(docStyle, "webkitTransform")) {
  3113. position = new WebkitTranslate3DPosition();
  3114. } else {
  3115. position = new AbsolutePosition();
  3116. }
  3117. }
  3118. private Logger getLogger() {
  3119. return Logger.getLogger(getClass().getName());
  3120. }
  3121. private static native boolean hasProperty(Style style, String name)
  3122. /*-{
  3123. return style[name] !== undefined;
  3124. }-*/;
  3125. /**
  3126. * Check whether there are both columns and any row data (for either
  3127. * headers, body or footer).
  3128. *
  3129. * @return <code>true</code> iff header, body or footer has rows && there
  3130. * are columns
  3131. */
  3132. private boolean hasColumnAndRowData() {
  3133. return (header.getRowCount() > 0 || body.getRowCount() > 0 || footer
  3134. .getRowCount() > 0) && columnConfiguration.getColumnCount() > 0;
  3135. }
  3136. /**
  3137. * Check whether there are any cells in the DOM.
  3138. *
  3139. * @return <code>true</code> iff header, body or footer has any child
  3140. * elements
  3141. */
  3142. private boolean hasSomethingInDom() {
  3143. return headElem.hasChildNodes() || bodyElem.hasChildNodes()
  3144. || footElem.hasChildNodes();
  3145. }
  3146. /**
  3147. * Returns the representation of this Escalator header.
  3148. *
  3149. * @return the header. Never <code>null</code>
  3150. */
  3151. public RowContainer getHeader() {
  3152. return header;
  3153. }
  3154. /**
  3155. * Returns the representation of this Escalator body.
  3156. *
  3157. * @return the body. Never <code>null</code>
  3158. */
  3159. public RowContainer getBody() {
  3160. return body;
  3161. }
  3162. /**
  3163. * Returns the representation of this Escalator footer.
  3164. *
  3165. * @return the footer. Never <code>null</code>
  3166. */
  3167. public RowContainer getFooter() {
  3168. return footer;
  3169. }
  3170. /**
  3171. * Returns the configuration object for the columns in this Escalator.
  3172. *
  3173. * @return the configuration object for the columns in this Escalator. Never
  3174. * <code>null</code>
  3175. */
  3176. public ColumnConfiguration getColumnConfiguration() {
  3177. return columnConfiguration;
  3178. }
  3179. /*
  3180. * TODO remove method once RequiresResize and the Vaadin layoutmanager
  3181. * listening mechanisms are implemented (https://trello.com/c/r3Kh0Kfy)
  3182. */
  3183. @Override
  3184. public void setWidth(final String width) {
  3185. super.setWidth(width != null && !width.isEmpty() ? width
  3186. : DEFAULT_WIDTH);
  3187. recalculateElementSizes();
  3188. }
  3189. /*
  3190. * TODO remove method once RequiresResize and the Vaadin layoutmanager
  3191. * listening mechanisms are implemented (https://trello.com/c/r3Kh0Kfy)
  3192. */
  3193. @Override
  3194. public void setHeight(final String height) {
  3195. final int escalatorRowsBefore = body.visualRowOrder.size();
  3196. super.setHeight(height != null && !height.isEmpty() ? height
  3197. : DEFAULT_HEIGHT);
  3198. recalculateElementSizes();
  3199. if (escalatorRowsBefore != body.visualRowOrder.size()) {
  3200. fireRowVisibilityChangeEvent();
  3201. }
  3202. }
  3203. /**
  3204. * Returns the vertical scroll offset. Note that this is not necessarily the
  3205. * same as the scroll top in the DOM
  3206. *
  3207. * @return the logical vertical scroll offset
  3208. */
  3209. public double getScrollTop() {
  3210. return verticalScrollbar.getScrollPos();
  3211. }
  3212. /**
  3213. * Sets the vertical scroll offset. Note that this is not necessarily the
  3214. * same as the scroll top in the DOM
  3215. *
  3216. * @param scrollTop
  3217. * the number of pixels to scroll vertically
  3218. */
  3219. public void setScrollTop(final double scrollTop) {
  3220. verticalScrollbar.setScrollPos((int) scrollTop);
  3221. }
  3222. /**
  3223. * Returns the logical horizontal scroll offset. Note that this is not
  3224. * necessarily the same as the scroll left in the DOM.
  3225. *
  3226. * @return the logical horizontal scroll offset
  3227. */
  3228. public int getScrollLeft() {
  3229. return horizontalScrollbar.getScrollPos();
  3230. }
  3231. /**
  3232. * Sets the logical horizontal scroll offset. Note that this is not
  3233. * necessarily the same as the scroll left in the DOM.
  3234. *
  3235. * @param scrollLeft
  3236. * the number of pixels to scroll horizontally
  3237. */
  3238. public void setScrollLeft(final int scrollLeft) {
  3239. horizontalScrollbar.setScrollPos(scrollLeft);
  3240. }
  3241. /**
  3242. * Scrolls the body horizontally so that the column at the given index is
  3243. * visible and there is at least {@code padding} pixels to the given scroll
  3244. * destination.
  3245. *
  3246. * @param columnIndex
  3247. * the index of the column to scroll to
  3248. * @param destination
  3249. * where the column should be aligned visually after scrolling
  3250. * @param padding
  3251. * the number pixels to place between the scrolled-to column and
  3252. * the viewport edge.
  3253. * @throws IndexOutOfBoundsException
  3254. * if {@code columnIndex} is not a valid index for an existing
  3255. * column
  3256. * @throws IllegalArgumentException
  3257. * if {@code destination} is {@link ScrollDestination#MIDDLE}
  3258. * and padding is nonzero, because having a padding on a
  3259. * centered column is undefined behavior, or if the column is
  3260. * frozen
  3261. */
  3262. public void scrollToColumn(final int columnIndex,
  3263. final ScrollDestination destination, final int padding)
  3264. throws IndexOutOfBoundsException, IllegalArgumentException {
  3265. if (destination == ScrollDestination.MIDDLE && padding != 0) {
  3266. throw new IllegalArgumentException(
  3267. "You cannot have a padding with a MIDDLE destination");
  3268. }
  3269. verifyValidColumnIndex(columnIndex);
  3270. if (columnIndex < columnConfiguration.frozenColumns) {
  3271. throw new IllegalArgumentException("The given column index "
  3272. + columnIndex + " is frozen.");
  3273. }
  3274. scroller.scrollToColumn(columnIndex, destination, padding);
  3275. }
  3276. private void verifyValidColumnIndex(final int columnIndex)
  3277. throws IndexOutOfBoundsException {
  3278. if (columnIndex < 0
  3279. || columnIndex >= columnConfiguration.getColumnCount()) {
  3280. throw new IndexOutOfBoundsException("The given column index "
  3281. + columnIndex + " does not exist.");
  3282. }
  3283. }
  3284. /**
  3285. * Scrolls the body vertically so that the row at the given index is visible
  3286. * and there is at least {@literal padding} pixels to the given scroll
  3287. * destination.
  3288. *
  3289. * @param rowIndex
  3290. * the index of the logical row to scroll to
  3291. * @param destination
  3292. * where the row should be aligned visually after scrolling
  3293. * @param padding
  3294. * the number pixels to place between the scrolled-to row and the
  3295. * viewport edge.
  3296. * @throws IndexOutOfBoundsException
  3297. * if {@code rowIndex} is not a valid index for an existing row
  3298. * @throws IllegalArgumentException
  3299. * if {@code destination} is {@link ScrollDestination#MIDDLE}
  3300. * and padding is nonzero, because having a padding on a
  3301. * centered row is undefined behavior
  3302. */
  3303. public void scrollToRow(final int rowIndex,
  3304. final ScrollDestination destination, final int padding)
  3305. throws IndexOutOfBoundsException, IllegalArgumentException {
  3306. if (destination == ScrollDestination.MIDDLE && padding != 0) {
  3307. throw new IllegalArgumentException(
  3308. "You cannot have a padding with a MIDDLE destination");
  3309. }
  3310. verifyValidRowIndex(rowIndex);
  3311. scroller.scrollToRow(rowIndex, destination, padding);
  3312. }
  3313. private void verifyValidRowIndex(final int rowIndex) {
  3314. if (rowIndex < 0 || rowIndex >= body.getRowCount()) {
  3315. throw new IndexOutOfBoundsException("The given row index "
  3316. + rowIndex + " does not exist.");
  3317. }
  3318. }
  3319. /**
  3320. * Recalculates the dimensions for all elements that require manual
  3321. * calculations. Also updates the dimension caches.
  3322. * <p>
  3323. * <em>Note:</em> This method has the <strong>side-effect</strong>
  3324. * automatically makes sure that an appropriate amount of escalator rows are
  3325. * present. So, if the body area grows, more <strong>escalator rows might be
  3326. * inserted</strong>. Conversely, if the body area shrinks,
  3327. * <strong>escalator rows might be removed</strong>.
  3328. */
  3329. private void recalculateElementSizes() {
  3330. if (!isAttached()) {
  3331. return;
  3332. }
  3333. Profiler.enter("Escalator.recalculateElementSizes");
  3334. widthOfEscalator = getPreciseWidth(getElement());
  3335. heightOfEscalator = getPreciseHeight(getElement());
  3336. for (final AbstractRowContainer rowContainer : rowContainers) {
  3337. rowContainer.recalculateSectionHeight();
  3338. }
  3339. scroller.recalculateScrollbarsForVirtualViewport();
  3340. body.verifyEscalatorCount();
  3341. Profiler.leave("Escalator.recalculateElementSizes");
  3342. }
  3343. /**
  3344. * A routing method for {@link Scroller#onScroll(double, double)}.
  3345. * <p>
  3346. * This is a workaround for GWT and JSNI unable to properly handle inner
  3347. * classes, so instead we call the outer class' method, which calls the
  3348. * inner class' respective method.
  3349. * <p>
  3350. * Ideally, this method would not exist, and
  3351. * {@link Scroller#onScroll(double, double)} would be called directly.
  3352. */
  3353. private void onScroll() {
  3354. scroller.onScroll();
  3355. }
  3356. /**
  3357. * Snap deltas of x and y to the major four axes (up, down, left, right)
  3358. * with a threshold of a number of degrees from those axes.
  3359. *
  3360. * @param deltaX
  3361. * the delta in the x axis
  3362. * @param deltaY
  3363. * the delta in the y axis
  3364. * @param thresholdRatio
  3365. * the threshold in ratio (0..1) between x and y for when to snap
  3366. * @return a two-element array: <code>[snappedX, snappedY]</code>
  3367. */
  3368. private static double[] snapDeltas(final double deltaX,
  3369. final double deltaY, final double thresholdRatio) {
  3370. final double[] array = new double[2];
  3371. if (deltaX != 0 && deltaY != 0) {
  3372. final double aDeltaX = Math.abs(deltaX);
  3373. final double aDeltaY = Math.abs(deltaY);
  3374. final double yRatio = aDeltaY / aDeltaX;
  3375. final double xRatio = aDeltaX / aDeltaY;
  3376. array[0] = (xRatio < thresholdRatio) ? 0 : deltaX;
  3377. array[1] = (yRatio < thresholdRatio) ? 0 : deltaY;
  3378. } else {
  3379. array[0] = deltaX;
  3380. array[1] = deltaY;
  3381. }
  3382. return array;
  3383. }
  3384. /**
  3385. * Adds an event handler that gets notified when the range of visible rows
  3386. * changes e.g. because of scrolling.
  3387. *
  3388. * @param rowVisibilityChangeHandler
  3389. * the event handler
  3390. * @return a handler registration for the added handler
  3391. */
  3392. public HandlerRegistration addRowVisibilityChangeHandler(
  3393. RowVisibilityChangeHandler rowVisibilityChangeHandler) {
  3394. return addHandler(rowVisibilityChangeHandler,
  3395. RowVisibilityChangeEvent.TYPE);
  3396. }
  3397. private void fireRowVisibilityChangeEvent() {
  3398. if (!body.visualRowOrder.isEmpty()) {
  3399. int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder
  3400. .getFirst());
  3401. int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder
  3402. .getLast()) + 1;
  3403. int visibleRowCount = visibleRangeEnd - visibleRangeStart;
  3404. fireEvent(new RowVisibilityChangeEvent(visibleRangeStart,
  3405. visibleRowCount));
  3406. } else {
  3407. fireEvent(new RowVisibilityChangeEvent(0, 0));
  3408. }
  3409. }
  3410. /**
  3411. * Accesses the package private method Widget#setParent()
  3412. *
  3413. * @param widget
  3414. * The widget to access
  3415. * @param parent
  3416. * The parent to set
  3417. */
  3418. static native final void setParent(Widget widget, Widget parent)
  3419. /*-{
  3420. widget.@com.google.gwt.user.client.ui.Widget::setParent(Lcom/google/gwt/user/client/ui/Widget;)(parent);
  3421. }-*/;
  3422. /**
  3423. * Returns the widget from a cell node or <code>null</code> if there is no
  3424. * widget in the cell
  3425. *
  3426. * @param cellNode
  3427. * The cell node
  3428. */
  3429. static Widget getWidgetFromCell(Node cellNode) {
  3430. Node possibleWidgetNode = cellNode.getFirstChild();
  3431. if (possibleWidgetNode != null
  3432. && possibleWidgetNode.getNodeType() == Node.ELEMENT_NODE) {
  3433. @SuppressWarnings("deprecation")
  3434. com.google.gwt.user.client.Element castElement = (com.google.gwt.user.client.Element) possibleWidgetNode
  3435. .cast();
  3436. return Util.findWidget(castElement, null);
  3437. }
  3438. return null;
  3439. }
  3440. /**
  3441. * Forces the escalator to recalculate the widths of its columns.
  3442. * <p>
  3443. * All columns that haven't been assigned an explicit width will be resized
  3444. * to fit all currently visible contents.
  3445. *
  3446. * @see ColumnConfiguration#setColumnWidth(int, int)
  3447. */
  3448. public void calculateColumnWidths() {
  3449. boolean widthsHaveChanged = false;
  3450. for (int colIndex = 0; colIndex < columnConfiguration.getColumnCount(); colIndex++) {
  3451. if (columnConfiguration.getColumnWidth(colIndex) >= 0) {
  3452. continue;
  3453. }
  3454. final int oldColumnWidth = columnConfiguration
  3455. .getColumnWidthActual(colIndex);
  3456. int maxColumnWidth = 0;
  3457. maxColumnWidth = Math.max(maxColumnWidth,
  3458. header.calculateMaxColWidth(colIndex));
  3459. maxColumnWidth = Math.max(maxColumnWidth,
  3460. body.calculateMaxColWidth(colIndex));
  3461. maxColumnWidth = Math.max(maxColumnWidth,
  3462. footer.calculateMaxColWidth(colIndex));
  3463. Logger.getLogger("Escalator.calculateColumnWidths").info(
  3464. "#" + colIndex + ": " + maxColumnWidth + "px");
  3465. if (oldColumnWidth != maxColumnWidth) {
  3466. columnConfiguration.setCalculatedColumnWidth(colIndex,
  3467. maxColumnWidth);
  3468. widthsHaveChanged = true;
  3469. }
  3470. }
  3471. if (widthsHaveChanged) {
  3472. header.reapplyColumnWidths();
  3473. body.reapplyColumnWidths();
  3474. footer.reapplyColumnWidths();
  3475. recalculateElementSizes();
  3476. }
  3477. }
  3478. @Override
  3479. public void setStylePrimaryName(String style) {
  3480. super.setStylePrimaryName(style);
  3481. verticalScrollbar.setStylePrimaryName(style);
  3482. horizontalScrollbar.setStylePrimaryName(style);
  3483. UIObject.setStylePrimaryName(tableWrapper, style + "-tablewrapper");
  3484. header.setStylePrimaryName(style);
  3485. body.setStylePrimaryName(style);
  3486. footer.setStylePrimaryName(style);
  3487. }
  3488. }