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 192KB

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