123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996 |
- /*
- * Copyright 2000-2016 Vaadin Ltd.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
- package com.vaadin.client.widgets;
-
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.ListIterator;
- import java.util.Map;
- import java.util.Map.Entry;
- import java.util.Optional;
- import java.util.TreeMap;
- import java.util.function.Consumer;
- import java.util.logging.Level;
- import java.util.logging.Logger;
-
- import com.google.gwt.animation.client.Animation;
- import com.google.gwt.animation.client.AnimationScheduler;
- import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
- import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
- import com.google.gwt.core.client.Duration;
- import com.google.gwt.core.client.JavaScriptObject;
- import com.google.gwt.core.client.JsArray;
- import com.google.gwt.core.client.Scheduler;
- import com.google.gwt.core.client.Scheduler.ScheduledCommand;
- import com.google.gwt.dom.client.DivElement;
- import com.google.gwt.dom.client.Document;
- import com.google.gwt.dom.client.Element;
- import com.google.gwt.dom.client.NativeEvent;
- import com.google.gwt.dom.client.Node;
- import com.google.gwt.dom.client.NodeList;
- import com.google.gwt.dom.client.Style;
- import com.google.gwt.dom.client.Style.Display;
- import com.google.gwt.dom.client.Style.Unit;
- import com.google.gwt.dom.client.TableCellElement;
- import com.google.gwt.dom.client.TableRowElement;
- import com.google.gwt.dom.client.TableSectionElement;
- import com.google.gwt.dom.client.Touch;
- import com.google.gwt.event.shared.HandlerRegistration;
- import com.google.gwt.logging.client.LogConfiguration;
- import com.google.gwt.user.client.DOM;
- import com.google.gwt.user.client.Window;
- import com.google.gwt.user.client.ui.RequiresResize;
- import com.google.gwt.user.client.ui.RootPanel;
- import com.google.gwt.user.client.ui.UIObject;
- import com.google.gwt.user.client.ui.Widget;
- import com.vaadin.client.BrowserInfo;
- import com.vaadin.client.ComputedStyle;
- import com.vaadin.client.DeferredWorker;
- import com.vaadin.client.Profiler;
- import com.vaadin.client.WidgetUtil;
- import com.vaadin.client.ui.SubPartAware;
- import com.vaadin.client.widget.escalator.Cell;
- import com.vaadin.client.widget.escalator.ColumnConfiguration;
- import com.vaadin.client.widget.escalator.EscalatorUpdater;
- import com.vaadin.client.widget.escalator.FlyweightCell;
- import com.vaadin.client.widget.escalator.FlyweightRow;
- import com.vaadin.client.widget.escalator.PositionFunction;
- import com.vaadin.client.widget.escalator.PositionFunction.Translate3DPosition;
- import com.vaadin.client.widget.escalator.PositionFunction.TranslatePosition;
- import com.vaadin.client.widget.escalator.PositionFunction.WebkitTranslate3DPosition;
- import com.vaadin.client.widget.escalator.Row;
- import com.vaadin.client.widget.escalator.RowContainer;
- import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer;
- import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent;
- import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler;
- import com.vaadin.client.widget.escalator.ScrollbarBundle;
- import com.vaadin.client.widget.escalator.ScrollbarBundle.HorizontalScrollbarBundle;
- import com.vaadin.client.widget.escalator.ScrollbarBundle.VerticalScrollbarBundle;
- import com.vaadin.client.widget.escalator.Spacer;
- import com.vaadin.client.widget.escalator.SpacerUpdater;
- import com.vaadin.client.widget.escalator.events.RowHeightChangedEvent;
- import com.vaadin.client.widget.grid.events.ScrollEvent;
- import com.vaadin.client.widget.grid.events.ScrollHandler;
- import com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle;
- import com.vaadin.shared.Range;
- import com.vaadin.shared.ui.grid.HeightMode;
- import com.vaadin.shared.ui.grid.ScrollDestination;
- import com.vaadin.shared.util.SharedUtil;
-
- /*-
-
- Maintenance Notes! Reading these might save your day.
- (note for editors: line width is 80 chars, including the
- one-space indentation)
-
-
- == Row Container Structure
-
- AbstractRowContainer
- |-- AbstractStaticRowContainer
- | |-- HeaderRowContainer
- | `-- FooterContainer
- `---- BodyRowContainerImpl
-
- AbstractRowContainer is intended to contain all common logic
- between RowContainers. It manages the bookkeeping of row
- count, makes sure that all individual cells are rendered
- the same way, and so on.
-
- AbstractStaticRowContainer has some special logic that is
- required by all RowContainers that don't scroll (hence the
- word "static"). HeaderRowContainer and FooterRowContainer
- are pretty thin special cases of a StaticRowContainer
- (mostly relating to positioning of the root element).
-
- BodyRowContainerImpl could also be split into an additional
- "AbstractScrollingRowContainer", but I felt that no more
- inner classes were needed. So it contains both logic
- required for making things scroll about, and equivalent
- special cases for layouting, as are found in
- Header/FooterRowContainers.
-
-
- == The Three Indices
-
- Each RowContainer can be thought to have three levels of
- indices for any given displayed row (but the distinction
- matters primarily for the BodyRowContainerImpl, because of
- the way it scrolls through data):
-
- - Logical index
- - Physical (or DOM) index
- - Visual index
-
- LOGICAL INDEX is the index that is linked to the data
- source. If you want your data source to represent a SQL
- database with 10 000 rows, the 7 000:th row in the SQL has a
- logical index of 6 999, since the index is 0-based (unless
- that data source does some funky logic).
-
- PHYSICAL INDEX is the index for a row that you see in a
- browser's DOM inspector. If your row is the second <tr>
- element within a <tbody> tag, it has a physical index of 1
- (because of 0-based indices). In Header and
- FooterRowContainers, you are safe to assume that the logical
- index is the same as the physical index. But because the
- BodyRowContainerImpl never displays large data sources
- entirely in the DOM, a physical index usually has no
- apparent direct relationship with its logical index.
-
- VISUAL INDEX is the index relating to the order that you
- see a row in, in the browser, as it is rendered. The
- topmost row is 0, the second is 1, and so on. The visual
- index is similar to the physical index in the sense that
- Header and FooterRowContainers can assume a 1:1
- relationship between visual index and logical index. And
- again, BodyRowContainerImpl has no such relationship. The
- body's visual index has additionally no apparent
- relationship with its physical index. Because the <tr> tags
- are reused in the body and visually repositioned with CSS
- as the user scrolls, the relationship between physical
- index and visual index is quickly broken. You can get an
- element's visual index via the field
- BodyRowContainerImpl.visualRowOrder.
-
- Currently, the physical and visual indices are kept in sync
- _most of the time_ by a deferred rearrangement of rows.
- They become desynced when scrolling. This is to help screen
- readers to read the contents from the DOM in a natural
- order. See BodyRowContainerImpl.DeferredDomSorter for more
- about that.
-
- */
-
- /**
- * A workaround-class for GWT and JSNI.
- * <p>
- * GWT is unable to handle some method calls to Java methods in inner-classes
- * from within JSNI blocks. Having that inner class extend a non-inner-class (or
- * implement such an interface), makes it possible for JSNI to indirectly refer
- * to the inner class, by invoking methods and fields in the non-inner-class
- * API.
- *
- * @see Escalator.Scroller
- */
- abstract class JsniWorkaround {
- /**
- * A JavaScript function that handles the scroll DOM event, and passes it on
- * to Java code.
- *
- * @see #createScrollListenerFunction(Escalator)
- * @see Escalator.Scroller#onScroll()
- */
- protected final JavaScriptObject scrollListenerFunction;
-
- /**
- * A JavaScript function that handles the mousewheel DOM event, and passes
- * it on to Java code.
- *
- * @see #createMousewheelListenerFunction(Escalator)
- * @see Escalator.Scroller#onScroll()
- */
- protected final JavaScriptObject mousewheelListenerFunction;
-
- /**
- * A JavaScript function that handles the touch start DOM event, and passes
- * it on to Java code.
- *
- * @see TouchHandlerBundle#touchStart(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
- */
- protected JavaScriptObject touchStartFunction;
-
- /**
- * A JavaScript function that handles the touch move DOM event, and passes
- * it on to Java code.
- *
- * @see TouchHandlerBundle#touchMove(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
- */
- protected JavaScriptObject touchMoveFunction;
-
- /**
- * A JavaScript function that handles the touch end and cancel DOM events,
- * and passes them on to Java code.
- *
- * @see TouchHandlerBundle#touchEnd(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
- */
- protected JavaScriptObject touchEndFunction;
-
- protected TouchHandlerBundle touchHandlerBundle;
-
- protected JsniWorkaround(final Escalator escalator) {
- scrollListenerFunction = createScrollListenerFunction(escalator);
- mousewheelListenerFunction = createMousewheelListenerFunction(
- escalator);
-
- touchHandlerBundle = new TouchHandlerBundle(escalator);
- touchStartFunction = touchHandlerBundle.getTouchStartHandler();
- touchMoveFunction = touchHandlerBundle.getTouchMoveHandler();
- touchEndFunction = touchHandlerBundle.getTouchEndHandler();
- }
-
- /**
- * A method that constructs the JavaScript function that will be stored into
- * {@link #scrollListenerFunction}.
- *
- * @param esc
- * a reference to the current instance of {@link Escalator}
- * @see Escalator.Scroller#onScroll()
- */
- protected abstract JavaScriptObject createScrollListenerFunction(
- Escalator esc);
-
- /**
- * A method that constructs the JavaScript function that will be stored into
- * {@link #mousewheelListenerFunction}.
- *
- * @param esc
- * a reference to the current instance of {@link Escalator}
- * @see Escalator.Scroller#onScroll()
- */
- protected abstract JavaScriptObject createMousewheelListenerFunction(
- Escalator esc);
- }
-
- /**
- * A low-level table-like widget that features a scrolling virtual viewport and
- * lazily generated rows.
- *
- * @since 7.4
- * @author Vaadin Ltd
- */
- public class Escalator extends Widget
- implements RequiresResize, DeferredWorker, SubPartAware {
-
- // todo comments legend
- /*
- * [[optimize]]: There's an opportunity to rewrite the code in such a way
- * that it _might_ perform better (rememeber to measure, implement,
- * re-measure)
- */
- /*
- * [[mpixscroll]]: This code will require alterations that are relevant for
- * supporting the scrolling through more pixels than some browsers normally
- * would support. (i.e. when we support more than "a million" pixels in the
- * escalator DOM). NOTE: these bits can most often also be identified by
- * searching for code that call scrollElem.getScrollTop();.
- */
- /*
- * [[spacer]]: Code that is important to make spacers work.
- */
-
- /**
- * A utility class that contains utility methods that are usually called
- * from JSNI.
- * <p>
- * The methods are moved in this class to minimize the amount of JSNI code
- * as much as feasible.
- */
- static class JsniUtil {
- public static class TouchHandlerBundle {
-
- public static final String POINTER_EVENT_TYPE_TOUCH = "touch";
-
- public static final int SIGNIFICANT_MOVE_THRESHOLD = 3;
-
- /**
- * A <a href=
- * "http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsOverlay.html"
- * >JavaScriptObject overlay</a> for the
- * <a href="http://www.w3.org/TR/touch-events/">JavaScript
- * TouchEvent</a> object.
- * <p>
- * This needs to be used in the touch event handlers, since GWT's
- * {@link com.google.gwt.event.dom.client.TouchEvent TouchEvent}
- * can't be cast from the JSNI call, and the
- * {@link com.google.gwt.dom.client.NativeEvent NativeEvent} isn't
- * properly populated with the correct values.
- */
- private static final class CustomTouchEvent
- extends JavaScriptObject {
- protected CustomTouchEvent() {
- }
-
- public native NativeEvent getNativeEvent()
- /*-{
- return this;
- }-*/;
-
- public native int getPageX()
- /*-{
- return this.targetTouches[0].pageX;
- }-*/;
-
- public native int getPageY()
- /*-{
- return this.targetTouches[0].pageY;
- }-*/;
-
- public native String getPointerType()
- /*-{
- return this.pointerType;
- }-*/;
- }
-
- private final Escalator escalator;
-
- public TouchHandlerBundle(final Escalator escalator) {
- this.escalator = escalator;
- }
-
- public native JavaScriptObject getTouchStartHandler()
- /*-{
- // we need to store "this", since it won't be preserved on call.
- var self = this;
- return $entry(function (e) {
- self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchStart(*)(e);
- });
- }-*/;
-
- public native JavaScriptObject getTouchMoveHandler()
- /*-{
- // we need to store "this", since it won't be preserved on call.
- var self = this;
- return $entry(function (e) {
- self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchMove(*)(e);
- });
- }-*/;
-
- public native JavaScriptObject getTouchEndHandler()
- /*-{
- // we need to store "this", since it won't be preserved on call.
- var self = this;
- return $entry(function (e) {
- self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchEnd(*)(e);
- });
- }-*/;
-
- // Duration of the inertial scrolling simulation. Devices with
- // larger screens take longer durations.
- private static final int DURATION = Window.getClientHeight();
- // multiply scroll velocity with repeated touching
- private int acceleration = 1;
- private boolean touching = false;
- // Two movement objects for storing status and processing touches
- private Movement yMov, xMov;
- // true if moved significantly since touch start
- private boolean movedSignificantly = false;
- private double touchStartTime;
- final double MIN_VEL = 0.6, MAX_VEL = 4, F_VEL = 1500, F_ACC = 0.7,
- F_AXIS = 1;
-
- // The object to deal with one direction scrolling
- private class Movement {
- final List<Double> speeds = new ArrayList<>();
- final ScrollbarBundle scroll;
- double position, offset, velocity, prevPos, prevTime, delta;
- boolean run, vertical;
-
- public Movement(boolean vertical) {
- this.vertical = vertical;
- scroll = vertical ? escalator.verticalScrollbar
- : escalator.horizontalScrollbar;
- }
-
- public void startTouch(CustomTouchEvent event) {
- speeds.clear();
- prevPos = pagePosition(event);
- prevTime = Duration.currentTimeMillis();
- }
-
- public void moveTouch(CustomTouchEvent event) {
- double pagePosition = pagePosition(event);
- if (pagePosition > -1) {
- delta = prevPos - pagePosition;
- double now = Duration.currentTimeMillis();
- double ellapsed = now - prevTime;
- velocity = delta / ellapsed;
- // if last speed was so low, reset speeds and start
- // storing again
- if (speeds.size() > 0 && !validSpeed(speeds.get(0))) {
- speeds.clear();
- run = true;
- }
- speeds.add(0, velocity);
- prevTime = now;
- prevPos = pagePosition;
- }
- }
-
- public void endTouch(CustomTouchEvent event) {
- // Compute average speed
- velocity = 0;
- for (double s : speeds) {
- velocity += s / speeds.size();
- }
- position = scroll.getScrollPos();
-
- // Compute offset, and adjust it with an easing curve so as
- // movement is smoother.
- offset = F_VEL * velocity * acceleration
- * easingInOutCos(velocity, MAX_VEL);
-
- // Enable or disable inertia movement in this axis
- run = validSpeed(velocity);
- if (run) {
- event.getNativeEvent().preventDefault();
- }
- }
-
- void validate(Movement other) {
- if (!run || other.velocity > 0
- && Math.abs(velocity / other.velocity) < F_AXIS) {
- delta = offset = 0;
- run = false;
- }
- }
-
- void stepAnimation(double progress) {
- scroll.setScrollPos(position + offset * progress);
- }
-
- int pagePosition(CustomTouchEvent event) {
- // Use native event's screen x and y for IE11 and Edge
- // since there is no touches for these browsers (#18737)
- if (isCurrentBrowserIE11OrEdge()) {
- return vertical
- ? event.getNativeEvent().getClientY()
- + Window.getScrollTop()
- : event.getNativeEvent().getClientX()
- + Window.getScrollLeft();
- }
- JsArray<Touch> a = event.getNativeEvent().getTouches();
- return vertical ? a.get(0).getPageY() : a.get(0).getPageX();
- }
-
- boolean validSpeed(double speed) {
- return Math.abs(speed) > MIN_VEL;
- }
- }
-
- // Using GWT animations which take care of native animation frames.
- private Animation animation = new Animation() {
- @Override
- public void onUpdate(double progress) {
- xMov.stepAnimation(progress);
- yMov.stepAnimation(progress);
- }
-
- @Override
- public double interpolate(double progress) {
- return easingOutCirc(progress);
- };
-
- @Override
- public void onComplete() {
- touching = false;
- escalator.body.domSorter.reschedule();
- };
-
- @Override
- public void run(int duration) {
- if (xMov.run || yMov.run) {
- super.run(duration);
- } else {
- onComplete();
- }
- };
- };
-
- public void touchStart(final CustomTouchEvent event) {
- if (allowTouch(event)) {
- if (yMov == null) {
- yMov = new Movement(true);
- xMov = new Movement(false);
- }
- if (animation.isRunning()) {
- acceleration += F_ACC;
- event.getNativeEvent().preventDefault();
- animation.cancel();
- } else {
- acceleration = 1;
- }
- xMov.startTouch(event);
- yMov.startTouch(event);
- touchStartTime = Duration.currentTimeMillis();
- touching = true;
- movedSignificantly = false;
- } else {
- touching = false;
- animation.cancel();
- acceleration = 1;
- }
- }
-
- public void touchMove(final CustomTouchEvent event) {
- if (touching) {
- if (!movedSignificantly) {
- double distanceSquared = Math.abs(xMov.delta)
- * Math.abs(xMov.delta)
- + Math.abs(yMov.delta) * Math.abs(yMov.delta);
- movedSignificantly = distanceSquared > SIGNIFICANT_MOVE_THRESHOLD
- * SIGNIFICANT_MOVE_THRESHOLD;
- }
- // allow handling long press differently, without triggering
- // scrolling
- if (escalator.getDelayToCancelTouchScroll() >= 0
- && !movedSignificantly
- && Duration.currentTimeMillis()
- - touchStartTime > escalator
- .getDelayToCancelTouchScroll()) {
- // cancel touch handling, don't prevent event
- touching = false;
- animation.cancel();
- acceleration = 1;
- return;
- }
- xMov.moveTouch(event);
- yMov.moveTouch(event);
- xMov.validate(yMov);
- yMov.validate(xMov);
- event.getNativeEvent().preventDefault();
- moveScrollFromEvent(escalator, xMov.delta, yMov.delta,
- event.getNativeEvent());
- }
- }
-
- public void touchEnd(final CustomTouchEvent event) {
- if (touching) {
- xMov.endTouch(event);
- yMov.endTouch(event);
- xMov.validate(yMov);
- yMov.validate(xMov);
- // Adjust duration so as longer movements take more duration
- boolean vert = !xMov.run || yMov.run
- && Math.abs(yMov.offset) > Math.abs(xMov.offset);
- double delta = Math.abs((vert ? yMov : xMov).offset);
- animation.run((int) (3 * DURATION * easingOutExp(delta)));
- }
- }
-
- // Allow touchStart for IE11 and Edge even though there is no touch
- // (#18737),
- // otherwise allow touch only if there is a single touch in the
- // event
- private boolean allowTouch(
- final TouchHandlerBundle.CustomTouchEvent event) {
- if (isCurrentBrowserIE11OrEdge()) {
- return (POINTER_EVENT_TYPE_TOUCH
- .equals(event.getPointerType()));
- } else {
- return (event.getNativeEvent().getTouches().length() == 1);
- }
- }
-
- private double easingInOutCos(double val, double max) {
- return 0.5 - 0.5 * Math.cos(Math.PI * Math.signum(val)
- * Math.min(Math.abs(val), max) / max);
- }
-
- private double easingOutExp(double delta) {
- return (1 - Math.pow(2, -delta / 1000));
- }
-
- private double easingOutCirc(double progress) {
- return Math.sqrt(1 - (progress - 1) * (progress - 1));
- }
- }
-
- public static void moveScrollFromEvent(final Escalator escalator,
- final double deltaX, final double deltaY,
- final NativeEvent event) {
-
- if (!Double.isNaN(deltaX)) {
- escalator.horizontalScrollbar.setScrollPosByDelta(deltaX);
- }
-
- if (!Double.isNaN(deltaY)) {
- escalator.verticalScrollbar.setScrollPosByDelta(deltaY);
- }
-
- /*
- * TODO: only prevent if not scrolled to end/bottom. Or no? UX team
- * needs to decide.
- */
- final boolean warrantedYScroll = deltaY != 0
- && escalator.verticalScrollbar.showsScrollHandle();
- final boolean warrantedXScroll = deltaX != 0
- && escalator.horizontalScrollbar.showsScrollHandle();
- if (warrantedYScroll || warrantedXScroll) {
- event.preventDefault();
- }
- }
- }
-
- /**
- * ScrollDestination case-specific handling logic.
- */
- private static double getScrollPos(final ScrollDestination destination,
- final double targetStartPx, final double targetEndPx,
- final double viewportStartPx, final double viewportEndPx,
- final double padding) {
-
- final double viewportLength = viewportEndPx - viewportStartPx;
-
- switch (destination) {
-
- /*
- * Scroll as little as possible to show the target element. If the
- * element fits into view, this works as START or END depending on the
- * current scroll position. If the element does not fit into view, this
- * works as START.
- */
- case ANY: {
- final double startScrollPos = targetStartPx - padding;
- final double endScrollPos = targetEndPx + padding - viewportLength;
-
- if (startScrollPos < viewportStartPx) {
- return startScrollPos;
- } else if (targetEndPx + padding > viewportEndPx) {
- return endScrollPos;
- } else {
- // NOOP, it's already visible
- return viewportStartPx;
- }
- }
-
- /*
- * Scrolls so that the element is shown at the end of the viewport. The
- * viewport will, however, not scroll before its first element.
- */
- case END: {
- return targetEndPx + padding - viewportLength;
- }
-
- /*
- * Scrolls so that the element is shown in the middle of the viewport.
- * The viewport will, however, not scroll beyond its contents, given
- * more elements than what the viewport is able to show at once. Under
- * no circumstances will the viewport scroll before its first element.
- */
- case MIDDLE: {
- final double targetMiddle = targetStartPx
- + (targetEndPx - targetStartPx) / 2;
- return targetMiddle - viewportLength / 2;
- }
-
- /*
- * Scrolls so that the element is shown at the start of the viewport.
- * The viewport will, however, not scroll beyond its contents.
- */
- case START: {
- return targetStartPx - padding;
- }
-
- /*
- * Throw an error if we're here. This can only mean that
- * ScrollDestination has been carelessly amended..
- */
- default: {
- throw new IllegalArgumentException(
- "Internal: ScrollDestination has been modified, "
- + "but Escalator.getScrollPos has not been updated "
- + "to match new values.");
- }
- }
-
- }
-
- /** An inner class that handles all logic related to scrolling. */
- private class Scroller extends JsniWorkaround {
- private double lastScrollTop = 0;
- private double lastScrollLeft = 0;
-
- public Scroller() {
- super(Escalator.this);
- }
-
- @Override
- protected native JavaScriptObject createScrollListenerFunction(
- Escalator esc)
- /*-{
- var vScroll = esc.@com.vaadin.client.widgets.Escalator::verticalScrollbar;
- var vScrollElem = vScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::getElement()();
-
- var hScroll = esc.@com.vaadin.client.widgets.Escalator::horizontalScrollbar;
- var hScrollElem = hScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::getElement()();
-
- return $entry(function(e) {
- var target = e.target;
-
- // in case the scroll event was native (i.e. scrollbars were dragged, or
- // the scrollTop/Left was manually modified), the bundles have old cache
- // values. We need to make sure that the caches are kept up to date.
- if (target === vScrollElem) {
- vScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::updateScrollPosFromDom()();
- } else if (target === hScrollElem) {
- hScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::updateScrollPosFromDom()();
- } else {
- $wnd.console.error("unexpected scroll target: "+target);
- }
- });
- }-*/;
-
- @Override
- protected native JavaScriptObject createMousewheelListenerFunction(
- Escalator esc)
- /*-{
- return $entry(function(e) {
- var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
- var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
-
- // Delta mode 0 is in pixels; we don't need to do anything...
-
- // A delta mode of 1 means we're scrolling by lines instead of pixels
- // We need to scale the number of lines by the default line height
- if(e.deltaMode === 1) {
- var brc = esc.@com.vaadin.client.widgets.Escalator::body;
- deltaY *= brc.@com.vaadin.client.widgets.Escalator.AbstractRowContainer::getDefaultRowHeight()();
- }
-
- // Other delta modes aren't supported
- if((e.deltaMode !== undefined) && (e.deltaMode >= 2 || e.deltaMode < 0)) {
- var msg = "Unsupported wheel delta mode \"" + e.deltaMode + "\"";
-
- // Print warning message
- esc.@com.vaadin.client.widgets.Escalator::logWarning(*)(msg);
- }
-
- // IE8 has only delta y
- if (isNaN(deltaY)) {
- deltaY = -0.5*e.wheelDelta;
- }
-
- @com.vaadin.client.widgets.Escalator.JsniUtil::moveScrollFromEvent(*)(esc, deltaX, deltaY, e);
- });
- }-*/;
-
- /**
- * Recalculates the virtual viewport represented by the scrollbars, so
- * that the sizes of the scroll handles appear correct in the browser
- */
- public void recalculateScrollbarsForVirtualViewport() {
- double scrollContentHeight = body.calculateTotalRowHeight()
- + body.spacerContainer.getSpacerHeightsSum();
- double scrollContentWidth = columnConfiguration.calculateRowWidth();
- double tableWrapperHeight = heightOfEscalator;
- double tableWrapperWidth = widthOfEscalator;
-
- boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
- + WidgetUtil.PIXEL_EPSILON - header.getHeightOfSection()
- - footer.getHeightOfSection();
- boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth
- + WidgetUtil.PIXEL_EPSILON;
-
- // One dimension got scrollbars, but not the other. Recheck time!
- if (verticalScrollNeeded != horizontalScrollNeeded) {
- if (!verticalScrollNeeded && horizontalScrollNeeded) {
- verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
- + WidgetUtil.PIXEL_EPSILON
- - header.getHeightOfSection()
- - footer.getHeightOfSection()
- - horizontalScrollbar.getScrollbarThickness();
- } else {
- horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth
- + WidgetUtil.PIXEL_EPSILON
- - verticalScrollbar.getScrollbarThickness();
- }
- }
-
- // let's fix the table wrapper size, since it's now stable.
- if (verticalScrollNeeded) {
- tableWrapperWidth -= verticalScrollbar.getScrollbarThickness();
- tableWrapperWidth = Math.max(0, tableWrapperWidth);
- }
- if (horizontalScrollNeeded) {
- tableWrapperHeight -= horizontalScrollbar
- .getScrollbarThickness();
- tableWrapperHeight = Math.max(0, tableWrapperHeight);
- }
- tableWrapper.getStyle().setHeight(tableWrapperHeight, Unit.PX);
- tableWrapper.getStyle().setWidth(tableWrapperWidth, Unit.PX);
-
- double footerHeight = footer.getHeightOfSection();
- double headerHeight = header.getHeightOfSection();
- double vScrollbarHeight = Math.max(0,
- tableWrapperHeight - footerHeight - headerHeight);
- verticalScrollbar.setOffsetSize(vScrollbarHeight);
- verticalScrollbar.setScrollSize(scrollContentHeight);
-
- /*
- * If decreasing the amount of frozen columns, and scrolled to the
- * right, the scroll position might reset. So we need to remember
- * the scroll position, and re-apply it once the scrollbar size has
- * been adjusted.
- */
- double prevScrollPos = horizontalScrollbar.getScrollPos();
-
- double unfrozenPixels = columnConfiguration
- .getCalculatedColumnsWidth(Range.between(
- columnConfiguration.getFrozenColumnCount(),
- columnConfiguration.getColumnCount()));
- double frozenPixels = scrollContentWidth - unfrozenPixels;
- double hScrollOffsetWidth = tableWrapperWidth - frozenPixels;
- horizontalScrollbar.setOffsetSize(hScrollOffsetWidth);
- horizontalScrollbar.setScrollSize(unfrozenPixels);
- horizontalScrollbar.getElement().getStyle().setLeft(frozenPixels,
- Unit.PX);
- horizontalScrollbar.setScrollPos(prevScrollPos);
-
- /*
- * only show the scrollbar wrapper if the scrollbar itself is
- * visible.
- */
- if (horizontalScrollbar.showsScrollHandle()) {
- horizontalScrollbarDeco.getStyle().clearDisplay();
- } else {
- horizontalScrollbarDeco.getStyle().setDisplay(Display.NONE);
- }
-
- /*
- * only show corner background divs if the vertical scrollbar is
- * visible.
- */
- Style hCornerStyle = headerDeco.getStyle();
- Style fCornerStyle = footerDeco.getStyle();
- if (verticalScrollbar.showsScrollHandle()) {
- hCornerStyle.clearDisplay();
- fCornerStyle.clearDisplay();
-
- if (horizontalScrollbar.showsScrollHandle()) {
- double offset = horizontalScrollbar.getScrollbarThickness();
- fCornerStyle.setBottom(offset, Unit.PX);
- } else {
- fCornerStyle.clearBottom();
- }
- } else {
- hCornerStyle.setDisplay(Display.NONE);
- fCornerStyle.setDisplay(Display.NONE);
- }
- }
-
- /**
- * Logical scrolling event handler for the entire widget.
- */
- public void onScroll() {
-
- final double scrollTop = verticalScrollbar.getScrollPos();
- final double scrollLeft = horizontalScrollbar.getScrollPos();
- if (lastScrollLeft != scrollLeft) {
- for (int i = 0; i < columnConfiguration.frozenColumns; i++) {
- header.updateFreezePosition(i, scrollLeft);
- body.updateFreezePosition(i, scrollLeft);
- footer.updateFreezePosition(i, scrollLeft);
- }
-
- position.set(headElem, -scrollLeft, 0);
- position.set(footElem, -scrollLeft, 0);
-
- lastScrollLeft = scrollLeft;
- }
-
- body.setBodyScrollPosition(scrollLeft, scrollTop);
-
- lastScrollTop = scrollTop;
- body.updateEscalatorRowsOnScroll();
- body.spacerContainer.updateSpacerDecosVisibility();
- /*
- * TODO [[optimize]]: Might avoid a reflow by first calculating new
- * scrolltop and scrolleft, then doing the escalator magic based on
- * those numbers and only updating the positions after that.
- */
- }
-
- public native void attachScrollListener(Element element)
- /*
- * Attaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- if (element.addEventListener) {
- element.addEventListener("scroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
- } else {
- element.attachEvent("onscroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
- }
- }-*/;
-
- public native void detachScrollListener(Element element)
- /*
- * Detaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- if (element.addEventListener) {
- element.removeEventListener("scroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
- } else {
- element.detachEvent("onscroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
- }
- }-*/;
-
- public native void attachMousewheelListener(Element element)
- /*
- * Attaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- // firefox likes "wheel", while others use "mousewheel"
- var eventName = 'onmousewheel' in element ? 'mousewheel' : 'wheel';
- element.addEventListener(eventName, this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
- }-*/;
-
- public native void detachMousewheelListener(Element element)
- /*
- * Detaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- // firefox likes "wheel", while others use "mousewheel"
- var eventName = element.onwheel===undefined?"mousewheel":"wheel";
- element.removeEventListener(eventName, this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
- }-*/;
-
- public native void attachTouchListeners(Element element)
- /*
- * Detaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- element.addEventListener("touchstart", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
- element.addEventListener("touchmove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
- element.addEventListener("touchend", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
- element.addEventListener("touchcancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
- }-*/;
-
- public native void detachTouchListeners(Element element)
- /*
- * Detaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- element.removeEventListener("touchstart", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
- element.removeEventListener("touchmove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
- element.removeEventListener("touchend", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
- element.removeEventListener("touchcancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
- }-*/;
-
- /**
- * Using pointerdown, pointermove, pointerup, and pointercancel for IE11
- * and Edge instead of touch* listeners (#18737)
- *
- * @param element
- */
- public native void attachPointerEventListeners(Element element)
- /*
- * Attaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- element.addEventListener("pointerdown", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
- element.addEventListener("pointermove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
- element.addEventListener("pointerup", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
- element.addEventListener("pointercancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
- }-*/;
-
- /**
- * Using pointerdown, pointermove, pointerup, and pointercancel for IE11
- * and Edge instead of touch* listeners (#18737)
- *
- * @param element
- */
- public native void detachPointerEventListeners(Element element)
- /*
- * Detaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- element.removeEventListener("pointerdown", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
- element.removeEventListener("pointermove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
- element.removeEventListener("pointerup", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
- element.removeEventListener("pointercancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
- }-*/;
-
- public void scrollToColumn(final int columnIndex,
- final ScrollDestination destination, final int padding) {
- assert columnIndex >= columnConfiguration.frozenColumns : "Can't scroll to a frozen column";
-
- /*
- * To cope with frozen columns, we just pretend those columns are
- * not there at all when calculating the position of the target
- * column and the boundaries of the viewport. The resulting
- * scrollLeft will be correct without compensation since the DOM
- * structure effectively means that scrollLeft also ignores the
- * frozen columns.
- */
- final double frozenPixels = columnConfiguration
- .getCalculatedColumnsWidth(Range.withLength(0,
- columnConfiguration.frozenColumns));
-
- final double targetStartPx = columnConfiguration
- .getCalculatedColumnsWidth(Range.withLength(0, columnIndex))
- - frozenPixels;
- final double targetEndPx = targetStartPx
- + columnConfiguration.getColumnWidthActual(columnIndex);
-
- final double viewportStartPx = getScrollLeft();
- double viewportEndPx = viewportStartPx
- + getBoundingWidth(getElement()) - frozenPixels;
- if (verticalScrollbar.showsScrollHandle()) {
- viewportEndPx -= WidgetUtil.getNativeScrollbarSize();
- }
-
- final double scrollLeft = getScrollPos(destination, targetStartPx,
- targetEndPx, viewportStartPx, viewportEndPx, padding);
-
- /*
- * note that it doesn't matter if the scroll would go beyond the
- * content, since the browser will adjust for that, and everything
- * fall into line accordingly.
- */
- setScrollLeft(scrollLeft);
- }
-
- public void scrollToRow(final int rowIndex,
- final ScrollDestination destination, final double padding) {
-
- final double targetStartPx = (body.getDefaultRowHeight() * rowIndex)
- + body.spacerContainer
- .getSpacerHeightsSumUntilIndex(rowIndex);
- final double targetEndPx = targetStartPx
- + body.getDefaultRowHeight();
-
- final double viewportStartPx = getScrollTop();
- final double viewportEndPx = viewportStartPx
- + body.getHeightOfSection();
-
- final double scrollTop = getScrollPos(destination, targetStartPx,
- targetEndPx, viewportStartPx, viewportEndPx, padding);
-
- /*
- * note that it doesn't matter if the scroll would go beyond the
- * content, since the browser will adjust for that, and everything
- * falls into line accordingly.
- */
- setScrollTop(scrollTop);
- }
- }
-
- public abstract class AbstractRowContainer implements RowContainer {
- private EscalatorUpdater updater = EscalatorUpdater.NULL;
-
- private int rows;
-
- /**
- * The table section element ({@code <thead>}, {@code <tbody>} or
- * {@code <tfoot>}) the rows (i.e. {@code
- *
- <tr>
- * } tags) are contained in.
- */
- protected final TableSectionElement root;
-
- /**
- * The primary style name of the escalator. Most commonly provided by
- * Escalator as "v-escalator".
- */
- private String primaryStyleName = null;
-
- private boolean defaultRowHeightShouldBeAutodetected = true;
-
- private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT;
-
- private boolean initialColumnSizesCalculated = false;
-
- public AbstractRowContainer(
- final TableSectionElement rowContainerElement) {
- root = rowContainerElement;
- }
-
- @Override
- public TableSectionElement getElement() {
- return root;
- }
-
- /**
- * Gets the tag name of an element to represent a cell in a row.
- * <p>
- * Usually {@code "th"} or {@code "td"}.
- * <p>
- * <em>Note:</em> To actually <em>create</em> such an element, use
- * {@link #createCellElement(double)} instead.
- *
- * @return the tag name for the element to represent cells as
- * @see #createCellElement(double)
- */
- protected abstract String getCellElementTagName();
-
- @Override
- public EscalatorUpdater getEscalatorUpdater() {
- return updater;
- }
-
- /**
- * {@inheritDoc}
- * <p>
- * <em>Implementation detail:</em> This method does no DOM modifications
- * (i.e. is very cheap to call) if there is no data for rows or columns
- * when this method is called.
- *
- * @see #hasColumnAndRowData()
- */
- @Override
- public void setEscalatorUpdater(
- final EscalatorUpdater escalatorUpdater) {
- if (escalatorUpdater == null) {
- throw new IllegalArgumentException(
- "escalator updater cannot be null");
- }
-
- updater = escalatorUpdater;
-
- if (hasColumnAndRowData() && getRowCount() > 0) {
- refreshRows(0, getRowCount());
- }
- }
-
- /**
- * {@inheritDoc}
- * <p>
- * <em>Implementation detail:</em> This method does no DOM modifications
- * (i.e. is very cheap to call) if there are no rows in the DOM when
- * this method is called.
- *
- * @see #hasSomethingInDom()
- */
- @Override
- public void removeRows(final int index, final int numberOfRows) {
- assertArgumentsAreValidAndWithinRange(index, numberOfRows);
-
- rows -= numberOfRows;
-
- if (!isAttached()) {
- return;
- }
-
- if (hasSomethingInDom()) {
- paintRemoveRows(index, numberOfRows);
- }
- }
-
- /**
- * Removes those row elements from the DOM that correspond to the given
- * range of logical indices. This may be fewer than {@code numberOfRows}
- * , even zero, if not all the removed rows are actually visible.
- * <p>
- * The implementation must call
- * {@link #paintRemoveRow(TableRowElement, int)} for each row that is
- * removed from the DOM.
- *
- * @param index
- * the logical index of the first removed row
- * @param numberOfRows
- * number of logical rows to remove
- */
- protected abstract void paintRemoveRows(final int index,
- final int numberOfRows);
-
- /**
- * Removes a row element from the DOM, invoking
- * {@link #getEscalatorUpdater()}
- * {@link EscalatorUpdater#preDetach(Row, Iterable) preDetach} and
- * {@link EscalatorUpdater#postDetach(Row, Iterable) postDetach} before
- * and after removing the row, respectively.
- * <p>
- * This method must be called for each removed DOM row by any
- * {@link #paintRemoveRows(int, int)} implementation.
- *
- * @param tr
- * the row element to remove.
- */
- protected void paintRemoveRow(final TableRowElement tr,
- final int logicalRowIndex) {
-
- flyweightRow.setup(tr, logicalRowIndex,
- columnConfiguration.getCalculatedColumnWidths());
-
- getEscalatorUpdater().preDetach(flyweightRow,
- flyweightRow.getCells());
-
- tr.removeFromParent();
-
- getEscalatorUpdater().postDetach(flyweightRow,
- flyweightRow.getCells());
-
- /*
- * the "assert" guarantees that this code is run only during
- * development/debugging.
- */
- assert flyweightRow.teardown();
-
- }
-
- protected void assertArgumentsAreValidAndWithinRange(final int index,
- final int numberOfRows)
- throws IllegalArgumentException, IndexOutOfBoundsException {
- if (numberOfRows < 1) {
- throw new IllegalArgumentException(
- "Number of rows must be 1 or greater (was "
- + numberOfRows + ")");
- }
-
- if (index < 0 || index + numberOfRows > getRowCount()) {
- throw new IndexOutOfBoundsException("The given " + "row range ("
- + index + ".." + (index + numberOfRows)
- + ") was outside of the current number of rows ("
- + getRowCount() + ")");
- }
- }
-
- @Override
- public int getRowCount() {
- return rows;
- }
-
- /**
- * This method calculates the current row count directly from the DOM.
- * <p>
- * While Escalator is stable, this value should equal to
- * {@link #getRowCount()}, but while row counts are being updated, these
- * two values might differ for a short while.
- * <p>
- * Any extra content, such as spacers for the body, should not be
- * included in this count.
- *
- * @since 7.5.0
- *
- * @return the actual DOM count of rows
- */
- public abstract int getDomRowCount();
-
- /**
- * {@inheritDoc}
- * <p>
- * <em>Implementation detail:</em> This method does no DOM modifications
- * (i.e. is very cheap to call) if there is no data for columns when
- * this method is called.
- *
- * @see #hasColumnAndRowData()
- */
- @Override
- public void insertRows(final int index, final int numberOfRows) {
- if (index < 0 || index > getRowCount()) {
- throw new IndexOutOfBoundsException("The given index (" + index
- + ") was outside of the current number of rows (0.."
- + getRowCount() + ")");
- }
-
- if (numberOfRows < 1) {
- throw new IllegalArgumentException(
- "Number of rows must be 1 or greater (was "
- + numberOfRows + ")");
- }
-
- rows += numberOfRows;
- /*
- * only add items in the DOM if the widget itself is attached to the
- * DOM. We can't calculate sizes otherwise.
- */
- if (isAttached()) {
- paintInsertRows(index, numberOfRows);
-
- /*
- * We are inserting the first rows in this container. We
- * potentially need to set the widths for the cells for the
- * first time.
- */
- if (rows == numberOfRows) {
- Scheduler.get().scheduleFinally(() -> {
- if (initialColumnSizesCalculated) {
- return;
- }
- initialColumnSizesCalculated = true;
-
- Map<Integer, Double> colWidths = new HashMap<>();
- for (int i = 0; i < getColumnConfiguration()
- .getColumnCount(); i++) {
- Double width = Double.valueOf(
- getColumnConfiguration().getColumnWidth(i));
- Integer col = Integer.valueOf(i);
- colWidths.put(col, width);
- }
- getColumnConfiguration().setColumnWidths(colWidths);
- });
- }
- }
- }
-
- /**
- * Actually add rows into the DOM, now that everything can be
- * calculated.
- *
- * @param visualIndex
- * the DOM index to add rows into
- * @param numberOfRows
- * the number of rows to insert
- * @return a list of the added row elements
- */
- protected abstract void paintInsertRows(final int visualIndex,
- final int numberOfRows);
-
- protected List<TableRowElement> paintInsertStaticRows(
- final int visualIndex, final int numberOfRows) {
- assert isAttached() : "Can't paint rows if Escalator is not attached";
-
- final List<TableRowElement> addedRows = new ArrayList<>();
-
- if (numberOfRows < 1) {
- return addedRows;
- }
-
- Node referenceRow;
- if (root.getChildCount() != 0 && visualIndex != 0) {
- // get the row node we're inserting stuff after
- referenceRow = root.getChild(visualIndex - 1);
- } else {
- // index is 0, so just prepend.
- referenceRow = null;
- }
-
- for (int row = visualIndex; row < visualIndex
- + numberOfRows; row++) {
- final TableRowElement tr = TableRowElement.as(DOM.createTR());
- addedRows.add(tr);
- tr.addClassName(getStylePrimaryName() + "-row");
-
- for (int col = 0; col < columnConfiguration
- .getColumnCount(); col++) {
- final double colWidth = columnConfiguration
- .getColumnWidthActual(col);
- final TableCellElement cellElem = createCellElement(
- colWidth);
- tr.appendChild(cellElem);
-
- // Set stylename and position if new cell is frozen
- if (col < columnConfiguration.frozenColumns) {
- cellElem.addClassName("frozen");
- position.set(cellElem, scroller.lastScrollLeft, 0);
- }
- if (columnConfiguration.frozenColumns > 0
- && col == columnConfiguration.frozenColumns - 1) {
- cellElem.addClassName("last-frozen");
- }
- }
-
- referenceRow = paintInsertRow(referenceRow, tr, row);
- }
- reapplyRowWidths();
-
- recalculateSectionHeight();
-
- return addedRows;
- }
-
- /**
- * Inserts a single row into the DOM, invoking
- * {@link #getEscalatorUpdater()}
- * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and
- * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before
- * and after inserting the row, respectively. The row should have its
- * cells already inserted.
- *
- * @param referenceRow
- * the row after which to insert or null if insert as first
- * @param tr
- * the row to be inserted
- * @param logicalRowIndex
- * the logical index of the inserted row
- * @return the inserted row to be used as the new reference
- */
- protected Node paintInsertRow(Node referenceRow,
- final TableRowElement tr, int logicalRowIndex) {
- flyweightRow.setup(tr, logicalRowIndex,
- columnConfiguration.getCalculatedColumnWidths());
-
- getEscalatorUpdater().preAttach(flyweightRow,
- flyweightRow.getCells());
-
- referenceRow = insertAfterReferenceAndUpdateIt(root, tr,
- referenceRow);
-
- getEscalatorUpdater().postAttach(flyweightRow,
- flyweightRow.getCells());
- updater.update(flyweightRow, flyweightRow.getCells());
-
- /*
- * the "assert" guarantees that this code is run only during
- * development/debugging.
- */
- assert flyweightRow.teardown();
- return referenceRow;
- }
-
- private Node insertAfterReferenceAndUpdateIt(final Element parent,
- final Element elem, final Node referenceNode) {
- if (referenceNode != null) {
- parent.insertAfter(elem, referenceNode);
- } else {
- /*
- * referencenode being null means we have offset 0, i.e. make it
- * the first row
- */
- /*
- * TODO [[optimize]]: Is insertFirst or append faster for an
- * empty root?
- */
- parent.insertFirst(elem);
- }
- return elem;
- }
-
- protected abstract void recalculateSectionHeight();
-
- /**
- * Returns the height of all rows in the row container.
- */
- protected double calculateTotalRowHeight() {
- return getDefaultRowHeight() * getRowCount();
- }
-
- /**
- * {@inheritDoc}
- * <p>
- * <em>Implementation detail:</em> This method does no DOM modifications
- * (i.e. is very cheap to call) if there is no data for columns when
- * this method is called.
- *
- * @see #hasColumnAndRowData()
- */
- @Override
- // overridden because of JavaDoc
- public void refreshRows(final int index, final int numberOfRows) {
- Range rowRange = Range.withLength(index, numberOfRows);
- Range colRange = Range.withLength(0,
- getColumnConfiguration().getColumnCount());
- refreshCells(rowRange, colRange);
- }
-
- protected abstract void refreshCells(Range logicalRowRange,
- Range colRange);
-
- void refreshRow(TableRowElement tr, int logicalRowIndex) {
- refreshRow(tr, logicalRowIndex, Range.withLength(0,
- getColumnConfiguration().getColumnCount()));
- }
-
- void refreshRow(final TableRowElement tr, final int logicalRowIndex,
- Range colRange) {
- flyweightRow.setup(tr, logicalRowIndex,
- columnConfiguration.getCalculatedColumnWidths());
- Iterable<FlyweightCell> cellsToUpdate = flyweightRow
- .getCells(colRange.getStart(), colRange.length());
- updater.update(flyweightRow, cellsToUpdate);
-
- /*
- * the "assert" guarantees that this code is run only during
- * development/debugging.
- */
- assert flyweightRow.teardown();
- }
-
- /**
- * Create and setup an empty cell element.
- *
- * @param width
- * the width of the cell, in pixels
- *
- * @return a set-up empty cell element
- */
- public TableCellElement createCellElement(final double width) {
- final TableCellElement cellElem = TableCellElement
- .as(DOM.createElement(getCellElementTagName()));
-
- final double height = getDefaultRowHeight();
- assert height >= 0 : "defaultRowHeight was negative. There's a setter leak somewhere.";
- cellElem.getStyle().setHeight(height, Unit.PX);
-
- if (width >= 0) {
- cellElem.getStyle().setWidth(width, Unit.PX);
- }
- cellElem.addClassName(getStylePrimaryName() + "-cell");
- return cellElem;
- }
-
- @Override
- public TableRowElement getRowElement(int index) {
- return getTrByVisualIndex(index);
- }
-
- /**
- * Gets the child element that is visually at a certain index
- *
- * @param index
- * the index of the element to retrieve
- * @return the element at position {@code index}
- * @throws IndexOutOfBoundsException
- * if {@code index} is not valid within {@link #root}
- */
- protected abstract TableRowElement getTrByVisualIndex(int index)
- throws IndexOutOfBoundsException;
-
- protected void paintRemoveColumns(final int offset,
- final int numberOfColumns) {
- for (int i = 0; i < getDomRowCount(); i++) {
- TableRowElement row = getTrByVisualIndex(i);
- flyweightRow.setup(row, i,
- columnConfiguration.getCalculatedColumnWidths());
-
- Iterable<FlyweightCell> attachedCells = flyweightRow
- .getCells(offset, numberOfColumns);
- getEscalatorUpdater().preDetach(flyweightRow, attachedCells);
-
- for (int j = 0; j < numberOfColumns; j++) {
- row.getCells().getItem(offset).removeFromParent();
- }
-
- Iterable<FlyweightCell> detachedCells = flyweightRow
- .getUnattachedCells(offset, numberOfColumns);
- getEscalatorUpdater().postDetach(flyweightRow, detachedCells);
-
- assert flyweightRow.teardown();
- }
- }
-
- protected void paintInsertColumns(final int offset,
- final int numberOfColumns, boolean frozen) {
-
- for (int row = 0; row < getDomRowCount(); row++) {
- final TableRowElement tr = getTrByVisualIndex(row);
- int logicalRowIndex = getLogicalRowIndex(tr);
- paintInsertCells(tr, logicalRowIndex, offset, numberOfColumns);
- }
- reapplyRowWidths();
-
- if (frozen) {
- for (int col = offset; col < offset + numberOfColumns; col++) {
- setColumnFrozen(col, true);
- }
- }
- }
-
- /**
- * Inserts new cell elements into a single row element, invoking
- * {@link #getEscalatorUpdater()}
- * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and
- * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before
- * and after inserting the cells, respectively.
- * <p>
- * Precondition: The row must be already attached to the DOM and the
- * FlyweightCell instances corresponding to the new columns added to
- * {@code flyweightRow}.
- *
- * @param tr
- * the row in which to insert the cells
- * @param logicalRowIndex
- * the index of the row
- * @param offset
- * the index of the first cell
- * @param numberOfCells
- * the number of cells to insert
- */
- private void paintInsertCells(final TableRowElement tr,
- int logicalRowIndex, final int offset,
- final int numberOfCells) {
-
- assert root.isOrHasChild(
- tr) : "The row must be attached to the document";
-
- flyweightRow.setup(tr, logicalRowIndex,
- columnConfiguration.getCalculatedColumnWidths());
-
- Iterable<FlyweightCell> cells = flyweightRow
- .getUnattachedCells(offset, numberOfCells);
-
- for (FlyweightCell cell : cells) {
- final double colWidth = columnConfiguration
- .getColumnWidthActual(cell.getColumn());
- final TableCellElement cellElem = createCellElement(colWidth);
- cell.setElement(cellElem);
- }
-
- getEscalatorUpdater().preAttach(flyweightRow, cells);
-
- Node referenceCell;
- if (offset != 0) {
- referenceCell = tr.getChild(offset - 1);
- } else {
- referenceCell = null;
- }
-
- for (FlyweightCell cell : cells) {
- referenceCell = insertAfterReferenceAndUpdateIt(tr,
- cell.getElement(), referenceCell);
- }
-
- getEscalatorUpdater().postAttach(flyweightRow, cells);
- getEscalatorUpdater().update(flyweightRow, cells);
-
- assert flyweightRow.teardown();
- }
-
- public void setColumnFrozen(int column, boolean frozen) {
- toggleFrozenColumnClass(column, frozen, "frozen");
-
- if (frozen) {
- updateFreezePosition(column, scroller.lastScrollLeft);
- }
- }
-
- private void toggleFrozenColumnClass(int column, boolean frozen,
- String className) {
- final NodeList<TableRowElement> childRows = root.getRows();
-
- for (int row = 0; row < childRows.getLength(); row++) {
- final TableRowElement tr = childRows.getItem(row);
- if (!rowCanBeFrozen(tr)) {
- continue;
- }
-
- TableCellElement cell = tr.getCells().getItem(column);
- if (frozen) {
- cell.addClassName(className);
- } else {
- cell.removeClassName(className);
- position.reset(cell);
- }
- }
- }
-
- public void setColumnLastFrozen(int column, boolean lastFrozen) {
- toggleFrozenColumnClass(column, lastFrozen, "last-frozen");
- }
-
- public void updateFreezePosition(int column, double scrollLeft) {
- final NodeList<TableRowElement> childRows = root.getRows();
-
- for (int row = 0; row < childRows.getLength(); row++) {
- final TableRowElement tr = childRows.getItem(row);
-
- if (rowCanBeFrozen(tr)) {
- TableCellElement cell = tr.getCells().getItem(column);
- position.set(cell, scrollLeft, 0);
- }
- }
- }
-
- /**
- * Checks whether a row is an element, or contains such elements, that
- * can be frozen.
- * <p>
- * In practice, this applies for all header and footer rows. For body
- * rows, it applies for all rows except spacer rows.
- *
- * @since 7.5.0
- *
- * @param tr
- * the row element to check for if it is or has elements that
- * can be frozen
- * @return <code>true</code> iff this the given element, or any of its
- * descendants, can be frozen
- */
- protected abstract boolean rowCanBeFrozen(TableRowElement tr);
-
- /**
- * Iterates through all the cells in a column and returns the width of
- * the widest element in this RowContainer.
- *
- * @param index
- * the index of the column to inspect
- * @return the pixel width of the widest element in the indicated column
- */
- public double calculateMaxColWidth(int index) {
- TableRowElement row = TableRowElement
- .as(root.getFirstChildElement());
- double maxWidth = 0;
- while (row != null) {
- final TableCellElement cell = row.getCells().getItem(index);
- final boolean isVisible = !cell.getStyle().getDisplay()
- .equals(Display.NONE.getCssName());
- if (isVisible) {
- maxWidth = Math.max(maxWidth, getBoundingWidth(cell));
- }
- row = TableRowElement.as(row.getNextSiblingElement());
- }
- return maxWidth;
- }
-
- /**
- * Reapplies all the cells' widths according to the calculated widths in
- * the column configuration.
- */
- public void reapplyColumnWidths() {
- Element row = root.getFirstChildElement();
- while (row != null) {
- // Only handle non-spacer rows
- if (!body.spacerContainer.isSpacer(row)) {
- Element cell = row.getFirstChildElement();
- int columnIndex = 0;
- while (cell != null) {
- final double width = getCalculatedColumnWidthWithColspan(
- cell, columnIndex);
-
- /*
- * TODO Should Escalator implement ProvidesResize at
- * some point, this is where we need to do that.
- */
- cell.getStyle().setWidth(width, Unit.PX);
-
- cell = cell.getNextSiblingElement();
- columnIndex++;
- }
- }
- row = row.getNextSiblingElement();
- }
-
- reapplyRowWidths();
- }
-
- private double getCalculatedColumnWidthWithColspan(final Element cell,
- final int columnIndex) {
- final int colspan = cell.getPropertyInt(FlyweightCell.COLSPAN_ATTR);
- Range spannedColumns = Range.withLength(columnIndex, colspan);
-
- /*
- * Since browsers don't explode with overflowing colspans, escalator
- * shouldn't either.
- */
- if (spannedColumns.getEnd() > columnConfiguration
- .getColumnCount()) {
- spannedColumns = Range.between(columnIndex,
- columnConfiguration.getColumnCount());
- }
- return columnConfiguration
- .getCalculatedColumnsWidth(spannedColumns);
- }
-
- /**
- * Applies the total length of the columns to each row element.
- * <p>
- * <em>Note:</em> In contrast to {@link #reapplyColumnWidths()}, this
- * method only modifies the width of the {@code
- *
- <tr>
- * } element, not the cells within.
- */
- protected void reapplyRowWidths() {
- double rowWidth = columnConfiguration.calculateRowWidth();
- if (rowWidth < 0) {
- return;
- }
-
- Element row = root.getFirstChildElement();
- while (row != null) {
- // IF there is a rounding error when summing the columns, we
- // need to round the tr width up to ensure that columns fit and
- // do not wrap
- // E.g.122.95+123.25+103.75+209.25+83.52+88.57+263.45+131.21+126.85+113.13=1365.9299999999998
- // For this we must set 1365.93 or the last column will wrap
- row.getStyle().setWidth(WidgetUtil.roundSizeUp(rowWidth),
- Unit.PX);
- row = row.getNextSiblingElement();
- }
- }
-
- /**
- * The primary style name for the container.
- *
- * @param primaryStyleName
- * the style name to use as prefix for all row and cell style
- * names.
- */
- protected void setStylePrimaryName(String primaryStyleName) {
- String oldStyle = getStylePrimaryName();
- if (SharedUtil.equals(oldStyle, primaryStyleName)) {
- return;
- }
-
- this.primaryStyleName = primaryStyleName;
-
- // Update already rendered rows and cells
- Element row = root.getRows().getItem(0);
- while (row != null) {
- UIObject.setStylePrimaryName(row, primaryStyleName + "-row");
- Element cell = TableRowElement.as(row).getCells().getItem(0);
- while (cell != null) {
- assert TableCellElement.is(cell);
- UIObject.setStylePrimaryName(cell,
- primaryStyleName + "-cell");
- cell = cell.getNextSiblingElement();
- }
- row = row.getNextSiblingElement();
- }
- }
-
- /**
- * Returns the primary style name of the container.
- *
- * @return The primary style name or <code>null</code> if not set.
- */
- protected String getStylePrimaryName() {
- return primaryStyleName;
- }
-
- @Override
- public void setDefaultRowHeight(double px)
- throws IllegalArgumentException {
- if (px < 1) {
- throw new IllegalArgumentException(
- "Height must be positive. " + px + " was given.");
- }
-
- defaultRowHeightShouldBeAutodetected = false;
- defaultRowHeight = px;
- reapplyDefaultRowHeights();
- }
-
- @Override
- public double getDefaultRowHeight() {
- return defaultRowHeight;
- }
-
- /**
- * The default height of rows has (most probably) changed.
- * <p>
- * Make sure that the displayed rows with a default height are updated
- * in height and top position.
- * <p>
- * <em>Note:</em>This implementation should not call
- * {@link Escalator#recalculateElementSizes()} - it is done by the
- * discretion of the caller of this method.
- */
- protected abstract void reapplyDefaultRowHeights();
-
- protected void reapplyRowHeight(final TableRowElement tr,
- final double heightPx) {
- assert heightPx >= 0 : "Height must not be negative";
-
- Element cellElem = tr.getFirstChildElement();
- while (cellElem != null) {
- cellElem.getStyle().setHeight(heightPx, Unit.PX);
- cellElem = cellElem.getNextSiblingElement();
- }
-
- /*
- * no need to apply height to tr-element, it'll be resized
- * implicitly.
- */
- }
-
- protected void setRowPosition(final TableRowElement tr, final int x,
- final double y) {
- positions.set(tr, x, y);
- }
-
- /**
- * Returns <em>the assigned</em> top position for the given element.
- * <p>
- * <em>Note:</em> This method does not calculate what a row's top
- * position should be. It just returns an assigned value, correct or
- * not.
- *
- * @param tr
- * the table row element to measure
- * @return the current top position for {@code tr}
- * @see BodyRowContainerImpl#getRowTop(int)
- */
- protected double getRowTop(final TableRowElement tr) {
- return positions.getTop(tr);
- }
-
- protected void removeRowPosition(TableRowElement tr) {
- positions.remove(tr);
- }
-
- public void autodetectRowHeightLater() {
- Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
- @Override
- public void execute() {
- if (defaultRowHeightShouldBeAutodetected && isAttached()) {
- autodetectRowHeightNow();
- defaultRowHeightShouldBeAutodetected = false;
- }
- }
- });
- }
-
- private void fireRowHeightChangedEventFinally() {
- if (!rowHeightChangedEventFired) {
- rowHeightChangedEventFired = true;
- Scheduler.get().scheduleFinally(new ScheduledCommand() {
- @Override
- public void execute() {
- fireEvent(new RowHeightChangedEvent());
- rowHeightChangedEventFired = false;
- }
- });
- }
- }
-
- public void autodetectRowHeightNow() {
- if (!isAttached()) {
- // Run again when attached
- defaultRowHeightShouldBeAutodetected = true;
- return;
- }
-
- final double oldRowHeight = defaultRowHeight;
-
- final Element detectionTr = DOM.createTR();
- detectionTr.setClassName(getStylePrimaryName() + "-row");
-
- final Element cellElem = DOM.createElement(getCellElementTagName());
- cellElem.setClassName(getStylePrimaryName() + "-cell");
- cellElem.setInnerText("Ij");
-
- detectionTr.appendChild(cellElem);
- root.appendChild(detectionTr);
- double boundingHeight = getBoundingHeight(cellElem);
- defaultRowHeight = Math.max(1.0d, boundingHeight);
- root.removeChild(detectionTr);
-
- if (root.hasChildNodes()) {
- reapplyDefaultRowHeights();
- applyHeightByRows();
- }
-
- if (oldRowHeight != defaultRowHeight) {
- fireRowHeightChangedEventFinally();
- }
- }
-
- @Override
- public Cell getCell(final Element element) {
- if (element == null) {
- throw new IllegalArgumentException("Element cannot be null");
- }
-
- /*
- * Ensure that element is not root nor the direct descendant of root
- * (a row) and ensure the element is inside the dom hierarchy of the
- * root element. If not, return.
- */
- if (root == element || element.getParentElement() == root
- || !root.isOrHasChild(element)) {
- return null;
- }
-
- /*
- * Ensure element is the cell element by iterating up the DOM
- * hierarchy until reaching cell element.
- */
- Element cellElementCandidate = element;
- while (cellElementCandidate.getParentElement()
- .getParentElement() != root) {
- cellElementCandidate = cellElementCandidate.getParentElement();
- }
- final TableCellElement cellElement = TableCellElement
- .as(cellElementCandidate);
-
- // Find dom column
- int domColumnIndex = -1;
- for (Element e = cellElement; e != null; e = e
- .getPreviousSiblingElement()) {
- domColumnIndex++;
- }
-
- // Find dom row
- int domRowIndex = -1;
- for (Element e = cellElement.getParentElement(); e != null; e = e
- .getPreviousSiblingElement()) {
- domRowIndex++;
- }
-
- return new Cell(domRowIndex, domColumnIndex, cellElement);
- }
-
- double measureCellWidth(TableCellElement cell, boolean withContent) {
- /*
- * To get the actual width of the contents, we need to get the cell
- * content without any hardcoded height or width.
- *
- * But we don't want to modify the existing column, because that
- * might trigger some unnecessary listeners and whatnot. So,
- * instead, we make a deep clone of that cell, but without any
- * explicit dimensions, and measure that instead.
- */
-
- TableCellElement cellClone = TableCellElement
- .as((Element) cell.cloneNode(withContent));
- cellClone.getStyle().clearHeight();
- cellClone.getStyle().clearWidth();
-
- cell.getParentElement().insertBefore(cellClone, cell);
- double requiredWidth = getBoundingWidth(cellClone);
- if (BrowserInfo.get().isIE()) {
- /*
- * IE browsers have some issues with subpixels. Occasionally
- * content is overflown even if not necessary. Increase the
- * counted required size by 0.01 just to be on the safe side.
- */
- requiredWidth += 0.01;
- }
-
- cellClone.removeFromParent();
-
- return requiredWidth;
- }
-
- /**
- * Gets the minimum width needed to display the cell properly.
- *
- * @param colIndex
- * index of column to measure
- * @param withContent
- * <code>true</code> if content is taken into account,
- * <code>false</code> if not
- * @return cell width needed for displaying correctly
- */
- double measureMinCellWidth(int colIndex, boolean withContent) {
- assert isAttached() : "Can't measure max width of cell, since Escalator is not attached to the DOM.";
-
- double minCellWidth = -1;
- NodeList<TableRowElement> rows = root.getRows();
-
- for (int row = 0; row < rows.getLength(); row++) {
-
- TableCellElement cell = rows.getItem(row).getCells()
- .getItem(colIndex);
-
- if (cell != null && !cellIsPartOfSpan(cell)) {
- double cellWidth = measureCellWidth(cell, withContent);
- minCellWidth = Math.max(minCellWidth, cellWidth);
- }
- }
-
- return minCellWidth;
- }
-
- private boolean cellIsPartOfSpan(TableCellElement cell) {
- boolean cellHasColspan = cell.getColSpan() > 1;
- boolean cellIsHidden = Display.NONE.getCssName()
- .equals(cell.getStyle().getDisplay());
- return cellHasColspan || cellIsHidden;
- }
-
- void refreshColumns(int index, int numberOfColumns) {
- if (getRowCount() > 0) {
- Range rowRange = Range.withLength(0, getRowCount());
- Range colRange = Range.withLength(index, numberOfColumns);
- refreshCells(rowRange, colRange);
- }
- }
-
- /**
- * The height of this table section.
- * <p>
- * Note that {@link Escalator#getBody() the body} will calculate its
- * height, while the others will return a precomputed value.
- *
- * @since 7.5.0
- *
- * @return the height of this table section
- */
- protected abstract double getHeightOfSection();
-
- /**
- * Gets the logical row index for the given table row element.
- *
- * @param tr
- * the table row element inside this container.
- * @return the logical index of the given element
- */
- public int getLogicalRowIndex(final TableRowElement tr) {
- return tr.getSectionRowIndex();
- };
-
- }
-
- private abstract class AbstractStaticRowContainer
- extends AbstractRowContainer {
-
- /** The height of the combined rows in the DOM. Never negative. */
- private double heightOfSection = 0;
-
- public AbstractStaticRowContainer(
- final TableSectionElement headElement) {
- super(headElement);
- }
-
- @Override
- public int getDomRowCount() {
- return root.getChildCount();
- }
-
- @Override
- protected void paintRemoveRows(final int index,
- final int numberOfRows) {
- for (int i = index; i < index + numberOfRows; i++) {
- final TableRowElement tr = root.getRows().getItem(index);
- paintRemoveRow(tr, index);
- }
- recalculateSectionHeight();
- }
-
- @Override
- protected TableRowElement getTrByVisualIndex(final int index)
- throws IndexOutOfBoundsException {
- if (index >= 0 && index < root.getChildCount()) {
- return root.getRows().getItem(index);
- } else {
- throw new IndexOutOfBoundsException(
- "No such visual index: " + index);
- }
- }
-
- @Override
- public void insertRows(int index, int numberOfRows) {
- super.insertRows(index, numberOfRows);
- recalculateElementSizes();
- applyHeightByRows();
- }
-
- @Override
- public void removeRows(int index, int numberOfRows) {
-
- /*
- * While the rows in a static section are removed, the scrollbar is
- * temporarily shrunk and then re-expanded. This leads to the fact
- * that the scroll position is scooted up a bit. This means that we
- * need to reset the position here.
- *
- * If Escalator, at some point, gets a JIT evaluation functionality,
- * this re-setting is a strong candidate for removal.
- */
- double oldScrollPos = verticalScrollbar.getScrollPos();
-
- super.removeRows(index, numberOfRows);
- recalculateElementSizes();
- applyHeightByRows();
-
- verticalScrollbar.setScrollPos(oldScrollPos);
- }
-
- @Override
- protected void reapplyDefaultRowHeights() {
- if (root.getChildCount() == 0) {
- return;
- }
-
- Profiler.enter(
- "Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
-
- Element tr = root.getRows().getItem(0);
- while (tr != null) {
- reapplyRowHeight(TableRowElement.as(tr), getDefaultRowHeight());
- tr = tr.getNextSiblingElement();
- }
-
- /*
- * Because all rows are immediately displayed in the static row
- * containers, the section's overall height has most probably
- * changed.
- */
- recalculateSectionHeight();
-
- Profiler.leave(
- "Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
- }
-
- @Override
- protected void recalculateSectionHeight() {
- Profiler.enter(
- "Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
-
- double newHeight = calculateTotalRowHeight();
- if (newHeight != heightOfSection) {
- heightOfSection = newHeight;
- sectionHeightCalculated();
-
- /*
- * We need to update the scrollbar dimension at this point. If
- * we are scrolled too far down and the static section shrinks,
- * the body will try to render rows that don't exist during
- * body.verifyEscalatorCount. This is because the logical row
- * indices are calculated from the scrollbar position.
- */
- verticalScrollbar.setOffsetSize(
- heightOfEscalator - header.getHeightOfSection()
- - footer.getHeightOfSection());
-
- body.verifyEscalatorCount();
- body.spacerContainer.updateSpacerDecosVisibility();
- }
-
- Profiler.leave(
- "Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
- }
-
- /**
- * Informs the row container that the height of its respective table
- * section has changed.
- * <p>
- * These calculations might affect some layouting logic, such as the
- * body is being offset by the footer, the footer needs to be readjusted
- * according to its height, and so on.
- * <p>
- * A table section is either header, body or footer.
- */
- protected abstract void sectionHeightCalculated();
-
- @Override
- protected void refreshCells(Range logicalRowRange, Range colRange) {
- assertArgumentsAreValidAndWithinRange(logicalRowRange.getStart(),
- logicalRowRange.length());
-
- if (!isAttached()) {
- return;
- }
-
- Profiler.enter("Escalator.AbstractStaticRowContainer.refreshCells");
-
- if (hasColumnAndRowData()) {
- for (int row = logicalRowRange.getStart(); row < logicalRowRange
- .getEnd(); row++) {
- final TableRowElement tr = getTrByVisualIndex(row);
- refreshRow(tr, row, colRange);
- }
- }
-
- Profiler.leave("Escalator.AbstractStaticRowContainer.refreshCells");
- }
-
- @Override
- protected void paintInsertRows(int visualIndex, int numberOfRows) {
- paintInsertStaticRows(visualIndex, numberOfRows);
- }
-
- @Override
- protected boolean rowCanBeFrozen(TableRowElement tr) {
- assert root.isOrHasChild(
- tr) : "Row does not belong to this table section";
- return true;
- }
-
- @Override
- protected double getHeightOfSection() {
- return Math.max(0, heightOfSection);
- }
- }
-
- private class HeaderRowContainer extends AbstractStaticRowContainer {
- public HeaderRowContainer(final TableSectionElement headElement) {
- super(headElement);
- }
-
- @Override
- protected void sectionHeightCalculated() {
- double heightOfSection = getHeightOfSection();
- bodyElem.getStyle().setMarginTop(heightOfSection, Unit.PX);
- spacerDecoContainer.getStyle().setMarginTop(heightOfSection,
- Unit.PX);
- verticalScrollbar.getElement().getStyle().setTop(heightOfSection,
- Unit.PX);
- headerDeco.getStyle().setHeight(heightOfSection, Unit.PX);
- }
-
- @Override
- protected String getCellElementTagName() {
- return "th";
- }
-
- @Override
- public void setStylePrimaryName(String primaryStyleName) {
- super.setStylePrimaryName(primaryStyleName);
- UIObject.setStylePrimaryName(root, primaryStyleName + "-header");
- }
- }
-
- private class FooterRowContainer extends AbstractStaticRowContainer {
- public FooterRowContainer(final TableSectionElement footElement) {
- super(footElement);
- }
-
- @Override
- public void setStylePrimaryName(String primaryStyleName) {
- super.setStylePrimaryName(primaryStyleName);
- UIObject.setStylePrimaryName(root, primaryStyleName + "-footer");
- }
-
- @Override
- protected String getCellElementTagName() {
- return "td";
- }
-
- @Override
- protected void sectionHeightCalculated() {
- double headerHeight = header.getHeightOfSection();
- double footerHeight = footer.getHeightOfSection();
- int vscrollHeight = (int) Math
- .floor(heightOfEscalator - headerHeight - footerHeight);
-
- final boolean horizontalScrollbarNeeded = columnConfiguration
- .calculateRowWidth() > widthOfEscalator;
- if (horizontalScrollbarNeeded) {
- vscrollHeight -= horizontalScrollbar.getScrollbarThickness();
- }
-
- footerDeco.getStyle().setHeight(footer.getHeightOfSection(),
- Unit.PX);
-
- verticalScrollbar.setOffsetSize(vscrollHeight);
- }
- }
-
- private class BodyRowContainerImpl extends AbstractRowContainer
- implements BodyRowContainer {
- /*
- * TODO [[optimize]]: check whether a native JsArray might be faster
- * than LinkedList
- */
- /**
- * The order in which row elements are rendered visually in the browser,
- * with the help of CSS tricks. Usually has nothing to do with the DOM
- * order.
- *
- * @see #sortDomElements()
- */
- private final LinkedList<TableRowElement> visualRowOrder = new LinkedList<>();
-
- /**
- * The logical index of the topmost row.
- *
- * @deprecated Use the accessors {@link #setTopRowLogicalIndex(int)},
- * {@link #updateTopRowLogicalIndex(int)} and
- * {@link #getTopRowLogicalIndex()} instead
- */
- @Deprecated
- private int topRowLogicalIndex = 0;
-
- /**
- * A callback function to be executed after new rows are added to the
- * escalator.
- */
- private Consumer<List<TableRowElement>> newEscalatorRowCallback;
-
- private void setTopRowLogicalIndex(int topRowLogicalIndex) {
- if (LogConfiguration.loggingIsEnabled(Level.INFO)) {
- Logger.getLogger("Escalator.BodyRowContainer")
- .fine("topRowLogicalIndex: " + this.topRowLogicalIndex
- + " -> " + topRowLogicalIndex);
- }
- assert topRowLogicalIndex >= 0 : "topRowLogicalIndex became negative (top left cell contents: "
- + visualRowOrder.getFirst().getCells().getItem(0)
- .getInnerText()
- + ") ";
- /*
- * if there's a smart way of evaluating and asserting the max index,
- * this would be a nice place to put it. I haven't found out an
- * effective and generic solution.
- */
-
- this.topRowLogicalIndex = topRowLogicalIndex;
- }
-
- public int getTopRowLogicalIndex() {
- return topRowLogicalIndex;
- }
-
- private void updateTopRowLogicalIndex(int diff) {
- setTopRowLogicalIndex(topRowLogicalIndex + diff);
- }
-
- private class DeferredDomSorter {
- private static final int SORT_DELAY_MILLIS = 50;
-
- // as it happens, 3 frames = 50ms @ 60fps.
- private static final int REQUIRED_FRAMES_PASSED = 3;
-
- private final AnimationCallback frameCounter = new AnimationCallback() {
- @Override
- public void execute(double timestamp) {
- framesPassed++;
- boolean domWasSorted = sortIfConditionsMet();
- if (!domWasSorted) {
- animationHandle = AnimationScheduler.get()
- .requestAnimationFrame(this);
- } else {
- waiting = false;
- }
- }
- };
-
- private int framesPassed;
- private double startTime;
- private AnimationHandle animationHandle;
-
- /** <code>true</code> if a sort is scheduled */
- public boolean waiting = false;
-
- public void reschedule() {
- waiting = true;
- resetConditions();
- animationHandle = AnimationScheduler.get()
- .requestAnimationFrame(frameCounter);
- }
-
- private boolean sortIfConditionsMet() {
- boolean enoughFramesHavePassed = framesPassed >= REQUIRED_FRAMES_PASSED;
- boolean enoughTimeHasPassed = (Duration.currentTimeMillis()
- - startTime) >= SORT_DELAY_MILLIS;
- boolean notTouchActivity = !scroller.touchHandlerBundle.touching;
- boolean conditionsMet = enoughFramesHavePassed
- && enoughTimeHasPassed && notTouchActivity;
-
- if (conditionsMet) {
- resetConditions();
- sortDomElements();
- }
-
- return conditionsMet;
- }
-
- private void resetConditions() {
- if (animationHandle != null) {
- animationHandle.cancel();
- animationHandle = null;
- }
- startTime = Duration.currentTimeMillis();
- framesPassed = 0;
- }
- }
-
- private DeferredDomSorter domSorter = new DeferredDomSorter();
-
- private final SpacerContainer spacerContainer = new SpacerContainer();
-
- public BodyRowContainerImpl(final TableSectionElement bodyElement) {
- super(bodyElement);
- }
-
- @Override
- public void setStylePrimaryName(String primaryStyleName) {
- super.setStylePrimaryName(primaryStyleName);
- UIObject.setStylePrimaryName(root, primaryStyleName + "-body");
- spacerContainer.setStylePrimaryName(primaryStyleName);
- }
-
- public void updateEscalatorRowsOnScroll() {
- if (visualRowOrder.isEmpty()) {
- return;
- }
-
- boolean rowsWereMoved = false;
-
- final double topElementPosition;
- final double nextRowBottomOffset;
- SpacerContainer.SpacerImpl topSpacer = spacerContainer
- .getSpacer(getTopRowLogicalIndex() - 1);
-
- if (topSpacer != null) {
- topElementPosition = topSpacer.getTop();
- nextRowBottomOffset = topSpacer.getHeight()
- + getDefaultRowHeight();
- } else {
- topElementPosition = getRowTop(visualRowOrder.getFirst());
- nextRowBottomOffset = getDefaultRowHeight();
- }
-
- // TODO [[mpixscroll]]
- final double scrollTop = tBodyScrollTop;
- final double viewportOffset = topElementPosition - scrollTop;
-
- /*
- * TODO [[optimize]] this if-else can most probably be refactored
- * into a neater block of code
- */
-
- if (viewportOffset > 0) {
- // there's empty room on top
-
- double rowPx = getRowHeightsSumBetweenPx(scrollTop,
- topElementPosition);
- int originalRowsToMove = (int) Math
- .ceil(rowPx / getDefaultRowHeight());
- int rowsToMove = Math.min(originalRowsToMove,
- visualRowOrder.size());
-
- final int end = visualRowOrder.size();
- final int start = end - rowsToMove;
- final int logicalRowIndex = getLogicalRowIndex(scrollTop);
-
- moveAndUpdateEscalatorRows(Range.between(start, end), 0,
- logicalRowIndex);
-
- setTopRowLogicalIndex(logicalRowIndex);
-
- rowsWereMoved = true;
- }
-
- else if (viewportOffset + nextRowBottomOffset <= 0) {
- /*
- * the viewport has been scrolled more than the topmost visual
- * row.
- */
-
- double rowPx = getRowHeightsSumBetweenPx(topElementPosition,
- scrollTop);
-
- int originalRowsToMove = (int) (rowPx / getDefaultRowHeight());
- int rowsToMove = Math.min(originalRowsToMove,
- visualRowOrder.size());
-
- int logicalRowIndex;
- if (rowsToMove < visualRowOrder.size()) {
- /*
- * We scroll so little that we can just keep adding the rows
- * below the current escalator
- */
- logicalRowIndex = getLogicalRowIndex(
- visualRowOrder.getLast()) + 1;
- } else {
- /*
- * Since we're moving all escalator rows, we need to
- * calculate the first logical row index from the scroll
- * position.
- */
- logicalRowIndex = getLogicalRowIndex(scrollTop);
- }
-
- /*
- * Since we're moving the viewport downwards, the visual index
- * is always at the bottom. Note: Due to how
- * moveAndUpdateEscalatorRows works, this will work out even if
- * we move all the rows, and try to place them "at the end".
- */
- final int targetVisualIndex = visualRowOrder.size();
-
- // make sure that we don't move rows over the data boundary
- boolean aRowWasLeftBehind = false;
- if (logicalRowIndex + rowsToMove > getRowCount()) {
- /*
- * TODO [[spacer]]: with constant row heights, there's
- * always exactly one row that will be moved beyond the data
- * source, when viewport is scrolled to the end. This,
- * however, isn't guaranteed anymore once row heights start
- * varying.
- */
- rowsToMove--;
- aRowWasLeftBehind = true;
- }
-
- /*
- * Make sure we don't scroll beyond the row content. This can
- * happen if we have spacers for the last rows.
- */
- rowsToMove = Math.max(0,
- Math.min(rowsToMove, getRowCount() - logicalRowIndex));
-
- moveAndUpdateEscalatorRows(Range.between(0, rowsToMove),
- targetVisualIndex, logicalRowIndex);
-
- if (aRowWasLeftBehind) {
- /*
- * To keep visualRowOrder as a spatially contiguous block of
- * rows, let's make sure that the one row we didn't move
- * visually still stays with the pack.
- */
- final Range strayRow = Range.withOnly(0);
-
- /*
- * We cannot trust getLogicalRowIndex, because it hasn't yet
- * been updated. But since we're leaving rows behind, it
- * means we've scrolled to the bottom. So, instead, we
- * simply count backwards from the end.
- */
- final int topLogicalIndex = getRowCount()
- - visualRowOrder.size();
- moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex);
- }
-
- final int naiveNewLogicalIndex = getTopRowLogicalIndex()
- + originalRowsToMove;
- final int maxLogicalIndex = getRowCount()
- - visualRowOrder.size();
- setTopRowLogicalIndex(
- Math.min(naiveNewLogicalIndex, maxLogicalIndex));
-
- rowsWereMoved = true;
- }
-
- if (rowsWereMoved) {
- fireRowVisibilityChangeEvent();
- domSorter.reschedule();
- }
- }
-
- private double getRowHeightsSumBetweenPx(double y1, double y2) {
- assert y1 < y2 : "y1 must be smaller than y2";
-
- double viewportPx = y2 - y1;
- double spacerPx = spacerContainer.getSpacerHeightsSumBetweenPx(y1,
- SpacerInclusionStrategy.PARTIAL, y2,
- SpacerInclusionStrategy.PARTIAL);
-
- return viewportPx - spacerPx;
- }
-
- private int getLogicalRowIndex(final double px) {
- double rowPx = px - spacerContainer.getSpacerHeightsSumUntilPx(px);
- return (int) (rowPx / getDefaultRowHeight());
- }
-
- @Override
- public void insertRows(int index, int numberOfRows) {
- super.insertRows(index, numberOfRows);
-
- if (heightMode == HeightMode.UNDEFINED) {
- setHeightByRows(getRowCount());
- }
- }
-
- @Override
- public void removeRows(int index, int numberOfRows) {
- super.removeRows(index, numberOfRows);
-
- if (heightMode == HeightMode.UNDEFINED) {
- setHeightByRows(getRowCount());
- }
- }
-
- @Override
- protected void paintInsertRows(final int index,
- final int numberOfRows) {
- if (numberOfRows == 0) {
- return;
- }
-
- spacerContainer.shiftSpacersByRows(index, numberOfRows);
-
- /*
- * TODO: this method should probably only add physical rows, and not
- * populate them - let everything be populated as appropriate by the
- * logic that follows.
- *
- * This also would lead to the fact that paintInsertRows wouldn't
- * need to return anything.
- */
- final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
- index, numberOfRows);
-
- /*
- * insertRows will always change the number of rows - update the
- * scrollbar sizes.
- */
- scroller.recalculateScrollbarsForVirtualViewport();
-
- final boolean addedRowsAboveCurrentViewport = index
- * getDefaultRowHeight() < getScrollTop();
- final boolean addedRowsBelowCurrentViewport = index
- * getDefaultRowHeight() > getScrollTop()
- + getHeightOfSection();
-
- if (addedRowsAboveCurrentViewport) {
- /*
- * We need to tweak the virtual viewport (scroll handle
- * positions, table "scroll position" and row locations), but
- * without re-evaluating any rows.
- */
-
- final double yDelta = numberOfRows * getDefaultRowHeight();
- moveViewportAndContent(yDelta);
- updateTopRowLogicalIndex(numberOfRows);
- }
-
- else if (addedRowsBelowCurrentViewport) {
- // NOOP, we already recalculated scrollbars.
- }
-
- else { // some rows were added inside the current viewport
-
- final int unupdatedLogicalStart = index + addedRows.size();
- final int visualOffset = getLogicalRowIndex(
- visualRowOrder.getFirst());
-
- /*
- * At this point, we have added new escalator rows, if so
- * needed.
- *
- * If more rows were added than the new escalator rows can
- * account for, we need to start to spin the escalator to update
- * the remaining rows aswell.
- */
- final int rowsStillNeeded = numberOfRows - addedRows.size();
-
- if (rowsStillNeeded > 0) {
- final Range unupdatedVisual = convertToVisual(
- Range.withLength(unupdatedLogicalStart,
- rowsStillNeeded));
- final int end = getDomRowCount();
- final int start = end - unupdatedVisual.length();
- final int visualTargetIndex = unupdatedLogicalStart
- - visualOffset;
- moveAndUpdateEscalatorRows(Range.between(start, end),
- visualTargetIndex, unupdatedLogicalStart);
-
- // move the surrounding rows to their correct places.
- double rowTop = (unupdatedLogicalStart + (end - start))
- * getDefaultRowHeight();
-
- // TODO: Get rid of this try/catch block by fixing the
- // underlying issue. The reason for this erroneous behavior
- // might be that Escalator actually works 'by mistake', and
- // the order of operations is, in fact, wrong.
- try {
- final ListIterator<TableRowElement> i = visualRowOrder
- .listIterator(
- visualTargetIndex + (end - start));
-
- int logicalRowIndexCursor = unupdatedLogicalStart;
- while (i.hasNext()) {
- rowTop += spacerContainer
- .getSpacerHeight(logicalRowIndexCursor++);
-
- final TableRowElement tr = i.next();
- setRowPosition(tr, 0, rowTop);
- rowTop += getDefaultRowHeight();
- }
- } catch (Exception e) {
- Logger logger = getLogger();
- logger.warning(
- "Ignored out-of-bounds row element access");
- logger.warning("Escalator state: start=" + start
- + ", end=" + end + ", visualTargetIndex="
- + visualTargetIndex + ", visualRowOrder.size()="
- + visualRowOrder.size());
- logger.warning(e.toString());
- }
- }
-
- fireRowVisibilityChangeEvent();
- sortDomElements();
- }
- }
-
- /**
- * Move escalator rows around, and make sure everything gets
- * appropriately repositioned and repainted.
- *
- * @param visualSourceRange
- * the range of rows to move to a new place
- * @param visualTargetIndex
- * the visual index where the rows will be placed to
- * @param logicalTargetIndex
- * the logical index to be assigned to the first moved row
- */
- private void moveAndUpdateEscalatorRows(final Range visualSourceRange,
- final int visualTargetIndex, final int logicalTargetIndex)
- throws IllegalArgumentException {
-
- if (visualSourceRange.isEmpty()) {
- return;
- }
-
- assert visualSourceRange.getStart() >= 0 : "Visual source start "
- + "must be 0 or greater (was "
- + visualSourceRange.getStart() + ")";
-
- assert logicalTargetIndex >= 0 : "Logical target must be 0 or "
- + "greater (was " + logicalTargetIndex + ")";
-
- assert visualTargetIndex >= 0 : "Visual target must be 0 or greater (was "
- + visualTargetIndex + ")";
-
- assert visualTargetIndex <= getDomRowCount() : "Visual target "
- + "must not be greater than the number of escalator rows (was "
- + visualTargetIndex + ", escalator rows " + getDomRowCount()
- + ")";
-
- assert logicalTargetIndex
- + visualSourceRange.length() <= getRowCount() : "Logical "
- + "target leads to rows outside of the data range ("
- + Range.withLength(logicalTargetIndex,
- visualSourceRange.length())
- + " goes beyond "
- + Range.withLength(0, getRowCount()) + ")";
-
- /*
- * Since we move a range into another range, the indices might move
- * about. Having 10 rows, if we move 0..1 to index 10 (to the end of
- * the collection), the target range will end up being 8..9, instead
- * of 10..11.
- *
- * This applies only if we move elements forward in the collection,
- * not backward.
- */
- final int adjustedVisualTargetIndex;
- if (visualSourceRange.getStart() < visualTargetIndex) {
- adjustedVisualTargetIndex = visualTargetIndex
- - visualSourceRange.length();
- } else {
- adjustedVisualTargetIndex = visualTargetIndex;
- }
-
- if (visualSourceRange.getStart() != adjustedVisualTargetIndex) {
-
- /*
- * Reorder the rows to their correct places within
- * visualRowOrder (unless rows are moved back to their original
- * places)
- */
-
- /*
- * TODO [[optimize]]: move whichever set is smaller: the ones
- * explicitly moved, or the others. So, with 10 escalator rows,
- * if we are asked to move idx[0..8] to the end of the list,
- * it's faster to just move idx[9] to the beginning.
- */
-
- final List<TableRowElement> removedRows = new ArrayList<>(
- visualSourceRange.length());
- for (int i = 0; i < visualSourceRange.length(); i++) {
- final TableRowElement tr = visualRowOrder
- .remove(visualSourceRange.getStart());
- removedRows.add(tr);
- }
- visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows);
- }
-
- { // Refresh the contents of the affected rows
- final ListIterator<TableRowElement> iter = visualRowOrder
- .listIterator(adjustedVisualTargetIndex);
- for (int logicalIndex = logicalTargetIndex; logicalIndex < logicalTargetIndex
- + visualSourceRange.length(); logicalIndex++) {
- final TableRowElement tr = iter.next();
- refreshRow(tr, logicalIndex);
- }
- }
-
- { // Reposition the rows that were moved
- double newRowTop = getRowTop(logicalTargetIndex);
-
- final ListIterator<TableRowElement> iter = visualRowOrder
- .listIterator(adjustedVisualTargetIndex);
- for (int i = 0; i < visualSourceRange.length(); i++) {
- final TableRowElement tr = iter.next();
- setRowPosition(tr, 0, newRowTop);
-
- newRowTop += getDefaultRowHeight();
- newRowTop += spacerContainer
- .getSpacerHeight(logicalTargetIndex + i);
- }
- }
- }
-
- /**
- * Adjust the scroll position and move the contained rows.
- * <p>
- * The difference between using this method and simply scrolling is that
- * this method "takes the rows and spacers with it" and renders them
- * appropriately. The viewport may be scrolled any arbitrary amount, and
- * the contents are moved appropriately, but always snapped into a
- * plausible place.
- * <p>
- * <dl>
- * <dt>Example 1</dt>
- * <dd>An Escalator with default row height 20px. Adjusting the scroll
- * position with 7.5px will move the viewport 7.5px down, but leave the
- * row where it is.</dd>
- * <dt>Example 2</dt>
- * <dd>An Escalator with default row height 20px. Adjusting the scroll
- * position with 27.5px will move the viewport 27.5px down, and place
- * the row at 20px.</dd>
- * </dl>
- *
- * @param yDelta
- * the delta of pixels by which to move the viewport and
- * content. A positive value moves everything downwards,
- * while a negative value moves everything upwards
- */
- public void moveViewportAndContent(final double yDelta) {
-
- if (yDelta == 0) {
- return;
- }
-
- double newTop = tBodyScrollTop + yDelta;
- verticalScrollbar.setScrollPos(newTop);
-
- final double defaultRowHeight = getDefaultRowHeight();
- double rowPxDelta = yDelta - (yDelta % defaultRowHeight);
- int rowIndexDelta = (int) (yDelta / defaultRowHeight);
- if (!WidgetUtil.pixelValuesEqual(rowPxDelta, 0)) {
-
- Collection<SpacerContainer.SpacerImpl> spacers = spacerContainer
- .getSpacersAfterPx(tBodyScrollTop,
- SpacerInclusionStrategy.PARTIAL);
- for (SpacerContainer.SpacerImpl spacer : spacers) {
- spacer.setPositionDiff(0, rowPxDelta);
- spacer.setRowIndex(spacer.getRow() + rowIndexDelta);
- }
-
- for (TableRowElement tr : visualRowOrder) {
- setRowPosition(tr, 0, getRowTop(tr) + rowPxDelta);
- }
- }
-
- setBodyScrollPosition(tBodyScrollLeft, newTop);
- }
-
- /**
- * Adds new physical escalator rows to the DOM at the given index if
- * there's still a need for more escalator rows.
- * <p>
- * If Escalator already is at (or beyond) max capacity, this method does
- * nothing to the DOM.
- *
- * @param index
- * the index at which to add new escalator rows.
- * <em>Note:</em>It is assumed that the index is both the
- * visual index and the logical index.
- * @param numberOfRows
- * the number of rows to add at <code>index</code>
- * @return a list of the added rows
- */
- private List<TableRowElement> fillAndPopulateEscalatorRowsIfNeeded(
- final int index, final int numberOfRows) {
-
- final int escalatorRowsStillFit = getMaxVisibleRowCount()
- - getDomRowCount();
- final int escalatorRowsNeeded = Math.min(numberOfRows,
- escalatorRowsStillFit);
-
- if (escalatorRowsNeeded > 0) {
-
- final List<TableRowElement> addedRows = paintInsertStaticRows(
- index, escalatorRowsNeeded);
- visualRowOrder.addAll(index, addedRows);
-
- double y = index * getDefaultRowHeight()
- + spacerContainer.getSpacerHeightsSumUntilIndex(index);
- for (int i = index; i < visualRowOrder.size(); i++) {
-
- final TableRowElement tr;
- if (i - index < addedRows.size()) {
- tr = addedRows.get(i - index);
- } else {
- tr = visualRowOrder.get(i);
- }
-
- setRowPosition(tr, 0, y);
- y += getDefaultRowHeight();
- y += spacerContainer.getSpacerHeight(i);
- }
-
- // Execute the registered callback function for newly created
- // rows
- Optional.ofNullable(newEscalatorRowCallback)
- .ifPresent(callback -> callback.accept(addedRows));
-
- return addedRows;
- } else {
- return Collections.emptyList();
- }
- }
-
- private int getMaxVisibleRowCount() {
- double heightOfSection = getHeightOfSection();
- // By including the possibly shown scrollbar height, we get a
- // consistent count and do not add/remove rows whenever a scrollbar
- // is shown
- heightOfSection += horizontalScrollbarDeco.getOffsetHeight();
- double defaultRowHeight = getDefaultRowHeight();
- final int maxVisibleRowCount = (int) Math
- .ceil(heightOfSection / defaultRowHeight) + 1;
-
- /*
- * maxVisibleRowCount can become negative if the headers and footers
- * start to overlap. This is a crazy situation, but Vaadin blinks
- * the components a lot, so it's feasible.
- */
- return Math.max(0, maxVisibleRowCount);
- }
-
- @Override
- protected void paintRemoveRows(final int index,
- final int numberOfRows) {
- if (numberOfRows == 0) {
- return;
- }
-
- final Range viewportRange = getVisibleRowRange();
- final Range removedRowsRange = Range.withLength(index,
- numberOfRows);
-
- /*
- * Removing spacers as the very first step will correct the
- * scrollbars and row offsets right away.
- *
- * TODO: actually, it kinda sounds like a Grid feature that a spacer
- * would be associated with a particular row. Maybe it would be
- * better to have a spacer separate from rows, and simply collapse
- * them if they happen to end up on top of each other. This would
- * probably make supporting the -1 row pretty easy, too.
- */
- spacerContainer.paintRemoveSpacers(removedRowsRange);
-
- final Range[] partitions = removedRowsRange
- .partitionWith(viewportRange);
- final Range removedAbove = partitions[0];
- final Range removedLogicalInside = partitions[1];
- final Range removedVisualInside = convertToVisual(
- removedLogicalInside);
-
- /*
- * TODO: extract the following if-block to a separate method. I'll
- * leave this be inlined for now, to make linediff-based code
- * reviewing easier. Probably will be moved in the following patch
- * set.
- */
-
- /*
- * Adjust scroll position in one of two scenarios:
- *
- * 1) Rows were removed above. Then we just need to adjust the
- * scrollbar by the height of the removed rows.
- *
- * 2) There are no logical rows above, and at least the first (if
- * not more) visual row is removed. Then we need to snap the scroll
- * position to the first visible row (i.e. reset scroll position to
- * absolute 0)
- *
- * The logic is optimized in such a way that the
- * moveViewportAndContent is called only once, to avoid extra
- * reflows, and thus the code might seem a bit obscure.
- */
- final boolean firstVisualRowIsRemoved = !removedVisualInside
- .isEmpty() && removedVisualInside.getStart() == 0;
-
- if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) {
- final double yDelta = removedAbove.length()
- * getDefaultRowHeight();
- final double firstLogicalRowHeight = getDefaultRowHeight();
- final boolean removalScrollsToShowFirstLogicalRow = verticalScrollbar
- .getScrollPos() - yDelta < firstLogicalRowHeight;
-
- if (removedVisualInside.isEmpty()
- && (!removalScrollsToShowFirstLogicalRow
- || !firstVisualRowIsRemoved)) {
- /*
- * rows were removed from above the viewport, so all we need
- * to do is to adjust the scroll position to account for the
- * removed rows
- */
- moveViewportAndContent(-yDelta);
- } else if (removalScrollsToShowFirstLogicalRow) {
- /*
- * It seems like we've removed all rows from above, and also
- * into the current viewport. This means we'll need to even
- * out the scroll position to exactly 0 (i.e. adjust by the
- * current negative scrolltop, presto!), so that it isn't
- * aligned funnily
- */
- moveViewportAndContent(-verticalScrollbar.getScrollPos());
- }
- }
-
- // ranges evaluated, let's do things.
- if (!removedVisualInside.isEmpty()) {
- int escalatorRowCount = body.getDomRowCount();
-
- /*
- * remember: the rows have already been subtracted from the row
- * count at this point
- */
- int rowsLeft = getRowCount();
- if (rowsLeft < escalatorRowCount) {
- /*
- * Remove extra DOM rows and refresh contents.
- */
- for (int i = escalatorRowCount - 1; i >= rowsLeft; i--) {
- final TableRowElement tr = visualRowOrder.remove(i);
- paintRemoveRow(tr, i);
- removeRowPosition(tr);
- }
-
- // Move rest of the rows to the Escalator's top
- Range visualRange = Range.withLength(0,
- visualRowOrder.size());
- moveAndUpdateEscalatorRows(visualRange, 0, 0);
-
- sortDomElements();
- setTopRowLogicalIndex(0);
-
- scroller.recalculateScrollbarsForVirtualViewport();
-
- fireRowVisibilityChangeEvent();
- return;
- }
-
- else {
- // No escalator rows need to be removed.
-
- /*
- * Two things (or a combination thereof) can happen:
- *
- * 1) We're scrolled to the bottom, the last rows are
- * removed. SOLUTION: moveAndUpdateEscalatorRows the
- * bottommost rows, and place them at the top to be
- * refreshed.
- *
- * 2) We're scrolled somewhere in the middle, arbitrary rows
- * are removed. SOLUTION: moveAndUpdateEscalatorRows the
- * removed rows, and place them at the bottom to be
- * refreshed.
- *
- * Since a combination can also happen, we need to handle
- * this in a smart way, all while avoiding
- * double-refreshing.
- */
-
- final double contentBottom = getRowCount()
- * getDefaultRowHeight();
- final double viewportBottom = tBodyScrollTop
- + getHeightOfSection();
- if (viewportBottom <= contentBottom) {
- /*
- * We're in the middle of the row container, everything
- * is added to the bottom
- */
- paintRemoveRowsAtMiddle(removedLogicalInside,
- removedVisualInside, 0);
- }
-
- else if (removedVisualInside.contains(0)
- && numberOfRows >= visualRowOrder.size()) {
- /*
- * We're removing so many rows that the viewport is
- * pushed up more than a screenful. This means we can
- * simply scroll up and everything will work without a
- * sweat.
- */
-
- double left = horizontalScrollbar.getScrollPos();
- double top = contentBottom
- - visualRowOrder.size() * getDefaultRowHeight();
- setBodyScrollPosition(left, top);
-
- Range allEscalatorRows = Range.withLength(0,
- visualRowOrder.size());
- int logicalTargetIndex = getRowCount()
- - allEscalatorRows.length();
- moveAndUpdateEscalatorRows(allEscalatorRows, 0,
- logicalTargetIndex);
-
- /*
- * moveAndUpdateEscalatorRows recalculates the rows, but
- * logical top row index bookkeeping is handled in this
- * method.
- *
- * TODO: Redesign how to keep it easy to track this.
- */
- updateTopRowLogicalIndex(
- -removedLogicalInside.length());
-
- /*
- * Scrolling the body to the correct location will be
- * fixed automatically. Because the amount of rows is
- * decreased, the viewport is pushed up as the scrollbar
- * shrinks. So no need to do anything there.
- *
- * TODO [[optimize]]: This might lead to a double body
- * refresh. Needs investigation.
- */
- }
-
- else if (contentBottom
- + (numberOfRows * getDefaultRowHeight())
- - viewportBottom < getDefaultRowHeight()) {
- /*
- * We're at the end of the row container, everything is
- * added to the top.
- */
-
- /*
- * FIXME [[spacer]]: above if-clause is coded to only
- * work with default row heights - will not work with
- * variable row heights
- */
-
- paintRemoveRowsAtBottom(removedLogicalInside,
- removedVisualInside);
- updateTopRowLogicalIndex(
- -removedLogicalInside.length());
- }
-
- else {
- /*
- * We're in a combination, where we need to both scroll
- * up AND show new rows at the bottom.
- *
- * Example: Scrolled down to show the second to last
- * row. Remove two. Viewport scrolls up, revealing the
- * row above row. The last element collapses up and into
- * view.
- *
- * Reminder: this use case handles only the case when
- * there are enough escalator rows to still render a
- * full view. I.e. all escalator rows will _always_ be
- * populated
- */
- /*-
- * 1 1 |1| <- newly rendered
- * |2| |2| |2|
- * |3| ==> |*| ==> |5| <- newly rendered
- * |4| |*|
- * 5 5
- *
- * 1 1 |1| <- newly rendered
- * |2| |*| |4|
- * |3| ==> |*| ==> |5| <- newly rendered
- * |4| |4|
- * 5 5
- */
-
- /*
- * STEP 1:
- *
- * reorganize deprecated escalator rows to bottom, but
- * don't re-render anything yet
- */
- /*-
- * 1 1 1
- * |2| |*| |4|
- * |3| ==> |*| ==> |*|
- * |4| |4| |*|
- * 5 5 5
- */
- double newTop = getRowTop(visualRowOrder
- .get(removedVisualInside.getStart()));
- for (int i = 0; i < removedVisualInside.length(); i++) {
- final TableRowElement tr = visualRowOrder
- .remove(removedVisualInside.getStart());
- visualRowOrder.addLast(tr);
- }
-
- for (int i = removedVisualInside
- .getStart(); i < escalatorRowCount; i++) {
- final TableRowElement tr = visualRowOrder.get(i);
- setRowPosition(tr, 0, (int) newTop);
- newTop += getDefaultRowHeight();
- newTop += spacerContainer.getSpacerHeight(
- i + removedLogicalInside.getStart());
- }
-
- /*
- * STEP 2:
- *
- * manually scroll
- */
- /*-
- * 1 |1| <-- newly rendered (by scrolling)
- * |4| |4|
- * |*| ==> |*|
- * |*|
- * 5 5
- */
- final double newScrollTop = contentBottom
- - getHeightOfSection();
- setScrollTop(newScrollTop);
- /*
- * Manually call the scroll handler, so we get immediate
- * effects in the escalator.
- */
- scroller.onScroll();
-
- /*
- * Move the bottommost (n+1:th) escalator row to top,
- * because scrolling up doesn't handle that for us
- * automatically
- */
- moveAndUpdateEscalatorRows(
- Range.withOnly(escalatorRowCount - 1), 0,
- getLogicalRowIndex(visualRowOrder.getFirst())
- - 1);
- updateTopRowLogicalIndex(-1);
-
- /*
- * STEP 3:
- *
- * update remaining escalator rows
- */
- /*-
- * |1| |1|
- * |4| ==> |4|
- * |*| |5| <-- newly rendered
- *
- * 5
- */
-
- final int rowsScrolled = (int) (Math
- .ceil((viewportBottom - contentBottom)
- / getDefaultRowHeight()));
- final int start = escalatorRowCount
- - (removedVisualInside.length() - rowsScrolled);
- final Range visualRefreshRange = Range.between(start,
- escalatorRowCount);
- final int logicalTargetIndex = getLogicalRowIndex(
- visualRowOrder.getFirst()) + start;
- // in-place move simply re-renders the rows.
- moveAndUpdateEscalatorRows(visualRefreshRange, start,
- logicalTargetIndex);
- }
- }
-
- fireRowVisibilityChangeEvent();
- sortDomElements();
- }
-
- updateTopRowLogicalIndex(-removedAbove.length());
-
- /*
- * this needs to be done after the escalator has been shrunk down,
- * or it won't work correctly (due to setScrollTop invocation)
- */
- scroller.recalculateScrollbarsForVirtualViewport();
- }
-
- private void paintRemoveRowsAtMiddle(final Range removedLogicalInside,
- final Range removedVisualInside, final int logicalOffset) {
- /*-
- * : : :
- * |2| |2| |2|
- * |3| ==> |*| ==> |4|
- * |4| |4| |6| <- newly rendered
- * : : :
- */
-
- final int escalatorRowCount = visualRowOrder.size();
-
- final int logicalTargetIndex = getLogicalRowIndex(
- visualRowOrder.getLast())
- - (removedVisualInside.length() - 1) + logicalOffset;
- moveAndUpdateEscalatorRows(removedVisualInside, escalatorRowCount,
- logicalTargetIndex);
-
- // move the surrounding rows to their correct places.
- final ListIterator<TableRowElement> iterator = visualRowOrder
- .listIterator(removedVisualInside.getStart());
-
- double rowTop = getRowTop(
- removedLogicalInside.getStart() + logicalOffset);
- for (int i = removedVisualInside.getStart(); i < escalatorRowCount
- - removedVisualInside.length(); i++) {
- final TableRowElement tr = iterator.next();
- setRowPosition(tr, 0, rowTop);
- rowTop += getDefaultRowHeight();
- rowTop += spacerContainer
- .getSpacerHeight(i + removedLogicalInside.getStart());
- }
- }
-
- private void paintRemoveRowsAtBottom(final Range removedLogicalInside,
- final Range removedVisualInside) {
- /*-
- * :
- * : : |4| <- newly rendered
- * |5| |5| |5|
- * |6| ==> |*| ==> |7|
- * |7| |7|
- */
-
- final int logicalTargetIndex = getLogicalRowIndex(
- visualRowOrder.getFirst()) - removedVisualInside.length();
- moveAndUpdateEscalatorRows(removedVisualInside, 0,
- logicalTargetIndex);
-
- // move the surrounding rows to their correct places.
- int firstUpdatedIndex = removedVisualInside.getEnd();
- final ListIterator<TableRowElement> iterator = visualRowOrder
- .listIterator(firstUpdatedIndex);
-
- double rowTop = getRowTop(removedLogicalInside.getStart());
- int i = 0;
- while (iterator.hasNext()) {
- final TableRowElement tr = iterator.next();
- setRowPosition(tr, 0, rowTop);
- rowTop += getDefaultRowHeight();
- rowTop += spacerContainer
- .getSpacerHeight(firstUpdatedIndex + i++);
- }
- }
-
- @Override
- public int getLogicalRowIndex(final TableRowElement tr) {
- assert tr
- .getParentNode() == root : "The given element isn't a row element in the body";
- int internalIndex = visualRowOrder.indexOf(tr);
- return getTopRowLogicalIndex() + internalIndex;
- }
-
- @Override
- protected void recalculateSectionHeight() {
- // NOOP for body, since it doesn't make any sense.
- }
-
- /**
- * Adjusts the row index and number to be relevant for the current
- * virtual viewport.
- * <p>
- * It converts a logical range of rows index to the matching visual
- * range, truncating the resulting range with the viewport.
- * <p>
- * <ul>
- * <li>Escalator contains logical rows 0..100
- * <li>Current viewport showing logical rows 20..29
- * <li>convertToVisual([20..29]) → [0..9]
- * <li>convertToVisual([15..24]) → [0..4]
- * <li>convertToVisual([25..29]) → [5..9]
- * <li>convertToVisual([26..39]) → [6..9]
- * <li>convertToVisual([0..5]) → [0..-1] <em>(empty)</em>
- * <li>convertToVisual([35..1]) → [0..-1] <em>(empty)</em>
- * <li>convertToVisual([0..100]) → [0..9]
- * </ul>
- *
- * @return a logical range converted to a visual range, truncated to the
- * current viewport. The first visual row has the index 0.
- */
- private Range convertToVisual(final Range logicalRange) {
-
- if (logicalRange.isEmpty()) {
- return logicalRange;
- } else if (visualRowOrder.isEmpty()) {
- // empty range
- return Range.withLength(0, 0);
- }
-
- /*
- * TODO [[spacer]]: these assumptions will be totally broken with
- * spacers.
- */
- final int maxVisibleRowCount = getMaxVisibleRowCount();
- final int currentTopRowIndex = getLogicalRowIndex(
- visualRowOrder.getFirst());
-
- final Range[] partitions = logicalRange.partitionWith(
- Range.withLength(currentTopRowIndex, maxVisibleRowCount));
- final Range insideRange = partitions[1];
- return insideRange.offsetBy(-currentTopRowIndex);
- }
-
- @Override
- protected String getCellElementTagName() {
- return "td";
- }
-
- @Override
- protected double getHeightOfSection() {
- final int tableHeight = tableWrapper.getOffsetHeight();
- final double footerHeight = footer.getHeightOfSection();
- final double headerHeight = header.getHeightOfSection();
-
- double heightOfSection = tableHeight - footerHeight - headerHeight;
- return Math.max(0, heightOfSection);
- }
-
- @Override
- protected void refreshCells(Range logicalRowRange, Range colRange) {
- Profiler.enter("Escalator.BodyRowContainer.refreshRows");
-
- final Range visualRange = convertToVisual(logicalRowRange);
-
- if (!visualRange.isEmpty()) {
- final int firstLogicalRowIndex = getLogicalRowIndex(
- visualRowOrder.getFirst());
- for (int rowNumber = visualRange
- .getStart(); rowNumber < visualRange
- .getEnd(); rowNumber++) {
- refreshRow(visualRowOrder.get(rowNumber),
- firstLogicalRowIndex + rowNumber, colRange);
- }
- }
-
- Profiler.leave("Escalator.BodyRowContainer.refreshRows");
- }
-
- @Override
- protected TableRowElement getTrByVisualIndex(final int index)
- throws IndexOutOfBoundsException {
- if (index >= 0 && index < visualRowOrder.size()) {
- return visualRowOrder.get(index);
- } else {
- throw new IndexOutOfBoundsException(
- "No such visual index: " + index);
- }
- }
-
- @Override
- public TableRowElement getRowElement(int index) {
- if (index < 0 || index >= getRowCount()) {
- throw new IndexOutOfBoundsException(
- "No such logical index: " + index);
- }
- int visualIndex = index
- - getLogicalRowIndex(visualRowOrder.getFirst());
- if (visualIndex >= 0 && visualIndex < visualRowOrder.size()) {
- return super.getRowElement(visualIndex);
- } else {
- throw new IllegalStateException("Row with logical index "
- + index + " is currently not available in the DOM");
- }
- }
-
- private void setBodyScrollPosition(final double scrollLeft,
- final double scrollTop) {
- tBodyScrollLeft = scrollLeft;
- tBodyScrollTop = scrollTop;
- position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop);
- position.set(spacerDecoContainer, 0, -tBodyScrollTop);
- }
-
- /**
- * Make sure that there is a correct amount of escalator rows: Add more
- * if needed, or remove any superfluous ones.
- * <p>
- * This method should be called when e.g. the height of the Escalator
- * changes.
- * <p>
- * <em>Note:</em> This method will make sure that the escalator rows are
- * placed in the proper places. By default new rows are added below, but
- * if the content is scrolled down, the rows are populated on top
- * instead.
- */
- public void verifyEscalatorCount() {
- /*
- * This method indeed has a smell very similar to paintRemoveRows
- * and paintInsertRows.
- *
- * Unfortunately, those the code can't trivially be shared, since
- * there are some slight differences in the respective
- * responsibilities. The "paint" methods fake the addition and
- * removal of rows, and make sure to either push existing data out
- * of view, or draw new data into view. Only in some special cases
- * will the DOM element count change.
- *
- * This method, however, has the explicit responsibility to verify
- * that when "something" happens, we still have the correct amount
- * of escalator rows in the DOM, and if not, we make sure to modify
- * that count. Only in some special cases do we need to take into
- * account other things than simply modifying the DOM element count.
- */
-
- Profiler.enter("Escalator.BodyRowContainer.verifyEscalatorCount");
-
- if (!isAttached()) {
- return;
- }
-
- final int maxVisibleRowCount = getMaxVisibleRowCount();
- final int neededEscalatorRows = Math.min(maxVisibleRowCount,
- body.getRowCount());
- final int neededEscalatorRowsDiff = neededEscalatorRows
- - visualRowOrder.size();
-
- if (neededEscalatorRowsDiff > 0) {
- // needs more
-
- /*
- * This is a workaround for the issue where we might be scrolled
- * to the bottom, and the widget expands beyond the content
- * range
- */
-
- final int index = visualRowOrder.size();
- final int nextLastLogicalIndex;
- if (!visualRowOrder.isEmpty()) {
- nextLastLogicalIndex = getLogicalRowIndex(
- visualRowOrder.getLast()) + 1;
- } else {
- nextLastLogicalIndex = 0;
- }
-
- final boolean contentWillFit = nextLastLogicalIndex < getRowCount()
- - neededEscalatorRowsDiff;
- if (contentWillFit) {
- final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
- index, neededEscalatorRowsDiff);
-
- /*
- * Since fillAndPopulateEscalatorRowsIfNeeded operates on
- * the assumption that index == visual index == logical
- * index, we thank for the added escalator rows, but since
- * they're painted in the wrong CSS position, we need to
- * move them to their actual locations.
- *
- * Note: this is the second (see body.paintInsertRows)
- * occasion where fillAndPopulateEscalatorRowsIfNeeded would
- * behave "more correctly" if it only would add escalator
- * rows to the DOM and appropriate bookkeping, and not
- * actually populate them :/
- */
- moveAndUpdateEscalatorRows(
- Range.withLength(index, addedRows.size()), index,
- nextLastLogicalIndex);
- } else {
- /*
- * TODO [[optimize]]
- *
- * We're scrolled so far down that all rows can't be simply
- * appended at the end, since we might start displaying
- * escalator rows that don't exist. To avoid the mess that
- * is body.paintRemoveRows, this is a dirty hack that dumbs
- * the problem down to a more basic and already-solved
- * problem:
- *
- * 1) scroll all the way up 2) add the missing escalator
- * rows 3) scroll back to the original position.
- *
- * Letting the browser scroll back to our original position
- * will automatically solve any possible overflow problems,
- * since the browser will not allow us to scroll beyond the
- * actual content.
- */
-
- final double oldScrollTop = getScrollTop();
- setScrollTop(0);
- scroller.onScroll();
- fillAndPopulateEscalatorRowsIfNeeded(index,
- neededEscalatorRowsDiff);
- setScrollTop(oldScrollTop);
- scroller.onScroll();
- }
- }
-
- else if (neededEscalatorRowsDiff < 0) {
- // needs less
-
- final ListIterator<TableRowElement> iter = visualRowOrder
- .listIterator(visualRowOrder.size());
- for (int i = 0; i < -neededEscalatorRowsDiff; i++) {
- final Element last = iter.previous();
- last.removeFromParent();
- iter.remove();
- }
-
- /*
- * If we were scrolled to the bottom so that we didn't have an
- * extra escalator row at the bottom, we'll probably end up with
- * blank space at the bottom of the escalator, and one extra row
- * above the header.
- *
- * Experimentation idea #1: calculate "scrollbottom" vs content
- * bottom and remove one row from top, rest from bottom. This
- * FAILED, since setHeight has already happened, thus we never
- * will detect ourselves having been scrolled all the way to the
- * bottom.
- */
-
- if (!visualRowOrder.isEmpty()) {
- final double firstRowTop = getRowTop(
- visualRowOrder.getFirst());
- final double firstRowMinTop = tBodyScrollTop
- - getDefaultRowHeight();
- if (firstRowTop < firstRowMinTop) {
- final int newLogicalIndex = getLogicalRowIndex(
- visualRowOrder.getLast()) + 1;
- moveAndUpdateEscalatorRows(Range.withOnly(0),
- visualRowOrder.size(), newLogicalIndex);
- }
- }
- }
-
- if (neededEscalatorRowsDiff != 0) {
- fireRowVisibilityChangeEvent();
- }
-
- Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount");
- }
-
- @Override
- protected void reapplyDefaultRowHeights() {
- if (visualRowOrder.isEmpty()) {
- return;
- }
-
- Profiler.enter(
- "Escalator.BodyRowContainer.reapplyDefaultRowHeights");
-
- /* step 1: resize and reposition rows */
- for (int i = 0; i < visualRowOrder.size(); i++) {
- TableRowElement tr = visualRowOrder.get(i);
- reapplyRowHeight(tr, getDefaultRowHeight());
-
- final int logicalIndex = getTopRowLogicalIndex() + i;
- setRowPosition(tr, 0, logicalIndex * getDefaultRowHeight());
- }
-
- /*
- * step 2: move scrollbar so that it corresponds to its previous
- * place
- */
-
- /*
- * This ratio needs to be calculated with the scrollsize (not max
- * scroll position) in order to align the top row with the new
- * scroll position.
- */
- double scrollRatio = verticalScrollbar.getScrollPos()
- / verticalScrollbar.getScrollSize();
- scroller.recalculateScrollbarsForVirtualViewport();
- verticalScrollbar.setScrollPos((int) (getDefaultRowHeight()
- * getRowCount() * scrollRatio));
- setBodyScrollPosition(horizontalScrollbar.getScrollPos(),
- verticalScrollbar.getScrollPos());
- scroller.onScroll();
-
- /*
- * step 3: make sure we have the correct amount of escalator rows.
- */
- verifyEscalatorCount();
-
- int logicalLogical = (int) (getRowTop(visualRowOrder.getFirst())
- / getDefaultRowHeight());
- setTopRowLogicalIndex(logicalLogical);
-
- Profiler.leave(
- "Escalator.BodyRowContainer.reapplyDefaultRowHeights");
- }
-
- /**
- * Sorts the rows in the DOM to correspond to the visual order.
- *
- * @see #visualRowOrder
- */
- private void sortDomElements() {
- final String profilingName = "Escalator.BodyRowContainer.sortDomElements";
- Profiler.enter(profilingName);
-
- /*
- * Focus is lost from an element if that DOM element is (or any of
- * its parents are) removed from the document. Therefore, we sort
- * everything around that row instead.
- */
- final TableRowElement focusedRow = getRowWithFocus();
-
- if (focusedRow != null) {
- assert focusedRow
- .getParentElement() == root : "Trying to sort around a row that doesn't exist in body";
- assert visualRowOrder.contains(focusedRow)
- || body.spacerContainer.isSpacer(
- focusedRow) : "Trying to sort around a row that doesn't exist in visualRowOrder or is not a spacer.";
- }
-
- /*
- * Two cases handled simultaneously:
- *
- * 1) No focus on rows. We iterate visualRowOrder backwards, and
- * take the respective element in the DOM, and place it as the first
- * child in the body element. Then we take the next-to-last from
- * visualRowOrder, and put that first, pushing the previous row as
- * the second child. And so on...
- *
- * 2) Focus on some row within Escalator body. Again, we iterate
- * visualRowOrder backwards. This time, we use the focused row as a
- * pivot: Instead of placing rows from the bottom of visualRowOrder
- * and placing it first, we place it underneath the focused row.
- * Once we hit the focused row, we don't move it (to not reset
- * focus) but change sorting mode. After that, we place all rows as
- * the first child.
- */
-
- List<TableRowElement> orderedBodyRows = new ArrayList<>(
- visualRowOrder);
- Map<Integer, SpacerContainer.SpacerImpl> spacers = body.spacerContainer
- .getSpacers();
-
- /*
- * Start at -1 to include a spacer that is rendered above the
- * viewport, but its parent row is still not shown
- */
- for (int i = -1; i < visualRowOrder.size(); i++) {
- SpacerContainer.SpacerImpl spacer = spacers
- .remove(Integer.valueOf(getTopRowLogicalIndex() + i));
-
- if (spacer != null) {
- orderedBodyRows.add(i + 1, spacer.getRootElement());
- spacer.show();
- }
- }
- /*
- * At this point, invisible spacers aren't reordered, so their
- * position in the DOM will remain undefined.
- */
-
- // If a spacer was not reordered, it means that it's out of view.
- for (SpacerContainer.SpacerImpl unmovedSpacer : spacers.values()) {
- unmovedSpacer.hide();
- }
-
- /*
- * If we have a focused row, start in the mode where we put
- * everything underneath that row. Otherwise, all rows are placed as
- * first child.
- */
- boolean insertFirst = (focusedRow == null);
-
- final ListIterator<TableRowElement> i = orderedBodyRows
- .listIterator(orderedBodyRows.size());
- while (i.hasPrevious()) {
- TableRowElement tr = i.previous();
-
- if (tr == focusedRow) {
- insertFirst = true;
- } else if (insertFirst) {
- // remove row explicitly to work around an IE11 bug (#9850)
- if (BrowserInfo.get().isIE11() && tr
- .equals(root.getFirstChildElement())) {
- root.removeChild(tr);
- }
- root.insertFirst(tr);
- } else {
- root.insertAfter(tr, focusedRow);
- }
- }
-
- Profiler.leave(profilingName);
- }
-
- /**
- * Get the {@literal <tbody>} row that contains (or has) focus.
- *
- * @return The {@literal <tbody>} row that contains a focused DOM
- * element, or <code>null</code> if focus is outside of a body
- * row.
- */
- private TableRowElement getRowWithFocus() {
- TableRowElement rowContainingFocus = null;
-
- final Element focusedElement = WidgetUtil.getFocusedElement();
-
- if (focusedElement != null && root.isOrHasChild(focusedElement)) {
- Element e = focusedElement;
-
- while (e != null && e != root) {
- /*
- * You never know if there's several tables embedded in a
- * cell... We'll take the deepest one.
- */
- if (TableRowElement.is(e)) {
- rowContainingFocus = TableRowElement.as(e);
- }
- e = e.getParentElement();
- }
- }
-
- return rowContainingFocus;
- }
-
- @Override
- public Cell getCell(Element element) {
- Cell cell = super.getCell(element);
- if (cell == null) {
- return null;
- }
-
- // Convert DOM coordinates to logical coordinates for rows
- TableRowElement rowElement = (TableRowElement) cell.getElement()
- .getParentElement();
- return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(),
- cell.getElement());
- }
-
- @Override
- public void setSpacer(int rowIndex, double height)
- throws IllegalArgumentException {
- spacerContainer.setSpacer(rowIndex, height);
- }
-
- @Override
- public void setSpacerUpdater(SpacerUpdater spacerUpdater)
- throws IllegalArgumentException {
- spacerContainer.setSpacerUpdater(spacerUpdater);
- }
-
- @Override
- public SpacerUpdater getSpacerUpdater() {
- return spacerContainer.getSpacerUpdater();
- }
-
- /**
- * <em>Calculates</em> the correct top position of a row at a logical
- * index, regardless if there is one there or not.
- * <p>
- * A correct result requires that both {@link #getDefaultRowHeight()} is
- * consistent, and the placement and height of all spacers above the
- * given logical index are consistent.
- *
- * @param logicalIndex
- * the logical index of the row for which to calculate the
- * top position
- * @return the position at which to place a row in {@code logicalIndex}
- * @see #getRowTop(TableRowElement)
- */
- private double getRowTop(int logicalIndex) {
- double top = spacerContainer
- .getSpacerHeightsSumUntilIndex(logicalIndex);
- return top + (logicalIndex * getDefaultRowHeight());
- }
-
- public void shiftRowPositions(int row, double diff) {
- for (TableRowElement tr : getVisibleRowsAfter(row)) {
- setRowPosition(tr, 0, getRowTop(tr) + diff);
- }
- }
-
- private List<TableRowElement> getVisibleRowsAfter(int logicalRow) {
- Range visibleRowLogicalRange = getVisibleRowRange();
-
- boolean allRowsAreInView = logicalRow < visibleRowLogicalRange
- .getStart();
- boolean noRowsAreInView = logicalRow >= visibleRowLogicalRange
- .getEnd() - 1;
-
- if (allRowsAreInView) {
- return Collections.unmodifiableList(visualRowOrder);
- } else if (noRowsAreInView) {
- return Collections.emptyList();
- } else {
- int fromIndex = (logicalRow - visibleRowLogicalRange.getStart())
- + 1;
- int toIndex = visibleRowLogicalRange.length();
- List<TableRowElement> sublist = visualRowOrder
- .subList(fromIndex, toIndex);
- return Collections.unmodifiableList(sublist);
- }
- }
-
- @Override
- public int getDomRowCount() {
- return root.getChildCount()
- - spacerContainer.getSpacersInDom().size();
- }
-
- @Override
- protected boolean rowCanBeFrozen(TableRowElement tr) {
- return visualRowOrder.contains(tr);
- }
-
- void reapplySpacerWidths() {
- spacerContainer.reapplySpacerWidths();
- }
-
- void scrollToSpacer(int spacerIndex, ScrollDestination destination,
- int padding) {
- spacerContainer.scrollToSpacer(spacerIndex, destination, padding);
- }
-
- @Override
- public void setNewRowCallback(
- Consumer<List<TableRowElement>> callback) {
- newEscalatorRowCallback = callback;
- }
- }
-
- private class ColumnConfigurationImpl implements ColumnConfiguration {
- public class Column {
- public static final double DEFAULT_COLUMN_WIDTH_PX = 100;
-
- private double definedWidth = -1;
- private double calculatedWidth = DEFAULT_COLUMN_WIDTH_PX;
- private boolean measuringRequested = false;
-
- public void setWidth(double px) {
- Profiler.enter(
- "Escalator.ColumnConfigurationImpl.Column.setWidth");
-
- definedWidth = px;
-
- if (px < 0) {
- if (isAttached()) {
- calculateWidth();
- } else {
- /*
- * the column's width is calculated at Escalator.onLoad
- * via measureAndSetWidthIfNeeded!
- */
- measuringRequested = true;
- }
- } else {
- calculatedWidth = px;
- }
-
- Profiler.leave(
- "Escalator.ColumnConfigurationImpl.Column.setWidth");
- }
-
- public double getDefinedWidth() {
- return definedWidth;
- }
-
- /**
- * Returns the actual width in the DOM.
- *
- * @return the width in pixels in the DOM. Returns -1 if the column
- * needs measuring, but has not been yet measured
- */
- public double getCalculatedWidth() {
- /*
- * This might return an untrue value (e.g. during init/onload),
- * since we haven't had a proper chance to actually calculate
- * widths yet.
- *
- * This is fixed during Escalator.onLoad, by the call to
- * "measureAndSetWidthIfNeeded", which fixes "everything".
- */
- if (!measuringRequested) {
- return calculatedWidth;
- } else {
- return -1;
- }
- }
-
- /**
- * Checks if the column needs measuring, and then measures it.
- * <p>
- * Called by {@link Escalator#onLoad()}.
- */
- public boolean measureAndSetWidthIfNeeded() {
- assert isAttached() : "Column.measureAndSetWidthIfNeeded() was called even though Escalator was not attached!";
-
- if (measuringRequested) {
- measuringRequested = false;
- setWidth(definedWidth);
- return true;
- }
- return false;
- }
-
- private void calculateWidth() {
- calculatedWidth = getMaxCellWidth(columns.indexOf(this));
- }
- }
-
- private final List<Column> columns = new ArrayList<>();
- private int frozenColumns = 0;
-
- /*
- * TODO: this is a bit of a duplicate functionality with the
- * Column.calculatedWidth caching. Probably should use one or the other,
- * not both
- */
- /**
- * A cached array of all the calculated column widths.
- *
- * @see #getCalculatedColumnWidths()
- */
- private double[] widthsArray = null;
-
- /**
- * {@inheritDoc}
- * <p>
- * <em>Implementation detail:</em> This method does no DOM modifications
- * (i.e. is very cheap to call) if there are no rows in the DOM when
- * this method is called.
- *
- * @see #hasSomethingInDom()
- */
- @Override
- public void removeColumns(final int index, final int numberOfColumns) {
- if (numberOfColumns == 0) {
- return;
- }
-
- // Validate
- assertArgumentsAreValidAndWithinRange(index, numberOfColumns);
-
- // Move the horizontal scrollbar to the left, if removed columns are
- // to the left of the viewport
- removeColumnsAdjustScrollbar(index, numberOfColumns);
-
- // Remove from DOM
- header.paintRemoveColumns(index, numberOfColumns);
- body.paintRemoveColumns(index, numberOfColumns);
- footer.paintRemoveColumns(index, numberOfColumns);
-
- // Remove from bookkeeping
- flyweightRow.removeCells(index, numberOfColumns);
- columns.subList(index, index + numberOfColumns).clear();
-
- // Adjust frozen columns
- if (index < getFrozenColumnCount()) {
- if (index + numberOfColumns < frozenColumns) {
- /*
- * Last removed column was frozen, meaning that all removed
- * columns were frozen. Just decrement the number of frozen
- * columns accordingly.
- */
- frozenColumns -= numberOfColumns;
- } else {
- /*
- * If last removed column was not frozen, we have removed
- * columns beyond the frozen range, so all remaining frozen
- * columns are to the left of the removed columns.
- */
- frozenColumns = index;
- }
- }
-
- scroller.recalculateScrollbarsForVirtualViewport();
- body.verifyEscalatorCount();
-
- if (getColumnConfiguration().getColumnCount() > 0) {
- reapplyRowWidths(header);
- reapplyRowWidths(body);
- reapplyRowWidths(footer);
- }
-
- /*
- * Colspans make any kind of automatic clever content re-rendering
- * impossible: As soon as anything has colspans, removing one might
- * reveal further colspans, modifying the DOM structure once again,
- * ending in a cascade of updates. Because we don't know how the
- * data is updated.
- *
- * So, instead, we don't do anything. The client code is responsible
- * for re-rendering the content (if so desired). Everything Just
- * Works (TM) if colspans aren't used.
- */
- }
-
- private void reapplyRowWidths(AbstractRowContainer container) {
- if (container.getRowCount() > 0) {
- container.reapplyRowWidths();
- }
- }
-
- private void removeColumnsAdjustScrollbar(int index,
- int numberOfColumns) {
- if (horizontalScrollbar.getOffsetSize() >= horizontalScrollbar
- .getScrollSize()) {
- return;
- }
-
- double leftPosOfFirstColumnToRemove = getCalculatedColumnsWidth(
- Range.between(0, index));
- double widthOfColumnsToRemove = getCalculatedColumnsWidth(
- Range.withLength(index, numberOfColumns));
-
- double scrollLeft = horizontalScrollbar.getScrollPos();
-
- if (scrollLeft <= leftPosOfFirstColumnToRemove) {
- /*
- * viewport is scrolled to the left of the first removed column,
- * so there's no need to adjust anything
- */
- return;
- }
-
- double adjustedScrollLeft = Math.max(leftPosOfFirstColumnToRemove,
- scrollLeft - widthOfColumnsToRemove);
- horizontalScrollbar.setScrollPos(adjustedScrollLeft);
- }
-
- /**
- * Calculate the width of a row, as the sum of columns' widths.
- *
- * @return the width of a row, in pixels
- */
- public double calculateRowWidth() {
- return getCalculatedColumnsWidth(
- Range.between(0, getColumnCount()));
- }
-
- private void assertArgumentsAreValidAndWithinRange(final int index,
- final int numberOfColumns) {
- if (numberOfColumns < 1) {
- throw new IllegalArgumentException(
- "Number of columns can't be less than 1 (was "
- + numberOfColumns + ")");
- }
-
- if (index < 0 || index + numberOfColumns > getColumnCount()) {
- throw new IndexOutOfBoundsException("The given "
- + "column range (" + index + ".."
- + (index + numberOfColumns)
- + ") was outside of the current "
- + "number of columns (" + getColumnCount() + ")");
- }
- }
-
- /**
- * {@inheritDoc}
- * <p>
- * <em>Implementation detail:</em> This method does no DOM modifications
- * (i.e. is very cheap to call) if there is no data for rows when this
- * method is called.
- *
- * @see #hasColumnAndRowData()
- */
- @Override
- public void insertColumns(final int index, final int numberOfColumns) {
- if (numberOfColumns == 0) {
- return;
- }
-
- // Validate
- if (index < 0 || index > getColumnCount()) {
- throw new IndexOutOfBoundsException("The given index(" + index
- + ") was outside of the current number of columns (0.."
- + getColumnCount() + ")");
- }
-
- if (numberOfColumns < 1) {
- throw new IllegalArgumentException(
- "Number of columns must be 1 or greater (was "
- + numberOfColumns);
- }
-
- // Add to bookkeeping
- flyweightRow.addCells(index, numberOfColumns);
- for (int i = 0; i < numberOfColumns; i++) {
- columns.add(index, new Column());
- }
-
- // Adjust frozen columns
- boolean frozen = index < frozenColumns;
- if (frozen) {
- frozenColumns += numberOfColumns;
- }
-
- // Add to DOM
- header.paintInsertColumns(index, numberOfColumns, frozen);
- body.paintInsertColumns(index, numberOfColumns, frozen);
- footer.paintInsertColumns(index, numberOfColumns, frozen);
-
- // this needs to be before the scrollbar adjustment.
- boolean scrollbarWasNeeded = horizontalScrollbar
- .getOffsetSize() < horizontalScrollbar.getScrollSize();
- scroller.recalculateScrollbarsForVirtualViewport();
- boolean scrollbarIsNowNeeded = horizontalScrollbar
- .getOffsetSize() < horizontalScrollbar.getScrollSize();
- if (!scrollbarWasNeeded && scrollbarIsNowNeeded) {
- // This might as a side effect move rows around (when scrolled
- // all the way down) and require the DOM to be up to date, i.e.
- // the column to be added
- body.verifyEscalatorCount();
- }
-
- // fix initial width
- if (header.getRowCount() > 0 || body.getRowCount() > 0
- || footer.getRowCount() > 0) {
-
- Map<Integer, Double> colWidths = new HashMap<>();
- Double width = Double.valueOf(Column.DEFAULT_COLUMN_WIDTH_PX);
- for (int i = index; i < index + numberOfColumns; i++) {
- Integer col = Integer.valueOf(i);
- colWidths.put(col, width);
- }
- getColumnConfiguration().setColumnWidths(colWidths);
- }
-
- // Adjust scrollbar
- double pixelsToInsertedColumn = columnConfiguration
- .getCalculatedColumnsWidth(Range.withLength(0, index));
- final boolean columnsWereAddedToTheLeftOfViewport = scroller.lastScrollLeft > pixelsToInsertedColumn;
-
- if (columnsWereAddedToTheLeftOfViewport) {
- double insertedColumnsWidth = columnConfiguration
- .getCalculatedColumnsWidth(
- Range.withLength(index, numberOfColumns));
- horizontalScrollbar.setScrollPos(
- scroller.lastScrollLeft + insertedColumnsWidth);
- }
-
- /*
- * Colspans make any kind of automatic clever content re-rendering
- * impossible: As soon as anything has colspans, adding one might
- * affect surrounding colspans, modifying the DOM structure once
- * again, ending in a cascade of updates. Because we don't know how
- * the data is updated.
- *
- * So, instead, we don't do anything. The client code is responsible
- * for re-rendering the content (if so desired). Everything Just
- * Works (TM) if colspans aren't used.
- */
- }
-
- @Override
- public int getColumnCount() {
- return columns.size();
- }
-
- @Override
- public void setFrozenColumnCount(int count)
- throws IllegalArgumentException {
- if (count < 0 || count > getColumnCount()) {
- throw new IllegalArgumentException(
- "count must be between 0 and the current number of columns ("
- + getColumnCount() + ")");
- }
- int oldCount = frozenColumns;
- if (count == oldCount) {
- return;
- }
-
- frozenColumns = count;
-
- if (hasSomethingInDom()) {
- // Are we freezing or unfreezing?
- boolean frozen = count > oldCount;
-
- int firstAffectedCol;
- int firstUnaffectedCol;
-
- if (frozen) {
- firstAffectedCol = oldCount;
- firstUnaffectedCol = count;
- } else {
- firstAffectedCol = count;
- firstUnaffectedCol = oldCount;
- }
-
- if (oldCount > 0) {
- header.setColumnLastFrozen(oldCount - 1, false);
- body.setColumnLastFrozen(oldCount - 1, false);
- footer.setColumnLastFrozen(oldCount - 1, false);
- }
- if (count > 0) {
- header.setColumnLastFrozen(count - 1, true);
- body.setColumnLastFrozen(count - 1, true);
- footer.setColumnLastFrozen(count - 1, true);
- }
-
- for (int col = firstAffectedCol; col < firstUnaffectedCol; col++) {
- header.setColumnFrozen(col, frozen);
- body.setColumnFrozen(col, frozen);
- footer.setColumnFrozen(col, frozen);
- }
- }
-
- scroller.recalculateScrollbarsForVirtualViewport();
- }
-
- @Override
- public int getFrozenColumnCount() {
- return frozenColumns;
- }
-
- @Override
- public void setColumnWidth(int index, double px)
- throws IllegalArgumentException {
- setColumnWidths(Collections.singletonMap(Integer.valueOf(index),
- Double.valueOf(px)));
- }
-
- @Override
- public void setColumnWidths(Map<Integer, Double> indexWidthMap)
- throws IllegalArgumentException {
-
- if (indexWidthMap == null) {
- throw new IllegalArgumentException("indexWidthMap was null");
- }
-
- if (indexWidthMap.isEmpty()) {
- return;
- }
-
- Profiler.enter("Escalator.ColumnConfigurationImpl.setColumnWidths");
- try {
-
- for (Entry<Integer, Double> entry : indexWidthMap.entrySet()) {
- int index = entry.getKey().intValue();
- double width = entry.getValue().doubleValue();
-
- checkValidColumnIndex(index);
-
- // Not all browsers will accept any fractional size..
- width = WidgetUtil.roundSizeDown(width);
- columns.get(index).setWidth(width);
-
- }
-
- widthsArray = null;
- header.reapplyColumnWidths();
- body.reapplyColumnWidths();
- footer.reapplyColumnWidths();
-
- recalculateElementSizes();
-
- } finally {
- Profiler.leave(
- "Escalator.ColumnConfigurationImpl.setColumnWidths");
- }
- }
-
- private void checkValidColumnIndex(int index)
- throws IllegalArgumentException {
- if (!Range.withLength(0, getColumnCount()).contains(index)) {
- throw new IllegalArgumentException("The given column index ("
- + index + ") does not exist");
- }
- }
-
- @Override
- public double getColumnWidth(int index)
- throws IllegalArgumentException {
- checkValidColumnIndex(index);
- return columns.get(index).getDefinedWidth();
- }
-
- @Override
- public double getColumnWidthActual(int index) {
- return columns.get(index).getCalculatedWidth();
- }
-
- private double getMaxCellWidth(int colIndex)
- throws IllegalArgumentException {
- double headerWidth = header.measureMinCellWidth(colIndex, true);
- double bodyWidth = body.measureMinCellWidth(colIndex, true);
- double footerWidth = footer.measureMinCellWidth(colIndex, true);
-
- double maxWidth = Math.max(headerWidth,
- Math.max(bodyWidth, footerWidth));
- assert maxWidth >= 0 : "Got a negative max width for a column, which should be impossible.";
- return maxWidth;
- }
-
- private double getMinCellWidth(int colIndex)
- throws IllegalArgumentException {
- double headerWidth = header.measureMinCellWidth(colIndex, false);
- double bodyWidth = body.measureMinCellWidth(colIndex, false);
- double footerWidth = footer.measureMinCellWidth(colIndex, false);
-
- double minWidth = Math.max(headerWidth,
- Math.max(bodyWidth, footerWidth));
- assert minWidth >= 0 : "Got a negative max width for a column, which should be impossible.";
- return minWidth;
- }
-
- /**
- * Calculates the width of the columns in a given range.
- *
- * @param columns
- * the columns to calculate
- * @return the total width of the columns in the given
- * <code>columns</code>
- */
- double getCalculatedColumnsWidth(final Range columns) {
- /*
- * This is an assert instead of an exception, since this is an
- * internal method.
- */
- assert columns
- .isSubsetOf(Range.between(0, getColumnCount())) : "Range "
- + "was outside of current column range (i.e.: "
- + Range.between(0, getColumnCount())
- + ", but was given :" + columns;
-
- double sum = 0;
- for (int i = columns.getStart(); i < columns.getEnd(); i++) {
- double columnWidthActual = getColumnWidthActual(i);
- sum += columnWidthActual;
- }
- return sum;
- }
-
- double[] getCalculatedColumnWidths() {
- if (widthsArray == null || widthsArray.length != getColumnCount()) {
- widthsArray = new double[getColumnCount()];
- for (int i = 0; i < columns.size(); i++) {
- widthsArray[i] = columns.get(i).getCalculatedWidth();
- }
- }
- return widthsArray;
- }
-
- @Override
- public void refreshColumns(int index, int numberOfColumns)
- throws IndexOutOfBoundsException, IllegalArgumentException {
- if (numberOfColumns < 1) {
- throw new IllegalArgumentException(
- "Number of columns must be 1 or greater (was "
- + numberOfColumns + ")");
- }
-
- if (index < 0 || index + numberOfColumns > getColumnCount()) {
- throw new IndexOutOfBoundsException(
- "The given " + "column range (" + index + ".."
- + (index + numberOfColumns)
- + ") was outside of the current number of columns ("
- + getColumnCount() + ")");
- }
-
- header.refreshColumns(index, numberOfColumns);
- body.refreshColumns(index, numberOfColumns);
- footer.refreshColumns(index, numberOfColumns);
- }
- }
-
- /**
- * A decision on how to measure a spacer when it is partially within a
- * designated range.
- * <p>
- * The meaning of each value may differ depending on the context it is being
- * used in. Check that particular method's JavaDoc.
- */
- private enum SpacerInclusionStrategy {
- /** A representation of "the entire spacer". */
- COMPLETE,
-
- /** A representation of "a partial spacer". */
- PARTIAL,
-
- /** A representation of "no spacer at all". */
- NONE
- }
-
- private class SpacerContainer {
-
- /** This is used mainly for testing purposes */
- private static final String SPACER_LOGICAL_ROW_PROPERTY = "vLogicalRow";
-
- private final class SpacerImpl implements Spacer {
- private TableCellElement spacerElement;
- private TableRowElement root;
- private DivElement deco;
- private int rowIndex;
- private double height = -1;
- private boolean domHasBeenSetup = false;
- private double decoHeight;
- private double defaultCellBorderBottomSize = -1;
-
- public SpacerImpl(int rowIndex) {
- this.rowIndex = rowIndex;
-
- root = TableRowElement.as(DOM.createTR());
- spacerElement = TableCellElement.as(DOM.createTD());
- root.appendChild(spacerElement);
- root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex);
- deco = DivElement.as(DOM.createDiv());
- }
-
- public void setPositionDiff(double x, double y) {
- setPosition(getLeft() + x, getTop() + y);
- }
-
- public void setupDom(double height) {
- assert !domHasBeenSetup : "DOM can't be set up twice.";
- assert RootPanel.get().getElement().isOrHasChild(
- root) : "Root element should've been attached to the DOM by now.";
- domHasBeenSetup = true;
-
- getRootElement().getStyle().setWidth(getInnerWidth(), Unit.PX);
- setHeight(height);
-
- spacerElement
- .setColSpan(getColumnConfiguration().getColumnCount());
-
- setStylePrimaryName(getStylePrimaryName());
- }
-
- public TableRowElement getRootElement() {
- return root;
- }
-
- @Override
- public Element getDecoElement() {
- return deco;
- }
-
- public void setPosition(double x, double y) {
- positions.set(getRootElement(), x, y);
- positions.set(getDecoElement(), 0,
- y - getSpacerDecoTopOffset());
- }
-
- private double getSpacerDecoTopOffset() {
- return getBody().getDefaultRowHeight();
- }
-
- public void setStylePrimaryName(String style) {
- UIObject.setStylePrimaryName(root, style + "-spacer");
- UIObject.setStylePrimaryName(deco, style + "-spacer-deco");
- }
-
- public void setHeight(double height) {
-
- assert height >= 0 : "Height must be more >= 0 (was " + height
- + ")";
-
- final double heightDiff = height - Math.max(0, this.height);
- final double oldHeight = this.height;
-
- this.height = height;
-
- // since the spacer might be rendered on top of the previous
- // rows border (done with css), need to increase height the
- // amount of the border thickness
- if (defaultCellBorderBottomSize < 0) {
- defaultCellBorderBottomSize = WidgetUtil
- .getBorderBottomThickness(body
- .getRowElement(
- getVisibleRowRange().getStart())
- .getFirstChildElement());
- }
- root.getStyle().setHeight(height + defaultCellBorderBottomSize,
- Unit.PX);
-
- // move the visible spacers getRow row onwards.
- shiftSpacerPositionsAfterRow(getRow(), heightDiff);
-
- /*
- * If we're growing, we'll adjust the scroll size first, then
- * adjust scrolling. If we're shrinking, we do it after the
- * second if-clause.
- */
- boolean spacerIsGrowing = heightDiff > 0;
- if (spacerIsGrowing) {
- verticalScrollbar.setScrollSize(
- verticalScrollbar.getScrollSize() + heightDiff);
- }
-
- /*
- * Don't modify the scrollbars if we're expanding the -1 spacer
- * while we're scrolled to the top.
- */
- boolean minusOneSpacerException = spacerIsGrowing
- && getRow() == -1 && body.getTopRowLogicalIndex() == 0;
-
- boolean viewportNeedsScrolling = getRow() < body
- .getTopRowLogicalIndex() && !minusOneSpacerException;
- if (viewportNeedsScrolling) {
-
- /*
- * We can't use adjustScrollPos here, probably because of a
- * bookkeeping-related race condition.
- *
- * This particular situation is easier, however, since we
- * know exactly how many pixels we need to move (heightDiff)
- * and all elements below the spacer always need to move
- * that pixel amount.
- */
-
- for (TableRowElement row : body.visualRowOrder) {
- body.setRowPosition(row, 0,
- body.getRowTop(row) + heightDiff);
- }
-
- double top = getTop();
- double bottom = top + oldHeight;
- double scrollTop = verticalScrollbar.getScrollPos();
-
- boolean viewportTopIsAtMidSpacer = top < scrollTop
- && scrollTop < bottom;
-
- final double moveDiff;
- if (viewportTopIsAtMidSpacer && !spacerIsGrowing) {
-
- /*
- * If the scroll top is in the middle of the modified
- * spacer, we want to scroll the viewport up as usual,
- * but we don't want to scroll past the top of it.
- *
- * Math.max ensures this (remember: the result is going
- * to be negative).
- */
-
- moveDiff = Math.max(heightDiff, top - scrollTop);
- } else {
- moveDiff = heightDiff;
- }
- body.setBodyScrollPosition(tBodyScrollLeft,
- tBodyScrollTop + moveDiff);
- verticalScrollbar.setScrollPosByDelta(moveDiff);
-
- } else {
- body.shiftRowPositions(getRow(), heightDiff);
- }
-
- if (!spacerIsGrowing) {
- verticalScrollbar.setScrollSize(
- verticalScrollbar.getScrollSize() + heightDiff);
- }
-
- updateDecoratorGeometry(height);
- }
-
- /** Resizes and places the decorator. */
- private void updateDecoratorGeometry(double detailsHeight) {
- Style style = deco.getStyle();
- decoHeight = detailsHeight + getBody().getDefaultRowHeight();
- style.setHeight(decoHeight, Unit.PX);
- }
-
- @Override
- public Element getElement() {
- return spacerElement;
- }
-
- @Override
- public int getRow() {
- return rowIndex;
- }
-
- public double getHeight() {
- assert height >= 0 : "Height was not previously set by setHeight.";
- return height;
- }
-
- public double getTop() {
- return positions.getTop(getRootElement());
- }
-
- public double getLeft() {
- return positions.getLeft(getRootElement());
- }
-
- /**
- * Sets a new row index for this spacer. Also updates the bookeeping
- * at {@link SpacerContainer#rowIndexToSpacer}.
- */
- @SuppressWarnings("boxing")
- public void setRowIndex(int rowIndex) {
- SpacerImpl spacer = rowIndexToSpacer.remove(this.rowIndex);
- assert this == spacer : "trying to move an unexpected spacer.";
- this.rowIndex = rowIndex;
- root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex);
- rowIndexToSpacer.put(this.rowIndex, this);
- }
-
- /**
- * Updates the spacer's visibility parameters, based on whether it
- * is being currently visible or not.
- */
- public void updateVisibility() {
- if (isInViewport()) {
- show();
- } else {
- hide();
- }
- }
-
- private boolean isInViewport() {
- int top = (int) Math.ceil(getTop());
- int height = (int) Math.floor(getHeight());
- Range location = Range.withLength(top, height);
- return getViewportPixels().intersects(location);
- }
-
- public void show() {
- getRootElement().getStyle().clearDisplay();
- getDecoElement().getStyle().clearDisplay();
- }
-
- public void hide() {
- getRootElement().getStyle().setDisplay(Display.NONE);
- getDecoElement().getStyle().setDisplay(Display.NONE);
- }
-
- /**
- * Crop the decorator element so that it doesn't overlap the header
- * and footer sections.
- *
- * @param bodyTop
- * the top cordinate of the escalator body
- * @param bodyBottom
- * the bottom cordinate of the escalator body
- * @param decoWidth
- * width of the deco
- */
- private void updateDecoClip(final double bodyTop,
- final double bodyBottom, final double decoWidth) {
- final int top = deco.getAbsoluteTop();
- final int bottom = deco.getAbsoluteBottom();
- /*
- * FIXME
- *
- * Height and its use is a workaround for the issue where
- * coordinates of the deco are not calculated yet. This will
- * prevent a deco from being displayed when it's added to DOM
- */
- final int height = bottom - top;
- if (top < bodyTop || bottom > bodyBottom) {
- final double topClip = Math.max(0.0D, bodyTop - top);
- final double bottomClip = height
- - Math.max(0.0D, bottom - bodyBottom);
- // TODO [optimize] not sure how GWT compiles this
- final String clip = new StringBuilder("rect(")
- .append(topClip).append("px,").append(decoWidth)
- .append("px,").append(bottomClip).append("px,0)")
- .toString();
- deco.getStyle().setProperty("clip", clip);
- } else {
- deco.getStyle().setProperty("clip", "auto");
- }
- }
- }
-
- private final TreeMap<Integer, SpacerImpl> rowIndexToSpacer = new TreeMap<>();
-
- private SpacerUpdater spacerUpdater = SpacerUpdater.NULL;
-
- private final ScrollHandler spacerScroller = new ScrollHandler() {
- private double prevScrollX = 0;
-
- @Override
- public void onScroll(ScrollEvent event) {
- if (WidgetUtil.pixelValuesEqual(getScrollLeft(), prevScrollX)) {
- return;
- }
-
- prevScrollX = getScrollLeft();
- for (SpacerImpl spacer : rowIndexToSpacer.values()) {
- spacer.setPosition(prevScrollX, spacer.getTop());
- }
- }
- };
- private HandlerRegistration spacerScrollerRegistration;
-
- /** Width of the spacers' decos. Calculated once then cached. */
- private double spacerDecoWidth = 0.0D;
-
- public void setSpacer(int rowIndex, double height)
- throws IllegalArgumentException {
-
- if (rowIndex < -1 || rowIndex >= getBody().getRowCount()) {
- throw new IllegalArgumentException("invalid row index: "
- + rowIndex + ", while the body only has "
- + getBody().getRowCount() + " rows.");
- }
-
- if (height >= 0) {
- if (!spacerExists(rowIndex)) {
- insertNewSpacer(rowIndex, height);
- } else {
- updateExistingSpacer(rowIndex, height);
- }
- } else if (spacerExists(rowIndex)) {
- removeSpacer(rowIndex);
- }
-
- updateSpacerDecosVisibility();
- }
-
- /** Checks if a given element is a spacer element */
- public boolean isSpacer(Element row) {
-
- /*
- * If this needs optimization, we could do a more heuristic check
- * based on stylenames and stuff, instead of iterating through the
- * map.
- */
-
- for (SpacerImpl spacer : rowIndexToSpacer.values()) {
- if (spacer.getRootElement().equals(row)) {
- return true;
- }
- }
-
- return false;
- }
-
- @SuppressWarnings("boxing")
- void scrollToSpacer(int spacerIndex, ScrollDestination destination,
- int padding) {
-
- assert !destination.equals(ScrollDestination.MIDDLE)
- || padding != 0 : "destination/padding check should be done before this method";
-
- if (!rowIndexToSpacer.containsKey(spacerIndex)) {
- throw new IllegalArgumentException(
- "No spacer open at index " + spacerIndex);
- }
-
- SpacerImpl spacer = rowIndexToSpacer.get(spacerIndex);
- double targetStartPx = spacer.getTop();
- double targetEndPx = targetStartPx + spacer.getHeight();
-
- Range viewportPixels = getViewportPixels();
- double viewportStartPx = viewportPixels.getStart();
- double viewportEndPx = viewportPixels.getEnd();
-
- double scrollTop = getScrollPos(destination, targetStartPx,
- targetEndPx, viewportStartPx, viewportEndPx, padding);
-
- setScrollTop(scrollTop);
- }
-
- public void reapplySpacerWidths() {
- // FIXME #16266 , spacers get couple pixels too much because borders
- final double width = getInnerWidth() - spacerDecoWidth;
- for (SpacerImpl spacer : rowIndexToSpacer.values()) {
- spacer.getRootElement().getStyle().setWidth(width, Unit.PX);
- }
- }
-
- public void paintRemoveSpacers(Range removedRowsRange) {
- removeSpacers(removedRowsRange);
- shiftSpacersByRows(removedRowsRange.getStart(),
- -removedRowsRange.length());
- }
-
- @SuppressWarnings("boxing")
- public void removeSpacers(Range removedRange) {
-
- Map<Integer, SpacerImpl> removedSpacers = rowIndexToSpacer.subMap(
- removedRange.getStart(), true, removedRange.getEnd(),
- false);
-
- if (removedSpacers.isEmpty()) {
- return;
- }
-
- for (SpacerImpl spacer : removedSpacers.values()) {
- /*
- * [[optimization]] TODO: Each invocation of the setHeight
- * method has a cascading effect in the DOM. if this proves to
- * be slow, the DOM offset could be updated as a batch.
- */
-
- destroySpacerContent(spacer);
- spacer.setHeight(0); // resets row offsets
- spacer.getRootElement().removeFromParent();
- spacer.getDecoElement().removeFromParent();
- }
-
- removedSpacers.clear();
-
- if (rowIndexToSpacer.isEmpty()) {
- assert spacerScrollerRegistration != null : "Spacer scroller registration was null";
- spacerScrollerRegistration.removeHandler();
- spacerScrollerRegistration = null;
- }
- }
-
- public Map<Integer, SpacerImpl> getSpacers() {
- return new HashMap<>(rowIndexToSpacer);
- }
-
- /**
- * Calculates the sum of all spacers.
- *
- * @return sum of all spacers, or 0 if no spacers present
- */
- public double getSpacerHeightsSum() {
- return getHeights(rowIndexToSpacer.values());
- }
-
- /**
- * Calculates the sum of all spacers from one row index onwards.
- *
- * @param logicalRowIndex
- * the spacer to include as the first calculated spacer
- * @return the sum of all spacers from {@code logicalRowIndex} and
- * onwards, or 0 if no suitable spacers were found
- */
- @SuppressWarnings("boxing")
- public Collection<SpacerImpl> getSpacersForRowAndAfter(
- int logicalRowIndex) {
- return new ArrayList<>(
- rowIndexToSpacer.tailMap(logicalRowIndex, true).values());
- }
-
- /**
- * Get all spacers from one pixel point onwards.
- * <p>
- *
- * In this method, the {@link SpacerInclusionStrategy} has the following
- * meaning when a spacer lies in the middle of either pixel argument:
- * <dl>
- * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE}
- * <dd>include the spacer
- * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL}
- * <dd>include the spacer
- * <dt>{@link SpacerInclusionStrategy#NONE NONE}
- * <dd>ignore the spacer
- * </dl>
- *
- * @param px
- * the pixel point after which to return all spacers
- * @param strategy
- * the inclusion strategy regarding the {@code px}
- * @return a collection of the spacers that exist after {@code px}
- */
- public Collection<SpacerImpl> getSpacersAfterPx(final double px,
- final SpacerInclusionStrategy strategy) {
-
- ArrayList<SpacerImpl> spacers = new ArrayList<>(
- rowIndexToSpacer.values());
-
- for (int i = 0; i < spacers.size(); i++) {
- SpacerImpl spacer = spacers.get(i);
-
- double top = spacer.getTop();
- double bottom = top + spacer.getHeight();
-
- if (top > px) {
- return spacers.subList(i, spacers.size());
- } else if (bottom > px) {
- if (strategy == SpacerInclusionStrategy.NONE) {
- return spacers.subList(i + 1, spacers.size());
- } else {
- return spacers.subList(i, spacers.size());
- }
- }
- }
-
- return Collections.emptySet();
- }
-
- /**
- * Gets the spacers currently rendered in the DOM.
- *
- * @return an unmodifiable (but live) collection of the spacers
- * currently in the DOM
- */
- public Collection<SpacerImpl> getSpacersInDom() {
- return Collections
- .unmodifiableCollection(rowIndexToSpacer.values());
- }
-
- /**
- * Gets the amount of pixels occupied by spacers between two pixel
- * points.
- * <p>
- * In this method, the {@link SpacerInclusionStrategy} has the following
- * meaning when a spacer lies in the middle of either pixel argument:
- * <dl>
- * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE}
- * <dd>take the entire spacer into account
- * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL}
- * <dd>take only the visible area into account
- * <dt>{@link SpacerInclusionStrategy#NONE NONE}
- * <dd>ignore that spacer
- * </dl>
- *
- * @param rangeTop
- * the top pixel point
- * @param topInclusion
- * the inclusion strategy regarding {@code rangeTop}.
- * @param rangeBottom
- * the bottom pixel point
- * @param bottomInclusion
- * the inclusion strategy regarding {@code rangeBottom}.
- * @return the pixels occupied by spacers between {@code rangeTop} and
- * {@code rangeBottom}
- */
- public double getSpacerHeightsSumBetweenPx(double rangeTop,
- SpacerInclusionStrategy topInclusion, double rangeBottom,
- SpacerInclusionStrategy bottomInclusion) {
-
- assert rangeTop <= rangeBottom : "rangeTop must be less than rangeBottom";
-
- double heights = 0;
-
- /*
- * TODO [[optimize]]: this might be somewhat inefficient (due to
- * iterator-based scanning, instead of using the treemap's search
- * functionalities). But it should be easy to write, read, verify
- * and maintain.
- */
- for (SpacerImpl spacer : rowIndexToSpacer.values()) {
- double top = spacer.getTop();
- double height = spacer.getHeight();
- double bottom = top + height;
-
- /*
- * If we happen to implement a DoubleRange (in addition to the
- * int-based Range) at some point, the following logic should
- * probably be converted into using the
- * Range.partitionWith-equivalent.
- */
-
- boolean topIsAboveRange = top < rangeTop;
- boolean topIsInRange = rangeTop <= top && top <= rangeBottom;
- boolean topIsBelowRange = rangeBottom < top;
-
- boolean bottomIsAboveRange = bottom < rangeTop;
- boolean bottomIsInRange = rangeTop <= bottom
- && bottom <= rangeBottom;
- boolean bottomIsBelowRange = rangeBottom < bottom;
-
- assert topIsAboveRange ^ topIsBelowRange
- ^ topIsInRange : "Bad top logic";
- assert bottomIsAboveRange ^ bottomIsBelowRange
- ^ bottomIsInRange : "Bad bottom logic";
-
- if (bottomIsAboveRange) {
- continue;
- } else if (topIsBelowRange) {
- return heights;
- }
-
- else if (topIsAboveRange && bottomIsInRange) {
- switch (topInclusion) {
- case PARTIAL:
- heights += bottom - rangeTop;
- break;
- case COMPLETE:
- heights += height;
- break;
- default:
- break;
- }
- }
-
- else if (topIsAboveRange && bottomIsBelowRange) {
-
- /*
- * Here we arbitrarily decide that the top inclusion will
- * have the honor of overriding the bottom inclusion if
- * happens to be a conflict of interests.
- */
- switch (topInclusion) {
- case NONE:
- return 0;
- case COMPLETE:
- return height;
- case PARTIAL:
- return rangeBottom - rangeTop;
- default:
- throw new IllegalArgumentException(
- "Unexpected inclusion state :" + topInclusion);
- }
-
- } else if (topIsInRange && bottomIsInRange) {
- heights += height;
- }
-
- else if (topIsInRange && bottomIsBelowRange) {
- switch (bottomInclusion) {
- case PARTIAL:
- heights += rangeBottom - top;
- break;
- case COMPLETE:
- heights += height;
- break;
- default:
- break;
- }
-
- return heights;
- }
-
- else {
- assert false : "Unnaccounted-for situation";
- }
- }
-
- return heights;
- }
-
- /**
- * Gets the amount of pixels occupied by spacers from the top until a
- * certain spot from the top of the body.
- *
- * @param px
- * pixels counted from the top
- * @return the pixels occupied by spacers up until {@code px}
- */
- public double getSpacerHeightsSumUntilPx(double px) {
- return getSpacerHeightsSumBetweenPx(0,
- SpacerInclusionStrategy.PARTIAL, px,
- SpacerInclusionStrategy.PARTIAL);
- }
-
- /**
- * Gets the amount of pixels occupied by spacers until a logical row
- * index.
- *
- * @param logicalIndex
- * a logical row index
- * @return the pixels occupied by spacers up until {@code logicalIndex}
- */
- @SuppressWarnings("boxing")
- public double getSpacerHeightsSumUntilIndex(int logicalIndex) {
- return getHeights(
- rowIndexToSpacer.headMap(logicalIndex, false).values());
- }
-
- private double getHeights(Collection<SpacerImpl> spacers) {
- double heights = 0;
- for (SpacerImpl spacer : spacers) {
- heights += spacer.getHeight();
- }
- return heights;
- }
-
- /**
- * Gets the height of the spacer for a row index.
- *
- * @param rowIndex
- * the index of the row where the spacer should be
- * @return the height of the spacer at index {@code rowIndex}, or 0 if
- * there is no spacer there
- */
- public double getSpacerHeight(int rowIndex) {
- SpacerImpl spacer = getSpacer(rowIndex);
- if (spacer != null) {
- return spacer.getHeight();
- } else {
- return 0;
- }
- }
-
- private boolean spacerExists(int rowIndex) {
- return rowIndexToSpacer.containsKey(Integer.valueOf(rowIndex));
- }
-
- @SuppressWarnings("boxing")
- private void insertNewSpacer(int rowIndex, double height) {
-
- if (spacerScrollerRegistration == null) {
- spacerScrollerRegistration = addScrollHandler(spacerScroller);
- }
-
- final SpacerImpl spacer = new SpacerImpl(rowIndex);
-
- rowIndexToSpacer.put(rowIndex, spacer);
- // set the position before adding it to DOM
- positions.set(spacer.getRootElement(), getScrollLeft(),
- calculateSpacerTop(rowIndex));
-
- TableRowElement spacerRoot = spacer.getRootElement();
- spacerRoot.getStyle()
- .setWidth(columnConfiguration.calculateRowWidth(), Unit.PX);
- body.getElement().appendChild(spacerRoot);
- spacer.setupDom(height);
- // set the deco position, requires that spacer is in the DOM
- positions.set(spacer.getDecoElement(), 0,
- spacer.getTop() - spacer.getSpacerDecoTopOffset());
-
- spacerDecoContainer.appendChild(spacer.getDecoElement());
- if (spacerDecoContainer.getParentElement() == null) {
- getElement().appendChild(spacerDecoContainer);
- // calculate the spacer deco width, it won't change
- spacerDecoWidth = getBoundingWidth(spacer.getDecoElement());
- }
-
- initSpacerContent(spacer);
-
- body.sortDomElements();
- }
-
- private void updateExistingSpacer(int rowIndex, double newHeight) {
- getSpacer(rowIndex).setHeight(newHeight);
- }
-
- public SpacerImpl getSpacer(int rowIndex) {
- return rowIndexToSpacer.get(Integer.valueOf(rowIndex));
- }
-
- private void removeSpacer(int rowIndex) {
- removeSpacers(Range.withOnly(rowIndex));
- }
-
- public void setStylePrimaryName(String style) {
- for (SpacerImpl spacer : rowIndexToSpacer.values()) {
- spacer.setStylePrimaryName(style);
- }
- }
-
- public void setSpacerUpdater(SpacerUpdater spacerUpdater)
- throws IllegalArgumentException {
- if (spacerUpdater == null) {
- throw new IllegalArgumentException(
- "spacer updater cannot be null");
- }
-
- destroySpacerContent(rowIndexToSpacer.values());
- this.spacerUpdater = spacerUpdater;
- initSpacerContent(rowIndexToSpacer.values());
- }
-
- public SpacerUpdater getSpacerUpdater() {
- return spacerUpdater;
- }
-
- private void destroySpacerContent(Iterable<SpacerImpl> spacers) {
- for (SpacerImpl spacer : spacers) {
- destroySpacerContent(spacer);
- }
- }
-
- private void destroySpacerContent(SpacerImpl spacer) {
- assert getElement().isOrHasChild(spacer
- .getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching";
- assert getElement().isOrHasChild(spacer
- .getElement()) : "Spacer element somehow got detached from Escalator before detaching";
- spacerUpdater.destroy(spacer);
- assert getElement().isOrHasChild(spacer
- .getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching";
- assert getElement().isOrHasChild(spacer
- .getElement()) : "Spacer element somehow got detached from Escalator before detaching";
- }
-
- private void initSpacerContent(Iterable<SpacerImpl> spacers) {
- for (SpacerImpl spacer : spacers) {
- initSpacerContent(spacer);
- }
- }
-
- private void initSpacerContent(SpacerImpl spacer) {
- assert getElement().isOrHasChild(spacer
- .getRootElement()) : "Spacer's root element somehow got detached from Escalator before attaching";
- assert getElement().isOrHasChild(spacer
- .getElement()) : "Spacer element somehow got detached from Escalator before attaching";
- spacerUpdater.init(spacer);
- assert getElement().isOrHasChild(spacer
- .getRootElement()) : "Spacer's root element somehow got detached from Escalator during attaching";
- assert getElement().isOrHasChild(spacer
- .getElement()) : "Spacer element somehow got detached from Escalator during attaching";
-
- spacer.updateVisibility();
- }
-
- public String getSubPartName(Element subElement) {
- for (SpacerImpl spacer : rowIndexToSpacer.values()) {
- if (spacer.getRootElement().isOrHasChild(subElement)) {
- return "spacer[" + spacer.getRow() + "]";
- }
- }
- return null;
- }
-
- public Element getSubPartElement(int index) {
- SpacerImpl spacer = rowIndexToSpacer.get(Integer.valueOf(index));
- if (spacer != null) {
- return spacer.getElement();
- } else {
- return null;
- }
- }
-
- private double calculateSpacerTop(int logicalIndex) {
- return body.getRowTop(logicalIndex) + body.getDefaultRowHeight();
- }
-
- @SuppressWarnings("boxing")
- private void shiftSpacerPositionsAfterRow(int changedRowIndex,
- double diffPx) {
- for (SpacerImpl spacer : rowIndexToSpacer
- .tailMap(changedRowIndex, false).values()) {
- spacer.setPositionDiff(0, diffPx);
- }
- }
-
- /**
- * Shifts spacers at and after a specific row by an amount of rows.
- * <p>
- * This moves both their associated row index and also their visual
- * placement.
- * <p>
- * <em>Note:</em> This method does not check for the validity of any
- * arguments.
- *
- * @param index
- * the index of first row to move
- * @param numberOfRows
- * the number of rows to shift the spacers with. A positive
- * value is downwards, a negative value is upwards.
- */
- public void shiftSpacersByRows(int index, int numberOfRows) {
- final double pxDiff = numberOfRows * body.getDefaultRowHeight();
- for (SpacerContainer.SpacerImpl spacer : getSpacersForRowAndAfter(
- index)) {
- spacer.setPositionDiff(0, pxDiff);
- spacer.setRowIndex(spacer.getRow() + numberOfRows);
- }
- }
-
- private void updateSpacerDecosVisibility() {
- final Range visibleRowRange = getVisibleRowRange();
- Collection<SpacerImpl> visibleSpacers = rowIndexToSpacer
- .subMap(visibleRowRange.getStart() - 1,
- visibleRowRange.getEnd() + 1)
- .values();
- if (!visibleSpacers.isEmpty()) {
- final double top = tableWrapper.getAbsoluteTop()
- + header.getHeightOfSection();
- final double bottom = tableWrapper.getAbsoluteBottom()
- - footer.getHeightOfSection();
- for (SpacerImpl spacer : visibleSpacers) {
- spacer.updateDecoClip(top, bottom, spacerDecoWidth);
- }
- }
- }
- }
-
- private class ElementPositionBookkeeper {
- /**
- * A map containing cached values of an element's current top position.
- */
- private final Map<Element, Double> elementTopPositionMap = new HashMap<>();
- private final Map<Element, Double> elementLeftPositionMap = new HashMap<>();
-
- public void set(final Element e, final double x, final double y) {
- assert e != null : "Element was null";
- position.set(e, x, y);
- elementTopPositionMap.put(e, Double.valueOf(y));
- elementLeftPositionMap.put(e, Double.valueOf(x));
- }
-
- public double getTop(final Element e) {
- Double top = elementTopPositionMap.get(e);
- if (top == null) {
- throw new IllegalArgumentException("Element " + e
- + " was not found in the position bookkeeping");
- }
- return top.doubleValue();
- }
-
- public double getLeft(final Element e) {
- Double left = elementLeftPositionMap.get(e);
- if (left == null) {
- throw new IllegalArgumentException("Element " + e
- + " was not found in the position bookkeeping");
- }
- return left.doubleValue();
- }
-
- public void remove(Element e) {
- elementTopPositionMap.remove(e);
- elementLeftPositionMap.remove(e);
- }
- }
-
- /**
- * Utility class for parsing and storing SubPart request string attributes
- * for Grid and Escalator.
- *
- * @since 7.5.0
- */
- public static class SubPartArguments {
- private String type;
- private int[] indices;
-
- private SubPartArguments(String type, int[] indices) {
- /*
- * The constructor is private so that no third party would by
- * mistake start using this parsing scheme, since it's not official
- * by TestBench (yet?).
- */
-
- this.type = type;
- this.indices = indices;
- }
-
- public String getType() {
- return type;
- }
-
- public int getIndicesLength() {
- return indices.length;
- }
-
- public int getIndex(int i) {
- return indices[i];
- }
-
- public int[] getIndices() {
- return Arrays.copyOf(indices, indices.length);
- }
-
- static SubPartArguments create(String subPart) {
- String[] splitArgs = subPart.split("\\[");
- String type = splitArgs[0];
- int[] indices = new int[splitArgs.length - 1];
- for (int i = 0; i < indices.length; ++i) {
- String tmp = splitArgs[i + 1];
- indices[i] = Integer
- .parseInt(tmp.substring(0, tmp.indexOf("]", 1)));
- }
- return new SubPartArguments(type, indices);
- }
- }
-
- // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y
- /**
- * The solution to
- * <code>|tan<sup>-1</sup>(<i>x</i>)|×(180/π) = 30</code>
- * .
- * <p>
- * This constant is placed in the Escalator class, instead of an inner
- * class, since even mathematical expressions aren't allowed in non-static
- * inner classes for constants.
- */
- private static final double RATIO_OF_30_DEGREES = 1 / Math.sqrt(3);
- /**
- * The solution to
- * <code>|tan<sup>-1</sup>(<i>x</i>)|×(180/π) = 40</code>
- * .
- * <p>
- * This constant is placed in the Escalator class, instead of an inner
- * class, since even mathematical expressions aren't allowed in non-static
- * inner classes for constants.
- */
- private static final double RATIO_OF_40_DEGREES = Math.tan(2 * Math.PI / 9);
-
- private static final String DEFAULT_WIDTH = "500.0px";
- private static final String DEFAULT_HEIGHT = "400.0px";
-
- private FlyweightRow flyweightRow = new FlyweightRow();
-
- /** The {@code <thead/>} tag. */
- private final TableSectionElement headElem = TableSectionElement
- .as(DOM.createTHead());
- /** The {@code <tbody/>} tag. */
- private final TableSectionElement bodyElem = TableSectionElement
- .as(DOM.createTBody());
- /** The {@code <tfoot/>} tag. */
- private final TableSectionElement footElem = TableSectionElement
- .as(DOM.createTFoot());
-
- /**
- * TODO: investigate whether this field is now unnecessary, as
- * {@link ScrollbarBundle} now caches its values.
- *
- * @deprecated maybe...
- */
- @Deprecated
- private double tBodyScrollTop = 0;
-
- /**
- * TODO: investigate whether this field is now unnecessary, as
- * {@link ScrollbarBundle} now caches its values.
- *
- * @deprecated maybe...
- */
- @Deprecated
- private double tBodyScrollLeft = 0;
-
- private final VerticalScrollbarBundle verticalScrollbar = new VerticalScrollbarBundle();
- private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle();
-
- private final HeaderRowContainer header = new HeaderRowContainer(headElem);
- private final BodyRowContainerImpl body = new BodyRowContainerImpl(
- bodyElem);
- private final FooterRowContainer footer = new FooterRowContainer(footElem);
-
- /**
- * Flag for keeping track of {@link RowHeightChangedEvent}s
- */
- private boolean rowHeightChangedEventFired = false;
-
- private final Scroller scroller = new Scroller();
-
- private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl();
- private final DivElement tableWrapper;
-
- private final DivElement horizontalScrollbarDeco = DivElement
- .as(DOM.createDiv());
- private final DivElement headerDeco = DivElement.as(DOM.createDiv());
- private final DivElement footerDeco = DivElement.as(DOM.createDiv());
- private final DivElement spacerDecoContainer = DivElement
- .as(DOM.createDiv());
-
- private PositionFunction position;
-
- /** The cached width of the escalator, in pixels. */
- private double widthOfEscalator = 0;
- /** The cached height of the escalator, in pixels. */
- private double heightOfEscalator = 0;
-
- /** The height of Escalator in terms of body rows. */
- private double heightByRows = 10.0d;
-
- /** The height of Escalator, as defined by {@link #setHeight(String)} */
- private String heightByCss = "";
-
- private HeightMode heightMode = HeightMode.CSS;
-
- private double delayToCancelTouchScroll = -1;
-
- private boolean layoutIsScheduled = false;
- private ScheduledCommand layoutCommand = new ScheduledCommand() {
- @Override
- public void execute() {
- recalculateElementSizes();
- layoutIsScheduled = false;
- }
- };
-
- private final ElementPositionBookkeeper positions = new ElementPositionBookkeeper();
-
- /**
- * Creates a new Escalator widget instance.
- */
- public Escalator() {
-
- detectAndApplyPositionFunction();
- getLogger().info("Using " + position.getClass().getSimpleName()
- + " for position");
-
- final Element root = DOM.createDiv();
- setElement(root);
-
- setupScrollbars(root);
-
- tableWrapper = DivElement.as(DOM.createDiv());
-
- root.appendChild(tableWrapper);
-
- final Element table = DOM.createTable();
- tableWrapper.appendChild(table);
-
- table.appendChild(headElem);
- table.appendChild(bodyElem);
- table.appendChild(footElem);
-
- Style hCornerStyle = headerDeco.getStyle();
- hCornerStyle.setWidth(verticalScrollbar.getScrollbarThickness(),
- Unit.PX);
- hCornerStyle.setDisplay(Display.NONE);
- root.appendChild(headerDeco);
-
- Style fCornerStyle = footerDeco.getStyle();
- fCornerStyle.setWidth(verticalScrollbar.getScrollbarThickness(),
- Unit.PX);
- fCornerStyle.setDisplay(Display.NONE);
- root.appendChild(footerDeco);
-
- Style hWrapperStyle = horizontalScrollbarDeco.getStyle();
- hWrapperStyle.setDisplay(Display.NONE);
- hWrapperStyle.setHeight(horizontalScrollbar.getScrollbarThickness(),
- Unit.PX);
- root.appendChild(horizontalScrollbarDeco);
-
- setStylePrimaryName("v-escalator");
-
- spacerDecoContainer.setAttribute("aria-hidden", "true");
-
- // init default dimensions
- setHeight(null);
- setWidth(null);
-
- publishJSHelpers(root);
- }
-
- private double getBoundingWidth(Element element) {
- // Gets the current width, including border and padding, for the element
- // while ignoring any transforms applied to the element (e.g. scale)
- return new ComputedStyle(element).getWidthIncludingBorderPadding();
- }
-
- private double getBoundingHeight(Element element) {
- // Gets the current height, including border and padding, for the
- // element while ignoring any transforms applied to the element (e.g.
- // scale)
- return new ComputedStyle(element).getHeightIncludingBorderPadding();
- }
-
- private int getBodyRowCount() {
- return getBody().getRowCount();
- }
-
- private native void publishJSHelpers(Element root)
- /*-{
- var self = this;
- root.getBodyRowCount = $entry(function () {
- return self.@Escalator::getBodyRowCount()();
- });
- }-*/;
-
- private void setupScrollbars(final Element root) {
-
- ScrollHandler scrollHandler = new ScrollHandler() {
- @Override
- public void onScroll(ScrollEvent event) {
- scroller.onScroll();
- fireEvent(new ScrollEvent());
- }
- };
-
- int scrollbarThickness = WidgetUtil.getNativeScrollbarSize();
- if (BrowserInfo.get().isIE()) {
- /*
- * IE refuses to scroll properly if the DIV isn't at least one pixel
- * larger than the scrollbar controls themselves.
- */
- scrollbarThickness += 1;
- }
-
- root.appendChild(verticalScrollbar.getElement());
- verticalScrollbar.addScrollHandler(scrollHandler);
- verticalScrollbar.setScrollbarThickness(scrollbarThickness);
-
- root.appendChild(horizontalScrollbar.getElement());
- horizontalScrollbar.addScrollHandler(scrollHandler);
- horizontalScrollbar.setScrollbarThickness(scrollbarThickness);
- horizontalScrollbar
- .addVisibilityHandler(new ScrollbarBundle.VisibilityHandler() {
-
- private boolean queued = false;
-
- @Override
- public void visibilityChanged(
- ScrollbarBundle.VisibilityChangeEvent event) {
- if (queued) {
- return;
- }
- queued = true;
-
- /*
- * We either lost or gained a scrollbar. In any case, we
- * need to change the height, if it's defined by rows.
- */
- Scheduler.get().scheduleFinally(new ScheduledCommand() {
-
- @Override
- public void execute() {
- applyHeightByRows();
- queued = false;
- }
- });
- }
- });
-
- /*
- * Because of all the IE hacks we've done above, we now have scrollbars
- * hiding underneath a lot of DOM elements.
- *
- * This leads to problems with OSX (and many touch-only devices) when
- * scrollbars are only shown when scrolling, as the scrollbar elements
- * are hidden underneath everything. We trust that the scrollbars behave
- * properly in these situations and simply pop them out with a bit of
- * z-indexing.
- */
- if (WidgetUtil.getNativeScrollbarSize() == 0) {
- verticalScrollbar.getElement().getStyle().setZIndex(90);
- horizontalScrollbar.getElement().getStyle().setZIndex(90);
- }
- }
-
- @Override
- protected void onLoad() {
- super.onLoad();
-
- header.autodetectRowHeightLater();
- body.autodetectRowHeightLater();
- footer.autodetectRowHeightLater();
-
- header.paintInsertRows(0, header.getRowCount());
- footer.paintInsertRows(0, footer.getRowCount());
-
- boolean columnsChanged = false;
- for (ColumnConfigurationImpl.Column column : columnConfiguration.columns) {
- boolean columnChanged = column.measureAndSetWidthIfNeeded();
- if (columnChanged) {
- columnsChanged = true;
- }
- }
- if (columnsChanged) {
- header.reapplyColumnWidths();
- body.reapplyColumnWidths();
- footer.reapplyColumnWidths();
- }
-
- verticalScrollbar.onLoad();
- horizontalScrollbar.onLoad();
-
- scroller.attachScrollListener(verticalScrollbar.getElement());
- scroller.attachScrollListener(horizontalScrollbar.getElement());
- scroller.attachMousewheelListener(getElement());
-
- if (isCurrentBrowserIE11OrEdge()) {
- // Touch listeners doesn't work for IE11 and Edge (#18737)
- scroller.attachPointerEventListeners(getElement());
- } else {
- scroller.attachTouchListeners(getElement());
- }
-
- /*
- * Note: There's no need to explicitly insert rows into the body.
- *
- * recalculateElementSizes will recalculate the height of the body. This
- * has the side-effect that as the body's size grows bigger (i.e. from 0
- * to its actual height), more escalator rows are populated. Those
- * escalator rows are then immediately rendered. This, in effect, is the
- * same thing as inserting those rows.
- *
- * In fact, having an extra paintInsertRows here would lead to duplicate
- * rows.
- */
- recalculateElementSizes();
- }
-
- @Override
- protected void onUnload() {
-
- scroller.detachScrollListener(verticalScrollbar.getElement());
- scroller.detachScrollListener(horizontalScrollbar.getElement());
- scroller.detachMousewheelListener(getElement());
-
- if (isCurrentBrowserIE11OrEdge()) {
- // Touch listeners doesn't work for IE11 and Edge (#18737)
- scroller.detachPointerEventListeners(getElement());
- } else {
- scroller.detachTouchListeners(getElement());
- }
-
- /*
- * We can call paintRemoveRows here, because static ranges are simple to
- * remove.
- */
- header.paintRemoveRows(0, header.getRowCount());
- footer.paintRemoveRows(0, footer.getRowCount());
-
- /*
- * We can't call body.paintRemoveRows since it relies on rowCount to be
- * updated correctly. Since it isn't, we'll simply and brutally rip out
- * the DOM elements (in an elegant way, of course).
- */
- int rowsToRemove = body.getDomRowCount();
- for (int i = 0; i < rowsToRemove; i++) {
- int index = rowsToRemove - i - 1;
- TableRowElement tr = bodyElem.getRows().getItem(index);
- body.paintRemoveRow(tr, index);
- positions.remove(tr);
- }
- body.visualRowOrder.clear();
- body.setTopRowLogicalIndex(0);
-
- super.onUnload();
- }
-
- private void detectAndApplyPositionFunction() {
- final Style docStyle = Document.get().getBody().getStyle();
- if (hasProperty(docStyle, "transform")) {
- if (hasProperty(docStyle, "transformStyle")) {
- position = new Translate3DPosition();
- } else {
- position = new TranslatePosition();
- }
- } else if (hasProperty(docStyle, "webkitTransform")) {
- position = new WebkitTranslate3DPosition();
- }
- }
-
- private Logger getLogger() {
- return Logger.getLogger(getClass().getName());
- }
-
- private static native boolean hasProperty(Style style, String name)
- /*-{
- return style[name] !== undefined;
- }-*/;
-
- /**
- * Check whether there are both columns and any row data (for either
- * headers, body or footer).
- *
- * @return <code>true</code> iff header, body or footer has rows && there
- * are columns
- */
- private boolean hasColumnAndRowData() {
- return (header.getRowCount() > 0 || body.getRowCount() > 0
- || footer.getRowCount() > 0)
- && columnConfiguration.getColumnCount() > 0;
- }
-
- /**
- * Check whether there are any cells in the DOM.
- *
- * @return <code>true</code> iff header, body or footer has any child
- * elements
- */
- private boolean hasSomethingInDom() {
- return headElem.hasChildNodes() || bodyElem.hasChildNodes()
- || footElem.hasChildNodes();
- }
-
- /**
- * Returns the row container for the header in this Escalator.
- *
- * @return the header. Never <code>null</code>
- */
- public RowContainer getHeader() {
- return header;
- }
-
- /**
- * Returns the row container for the body in this Escalator.
- *
- * @return the body. Never <code>null</code>
- */
- public BodyRowContainer getBody() {
- return body;
- }
-
- /**
- * Returns the row container for the footer in this Escalator.
- *
- * @return the footer. Never <code>null</code>
- */
- public RowContainer getFooter() {
- return footer;
- }
-
- /**
- * Returns the configuration object for the columns in this Escalator.
- *
- * @return the configuration object for the columns in this Escalator. Never
- * <code>null</code>
- */
- public ColumnConfiguration getColumnConfiguration() {
- return columnConfiguration;
- }
-
- @Override
- public void setWidth(final String width) {
- if (width != null && !width.isEmpty()) {
- super.setWidth(width);
- } else {
- super.setWidth(DEFAULT_WIDTH);
- }
-
- recalculateElementSizes();
- }
-
- /**
- * {@inheritDoc}
- * <p>
- * If Escalator is currently not in {@link HeightMode#CSS}, the given value
- * is remembered, and applied once the mode is applied.
- *
- * @see #setHeightMode(HeightMode)
- */
- @Override
- public void setHeight(String height) {
- /*
- * TODO remove method once RequiresResize and the Vaadin layoutmanager
- * listening mechanisms are implemented
- */
-
- if (height != null && !height.isEmpty()) {
- heightByCss = height;
- } else {
- if (getHeightMode() == HeightMode.UNDEFINED) {
- heightByRows = body.getRowCount();
- applyHeightByRows();
- return;
- } else {
- heightByCss = DEFAULT_HEIGHT;
- }
- }
-
- if (getHeightMode() == HeightMode.CSS) {
- setHeightInternal(height);
- }
- }
-
- private void setHeightInternal(final String height) {
- final int escalatorRowsBefore = body.visualRowOrder.size();
-
- if (height != null && !height.isEmpty()) {
- super.setHeight(height);
- } else {
- if (getHeightMode() == HeightMode.UNDEFINED) {
- int newHeightByRows = body.getRowCount();
- if (heightByRows != newHeightByRows) {
- heightByRows = newHeightByRows;
- applyHeightByRows();
- }
- return;
- } else {
- super.setHeight(DEFAULT_HEIGHT);
- }
- }
-
- recalculateElementSizes();
-
- if (escalatorRowsBefore != body.visualRowOrder.size()) {
- fireRowVisibilityChangeEvent();
- }
- }
-
- /**
- * Returns the vertical scroll offset. Note that this is not necessarily the
- * same as the {@code scrollTop} attribute in the DOM.
- *
- * @return the logical vertical scroll offset
- */
- public double getScrollTop() {
- return verticalScrollbar.getScrollPos();
- }
-
- /**
- * Sets the vertical scroll offset. Note that this will not necessarily
- * become the same as the {@code scrollTop} attribute in the DOM.
- *
- * @param scrollTop
- * the number of pixels to scroll vertically
- */
- public void setScrollTop(final double scrollTop) {
- verticalScrollbar.setScrollPos(scrollTop);
- }
-
- /**
- * Returns the logical horizontal scroll offset. Note that this is not
- * necessarily the same as the {@code scrollLeft} attribute in the DOM.
- *
- * @return the logical horizontal scroll offset
- */
- public double getScrollLeft() {
- return horizontalScrollbar.getScrollPos();
- }
-
- /**
- * Sets the logical horizontal scroll offset. Note that will not necessarily
- * become the same as the {@code scrollLeft} attribute in the DOM.
- *
- * @param scrollLeft
- * the number of pixels to scroll horizontally
- */
- public void setScrollLeft(final double scrollLeft) {
- horizontalScrollbar.setScrollPos(scrollLeft);
- }
-
- /**
- * Returns the scroll width for the escalator. Note that this is not
- * necessary the same as {@code Element.scrollWidth} in the DOM.
- *
- * @since 7.5.0
- * @return the scroll width in pixels
- */
- public double getScrollWidth() {
- return horizontalScrollbar.getScrollSize();
- }
-
- /**
- * Returns the scroll height for the escalator. Note that this is not
- * necessary the same as {@code Element.scrollHeight} in the DOM.
- *
- * @since 7.5.0
- * @return the scroll height in pixels
- */
- public double getScrollHeight() {
- return verticalScrollbar.getScrollSize();
- }
-
- /**
- * Scrolls the body horizontally so that the column at the given index is
- * visible and there is at least {@code padding} pixels in the direction of
- * the given scroll destination.
- *
- * @param columnIndex
- * the index of the column to scroll to
- * @param destination
- * where the column should be aligned visually after scrolling
- * @param padding
- * the number pixels to place between the scrolled-to column and
- * the viewport edge.
- * @throws IndexOutOfBoundsException
- * if {@code columnIndex} is not a valid index for an existing
- * column
- * @throws IllegalArgumentException
- * if {@code destination} is {@link ScrollDestination#MIDDLE}
- * and padding is nonzero; or if the indicated column is frozen;
- * or if {@code destination == null}
- */
- public void scrollToColumn(final int columnIndex,
- final ScrollDestination destination, final int padding)
- throws IndexOutOfBoundsException, IllegalArgumentException {
- validateScrollDestination(destination, padding);
- verifyValidColumnIndex(columnIndex);
-
- if (columnIndex < columnConfiguration.frozenColumns) {
- throw new IllegalArgumentException(
- "The given column index " + columnIndex + " is frozen.");
- }
-
- scroller.scrollToColumn(columnIndex, destination, padding);
- }
-
- private void verifyValidColumnIndex(final int columnIndex)
- throws IndexOutOfBoundsException {
- if (columnIndex < 0
- || columnIndex >= columnConfiguration.getColumnCount()) {
- throw new IndexOutOfBoundsException("The given column index "
- + columnIndex + " does not exist.");
- }
- }
-
- /**
- * Scrolls the body vertically so that the row at the given index is visible
- * and there is at least {@literal padding} pixels to the given scroll
- * destination.
- *
- * @param rowIndex
- * the index of the logical row to scroll to
- * @param destination
- * where the row should be aligned visually after scrolling
- * @param padding
- * the number pixels to place between the scrolled-to row and the
- * viewport edge.
- * @throws IndexOutOfBoundsException
- * if {@code rowIndex} is not a valid index for an existing row
- * @throws IllegalArgumentException
- * if {@code destination} is {@link ScrollDestination#MIDDLE}
- * and padding is nonzero; or if {@code destination == null}
- * @see #scrollToRowAndSpacer(int, ScrollDestination, int)
- * @see #scrollToSpacer(int, ScrollDestination, int)
- */
- public void scrollToRow(final int rowIndex,
- final ScrollDestination destination, final int padding)
- throws IndexOutOfBoundsException, IllegalArgumentException {
- Scheduler.get().scheduleFinally(new ScheduledCommand() {
- @Override
- public void execute() {
- validateScrollDestination(destination, padding);
- verifyValidRowIndex(rowIndex);
- scroller.scrollToRow(rowIndex, destination, padding);
- }
- });
- }
-
- private void verifyValidRowIndex(final int rowIndex) {
- if (rowIndex < 0 || rowIndex >= body.getRowCount()) {
- throw new IndexOutOfBoundsException(
- "The given row index " + rowIndex + " does not exist.");
- }
- }
-
- /**
- * Scrolls the body vertically so that the spacer at the given row index is
- * visible and there is at least {@literal padding} pixesl to the given
- * scroll destination.
- *
- * @since 7.5.0
- * @param spacerIndex
- * the row index of the spacer to scroll to
- * @param destination
- * where the spacer should be aligned visually after scrolling
- * @param padding
- * the number of pixels to place between the scrolled-to spacer
- * and the viewport edge
- * @throws IllegalArgumentException
- * if {@code spacerIndex} is not an opened spacer; or if
- * {@code destination} is {@link ScrollDestination#MIDDLE} and
- * padding is nonzero; or if {@code destination == null}
- * @see #scrollToRow(int, ScrollDestination, int)
- * @see #scrollToRowAndSpacer(int, ScrollDestination, int)
- */
- public void scrollToSpacer(final int spacerIndex,
- ScrollDestination destination, final int padding)
- throws IllegalArgumentException {
- validateScrollDestination(destination, padding);
- body.scrollToSpacer(spacerIndex, destination, padding);
- }
-
- /**
- * Scrolls vertically to a row and the spacer below it.
- * <p>
- * If a spacer is not open at that index, this method behaves like
- * {@link #scrollToRow(int, ScrollDestination, int)}
- *
- * @since 7.5.0
- * @param rowIndex
- * the index of the logical row to scroll to. -1 takes the
- * topmost spacer into account as well.
- * @param destination
- * where the row should be aligned visually after scrolling
- * @param padding
- * the number pixels to place between the scrolled-to row and the
- * viewport edge.
- * @see #scrollToRow(int, ScrollDestination, int)
- * @see #scrollToSpacer(int, ScrollDestination, int)
- * @throws IllegalArgumentException
- * if {@code destination} is {@link ScrollDestination#MIDDLE}
- * and {@code padding} is not zero; or if {@code rowIndex} is
- * not a valid row index, or -1; or if
- * {@code destination == null}; or if {@code rowIndex == -1} and
- * there is no spacer open at that index.
- */
- public void scrollToRowAndSpacer(final int rowIndex,
- final ScrollDestination destination, final int padding)
- throws IllegalArgumentException {
- Scheduler.get().scheduleFinally(new ScheduledCommand() {
- @Override
- public void execute() {
- validateScrollDestination(destination, padding);
- if (rowIndex != -1) {
- verifyValidRowIndex(rowIndex);
- }
-
- // row range
- final Range rowRange;
- if (rowIndex != -1) {
- int rowTop = (int) Math.floor(body.getRowTop(rowIndex));
- int rowHeight = (int) Math.ceil(body.getDefaultRowHeight());
- rowRange = Range.withLength(rowTop, rowHeight);
- } else {
- rowRange = Range.withLength(0, 0);
- }
-
- // get spacer
- final SpacerContainer.SpacerImpl spacer = body.spacerContainer
- .getSpacer(rowIndex);
-
- if (rowIndex == -1 && spacer == null) {
- throw new IllegalArgumentException(
- "Cannot scroll to row index "
- + "-1, as there is no spacer open at that index.");
- }
-
- // make into target range
- final Range targetRange;
- if (spacer != null) {
- final int spacerTop = (int) Math.floor(spacer.getTop());
- final int spacerHeight = (int) Math
- .ceil(spacer.getHeight());
- Range spacerRange = Range.withLength(spacerTop,
- spacerHeight);
-
- targetRange = rowRange.combineWith(spacerRange);
- } else {
- targetRange = rowRange;
- }
-
- // get params
- int targetStart = targetRange.getStart();
- int targetEnd = targetRange.getEnd();
- double viewportStart = getScrollTop();
- double viewportEnd = viewportStart + body.getHeightOfSection();
-
- double scrollPos = getScrollPos(destination, targetStart,
- targetEnd, viewportStart, viewportEnd, padding);
-
- setScrollTop(scrollPos);
- }
- });
- }
-
- private static void validateScrollDestination(
- final ScrollDestination destination, final int padding) {
- if (destination == null) {
- throw new IllegalArgumentException("Destination cannot be null");
- }
-
- if (destination == ScrollDestination.MIDDLE && padding != 0) {
- throw new IllegalArgumentException(
- "You cannot have a padding with a MIDDLE destination");
- }
- }
-
- /**
- * Recalculates the dimensions for all elements that require manual
- * calculations. Also updates the dimension caches.
- * <p>
- * <em>Note:</em> This method has the <strong>side-effect</strong>
- * automatically makes sure that an appropriate amount of escalator rows are
- * present. So, if the body area grows, more <strong>escalator rows might be
- * inserted</strong>. Conversely, if the body area shrinks,
- * <strong>escalator rows might be removed</strong>.
- */
- private void recalculateElementSizes() {
- if (!isAttached()) {
- return;
- }
-
- Profiler.enter("Escalator.recalculateElementSizes");
- widthOfEscalator = Math.max(0, getBoundingWidth(getElement()));
- heightOfEscalator = Math.max(0, getBoundingHeight(getElement()));
-
- header.recalculateSectionHeight();
- body.recalculateSectionHeight();
- footer.recalculateSectionHeight();
-
- scroller.recalculateScrollbarsForVirtualViewport();
- body.verifyEscalatorCount();
- body.reapplySpacerWidths();
- Profiler.leave("Escalator.recalculateElementSizes");
- }
-
- /**
- * Snap deltas of x and y to the major four axes (up, down, left, right)
- * with a threshold of a number of degrees from those axes.
- *
- * @param deltaX
- * the delta in the x axis
- * @param deltaY
- * the delta in the y axis
- * @param thresholdRatio
- * the threshold in ratio (0..1) between x and y for when to snap
- * @return a two-element array: <code>[snappedX, snappedY]</code>
- */
- private static double[] snapDeltas(final double deltaX, final double deltaY,
- final double thresholdRatio) {
-
- final double[] array = new double[2];
- if (deltaX != 0 && deltaY != 0) {
- final double aDeltaX = Math.abs(deltaX);
- final double aDeltaY = Math.abs(deltaY);
- final double yRatio = aDeltaY / aDeltaX;
- final double xRatio = aDeltaX / aDeltaY;
-
- array[0] = (xRatio < thresholdRatio) ? 0 : deltaX;
- array[1] = (yRatio < thresholdRatio) ? 0 : deltaY;
- } else {
- array[0] = deltaX;
- array[1] = deltaY;
- }
-
- return array;
- }
-
- /**
- * Adds an event handler that gets notified when the range of visible rows
- * changes e.g. because of scrolling, row resizing or spacers
- * appearing/disappearing.
- *
- * @param rowVisibilityChangeHandler
- * the event handler
- * @return a handler registration for the added handler
- */
- public HandlerRegistration addRowVisibilityChangeHandler(
- RowVisibilityChangeHandler rowVisibilityChangeHandler) {
- return addHandler(rowVisibilityChangeHandler,
- RowVisibilityChangeEvent.TYPE);
- }
-
- private void fireRowVisibilityChangeEvent() {
- if (!body.visualRowOrder.isEmpty()) {
- int visibleRangeStart = body
- .getLogicalRowIndex(body.visualRowOrder.getFirst());
- int visibleRangeEnd = body
- .getLogicalRowIndex(body.visualRowOrder.getLast()) + 1;
-
- int visibleRowCount = visibleRangeEnd - visibleRangeStart;
- fireEvent(new RowVisibilityChangeEvent(visibleRangeStart,
- visibleRowCount));
- } else {
- fireEvent(new RowVisibilityChangeEvent(0, 0));
- }
- }
-
- /**
- * Gets the logical index range of currently visible rows.
- *
- * @return logical index range of visible rows
- */
- public Range getVisibleRowRange() {
- if (!body.visualRowOrder.isEmpty()) {
- return Range.withLength(body.getTopRowLogicalIndex(),
- body.visualRowOrder.size());
- } else {
- return Range.withLength(0, 0);
- }
- }
-
- /**
- * Returns the widget from a cell node or <code>null</code> if there is no
- * widget in the cell
- *
- * @param cellNode
- * The cell node
- */
- static Widget getWidgetFromCell(Node cellNode) {
- Node possibleWidgetNode = cellNode.getFirstChild();
- if (possibleWidgetNode != null
- && possibleWidgetNode.getNodeType() == Node.ELEMENT_NODE) {
- @SuppressWarnings("deprecation")
- com.google.gwt.user.client.Element castElement = (com.google.gwt.user.client.Element) possibleWidgetNode
- .cast();
- Widget w = WidgetUtil.findWidget(castElement);
-
- // Ensure findWidget did not traverse past the cell element in the
- // DOM hierarchy
- if (cellNode.isOrHasChild(w.getElement())) {
- return w;
- }
- }
- return null;
- }
-
- @Override
- public void setStylePrimaryName(String style) {
- super.setStylePrimaryName(style);
-
- verticalScrollbar.setStylePrimaryName(style);
- horizontalScrollbar.setStylePrimaryName(style);
-
- UIObject.setStylePrimaryName(tableWrapper, style + "-tablewrapper");
- UIObject.setStylePrimaryName(headerDeco, style + "-header-deco");
- UIObject.setStylePrimaryName(footerDeco, style + "-footer-deco");
- UIObject.setStylePrimaryName(horizontalScrollbarDeco,
- style + "-horizontal-scrollbar-deco");
- UIObject.setStylePrimaryName(spacerDecoContainer,
- style + "-spacer-deco-container");
-
- header.setStylePrimaryName(style);
- body.setStylePrimaryName(style);
- footer.setStylePrimaryName(style);
- }
-
- /**
- * Sets the number of rows that should be visible in Escalator's body, while
- * {@link #getHeightMode()} is {@link HeightMode#ROW}.
- * <p>
- * If Escalator is currently not in {@link HeightMode#ROW}, the given value
- * is remembered, and applied once the mode is applied.
- *
- * @param rows
- * the number of rows that should be visible in Escalator's body
- * @throws IllegalArgumentException
- * if {@code rows} is ≤ 0, {@link Double#isInfinite(double)
- * infinite} or {@link Double#isNaN(double) NaN}.
- * @see #setHeightMode(HeightMode)
- */
- public void setHeightByRows(double rows) throws IllegalArgumentException {
- if (rows < 0) {
- throw new IllegalArgumentException(
- "The number of rows must be a positive number.");
- } else if (Double.isInfinite(rows)) {
- throw new IllegalArgumentException(
- "The number of rows must be finite.");
- } else if (Double.isNaN(rows)) {
- throw new IllegalArgumentException("The number must not be NaN.");
- }
-
- heightByRows = rows;
- applyHeightByRows();
- }
-
- /**
- * Gets the amount of rows in Escalator's body that are shown, while
- * {@link #getHeightMode()} is {@link HeightMode#ROW}.
- * <p>
- * By default, it is 10.
- *
- * @return the amount of rows that are being shown in Escalator's body
- * @see #setHeightByRows(double)
- */
- public double getHeightByRows() {
- return heightByRows;
- }
-
- /**
- * Reapplies the row-based height of the Grid, if Grid currently should
- * define its height that way.
- */
- private void applyHeightByRows() {
- if (heightMode != HeightMode.ROW
- && heightMode != HeightMode.UNDEFINED) {
- return;
- }
-
- double headerHeight = header.getHeightOfSection();
- double footerHeight = footer.getHeightOfSection();
- double bodyHeight = body.getDefaultRowHeight() * heightByRows;
- double scrollbar = horizontalScrollbar.showsScrollHandle()
- ? horizontalScrollbar.getScrollbarThickness() : 0;
- double spacerHeight = 0; // ignored if HeightMode.ROW
- if (heightMode == HeightMode.UNDEFINED) {
- spacerHeight = body.spacerContainer.getSpacerHeightsSum();
- }
-
- double totalHeight = headerHeight + bodyHeight + spacerHeight
- + scrollbar + footerHeight;
- setHeightInternal(totalHeight + "px");
- }
-
- /**
- * Defines the mode in which the Escalator widget's height is calculated.
- * <p>
- * If {@link HeightMode#CSS} is given, Escalator will respect the values
- * given via {@link #setHeight(String)}, and behave as a traditional Widget.
- * <p>
- * If {@link HeightMode#ROW} is given, Escalator will make sure that the
- * {@link #getBody() body} will display as many rows as
- * {@link #getHeightByRows()} defines. <em>Note:</em> If headers/footers are
- * inserted or removed, the widget will resize itself to still display the
- * required amount of rows in its body. It also takes the horizontal
- * scrollbar into account.
- *
- * @param heightMode
- * the mode in to which Escalator should be set
- */
- public void setHeightMode(HeightMode heightMode) {
- /*
- * This method is a workaround for the fact that Vaadin re-applies
- * widget dimensions (height/width) on each state change event. The
- * original design was to have setHeight an setHeightByRow be equals,
- * and whichever was called the latest was considered in effect.
- *
- * But, because of Vaadin always calling setHeight on the widget, this
- * approach doesn't work.
- */
-
- if (heightMode != this.heightMode) {
- this.heightMode = heightMode;
-
- switch (this.heightMode) {
- case CSS:
- setHeight(heightByCss);
- break;
- case ROW:
- setHeightByRows(heightByRows);
- break;
- case UNDEFINED:
- setHeightByRows(body.getRowCount());
- break;
- default:
- throw new IllegalStateException("Unimplemented feature "
- + "- unknown HeightMode: " + this.heightMode);
- }
- }
- }
-
- /**
- * Returns the current {@link HeightMode} the Escalator is in.
- * <p>
- * Defaults to {@link HeightMode#CSS}.
- *
- * @return the current HeightMode
- */
- public HeightMode getHeightMode() {
- return heightMode;
- }
-
- /**
- * Returns the {@link RowContainer} which contains the element.
- *
- * @param element
- * the element to check for
- * @return the container the element is in or <code>null</code> if element
- * is not present in any container.
- */
- public RowContainer findRowContainer(Element element) {
- if (getHeader().getElement() != element
- && getHeader().getElement().isOrHasChild(element)) {
- return getHeader();
- } else if (getBody().getElement() != element
- && getBody().getElement().isOrHasChild(element)) {
- return getBody();
- } else if (getFooter().getElement() != element
- && getFooter().getElement().isOrHasChild(element)) {
- return getFooter();
- }
- return null;
- }
-
- /**
- * Sets whether a scroll direction is locked or not.
- * <p>
- * If a direction is locked, the escalator will refuse to scroll in that
- * direction.
- *
- * @param direction
- * the orientation of the scroll to set the lock status
- * @param locked
- * <code>true</code> to lock, <code>false</code> to unlock
- */
- public void setScrollLocked(ScrollbarBundle.Direction direction,
- boolean locked) {
- switch (direction) {
- case HORIZONTAL:
- horizontalScrollbar.setLocked(locked);
- break;
- case VERTICAL:
- verticalScrollbar.setLocked(locked);
- break;
- default:
- throw new UnsupportedOperationException(
- "Unexpected value: " + direction);
- }
- }
-
- /**
- * Checks whether or not an direction is locked for scrolling.
- *
- * @param direction
- * the direction of the scroll of which to check the lock status
- * @return <code>true</code> iff the direction is locked
- */
- public boolean isScrollLocked(ScrollbarBundle.Direction direction) {
- switch (direction) {
- case HORIZONTAL:
- return horizontalScrollbar.isLocked();
- case VERTICAL:
- return verticalScrollbar.isLocked();
- default:
- throw new UnsupportedOperationException(
- "Unexpected value: " + direction);
- }
- }
-
- /**
- * Adds a scroll handler to this escalator
- *
- * @param handler
- * the scroll handler to add
- * @return a handler registration for the registered scroll handler
- */
- public HandlerRegistration addScrollHandler(ScrollHandler handler) {
- return addHandler(handler, ScrollEvent.TYPE);
- }
-
- /**
- * Returns true if the Escalator is currently scrolling by touch, or has not
- * made the decision yet whether to accept touch actions as scrolling or
- * not.
- *
- * @see #setDelayToCancelTouchScroll(double)
- *
- * @return true when the component is touch scrolling at the moment
- * @since 8.1
- */
- public boolean isTouchScrolling() {
- return scroller.touchHandlerBundle.touching;
- }
-
- /**
- * Returns the time after which to not consider a touch event a scroll event
- * if the user has not moved the touch. This can be used to differentiate
- * between quick touch move (scrolling) and long tap (e.g. context menu or
- * drag and drop operation).
- *
- * @return delay in milliseconds after which to cancel touch scrolling if
- * there is no movement, -1 means scrolling is always allowed
- * @since 8.1
- */
- public double getDelayToCancelTouchScroll() {
- return delayToCancelTouchScroll;
- }
-
- /**
- * Sets the time after which to not consider a touch event a scroll event if
- * the user has not moved the touch. This can be used to differentiate
- * between quick touch move (scrolling) and long tap (e.g. context menu or
- * drag and drop operation).
- *
- * @param delayToCancelTouchScroll
- * delay in milliseconds after which to cancel touch scrolling if
- * there is no movement, -1 to always allow scrolling
- * @since 8.1
- */
- public void setDelayToCancelTouchScroll(double delayToCancelTouchScroll) {
- this.delayToCancelTouchScroll = delayToCancelTouchScroll;
- }
-
- @Override
- public boolean isWorkPending() {
- return body.domSorter.waiting || verticalScrollbar.isWorkPending()
- || horizontalScrollbar.isWorkPending() || layoutIsScheduled;
- }
-
- @Override
- public void onResize() {
- if (isAttached() && !layoutIsScheduled) {
- layoutIsScheduled = true;
- Scheduler.get().scheduleFinally(layoutCommand);
- }
- }
-
- /**
- * Gets the maximum number of body rows that can be visible on the screen at
- * once.
- *
- * @return the maximum capacity
- */
- public int getMaxVisibleRowCount() {
- return body.getMaxVisibleRowCount();
- }
-
- /**
- * Gets the escalator's inner width. This is the entire width in pixels,
- * without the vertical scrollbar.
- *
- * @return escalator's inner width
- */
- public double getInnerWidth() {
- return getBoundingWidth(tableWrapper);
- }
-
- /**
- * Resets all cached pixel sizes and reads new values from the DOM. This
- * methods should be used e.g. when styles affecting the dimensions of
- * elements in this escalator have been changed.
- */
- public void resetSizesFromDom() {
- header.autodetectRowHeightNow();
- body.autodetectRowHeightNow();
- footer.autodetectRowHeightNow();
-
- for (int i = 0; i < columnConfiguration.getColumnCount(); i++) {
- columnConfiguration.setColumnWidth(i,
- columnConfiguration.getColumnWidth(i));
- }
- }
-
- private Range getViewportPixels() {
- int from = (int) Math.floor(verticalScrollbar.getScrollPos());
- int to = (int) body.getHeightOfSection();
- return Range.withLength(from, to);
- }
-
- @Override
- @SuppressWarnings("deprecation")
- public com.google.gwt.user.client.Element getSubPartElement(
- String subPart) {
- SubPartArguments args = SubPartArguments.create(subPart);
-
- Element tableStructureElement = getSubPartElementTableStructure(args);
- if (tableStructureElement != null) {
- return DOM.asOld(tableStructureElement);
- }
-
- Element spacerElement = getSubPartElementSpacer(args);
- if (spacerElement != null) {
- return DOM.asOld(spacerElement);
- }
-
- return null;
- }
-
- /**
- * Returns the {@code <div class="{primary-stylename}-tablewrapper" />}
- * element which has the table inside it. {primary-stylename} is .e.g
- * {@code v-grid}.
- * <p>
- * <em>NOTE: you should not do any modifications to the returned element.
- * This API is only available for querying data from the element.</em>
- *
- * @return the table wrapper element
- * @since 8.1
- */
- public Element getTableWrapper() {
- return tableWrapper;
- }
-
- private Element getSubPartElementTableStructure(SubPartArguments args) {
-
- String type = args.getType();
- int[] indices = args.getIndices();
-
- // Get correct RowContainer for type from Escalator
- RowContainer container = null;
- if (type.equalsIgnoreCase("header")) {
- container = getHeader();
- } else if (type.equalsIgnoreCase("cell")) {
- // If wanted row is not visible, we need to scroll there.
- Range visibleRowRange = getVisibleRowRange();
- if (indices.length > 0) {
- // Contains a row number, ensure it is available and visible
- boolean rowInCache = visibleRowRange.contains(indices[0]);
-
- // Scrolling might be a no-op if row is already in the viewport
- scrollToRow(indices[0], ScrollDestination.ANY, 0);
-
- if (!rowInCache) {
- // Row was not in cache, scrolling caused lazy loading and
- // the caller needs to wait and call this method again to be
- // able to get the requested element
- return null;
- }
- }
- container = getBody();
- } else if (type.equalsIgnoreCase("footer")) {
- container = getFooter();
- }
-
- if (null != container) {
- if (indices.length == 0) {
- // No indexing. Just return the wanted container element
- return container.getElement();
- } else {
- try {
- return getSubPart(container, indices);
- } catch (Exception e) {
- getLogger().log(Level.SEVERE, e.getMessage());
- }
- }
- }
- return null;
- }
-
- private Element getSubPart(RowContainer container, int[] indices) {
- Element targetElement = container.getRowElement(indices[0]);
-
- // Scroll wanted column to view if able
- if (indices.length > 1 && targetElement != null) {
- if (getColumnConfiguration().getFrozenColumnCount() <= indices[1]) {
- scrollToColumn(indices[1], ScrollDestination.ANY, 0);
- }
-
- targetElement = getCellFromRow(TableRowElement.as(targetElement),
- indices[1]);
-
- for (int i = 2; i < indices.length && targetElement != null; ++i) {
- targetElement = (Element) targetElement.getChild(indices[i]);
- }
- }
-
- return targetElement;
- }
-
- private static Element getCellFromRow(TableRowElement rowElement,
- int index) {
- int childCount = rowElement.getCells().getLength();
- if (index < 0 || index >= childCount) {
- return null;
- }
-
- TableCellElement currentCell = null;
- boolean indexInColspan = false;
- int i = 0;
-
- while (!indexInColspan) {
- currentCell = rowElement.getCells().getItem(i);
-
- // Calculate if this is the cell we are looking for
- int colSpan = currentCell.getColSpan();
- indexInColspan = index < colSpan + i;
-
- // Increment by colspan to skip over hidden cells
- i += colSpan;
- }
- return currentCell;
- }
-
- private Element getSubPartElementSpacer(SubPartArguments args) {
- if ("spacer".equals(args.getType()) && args.getIndicesLength() == 1) {
- return body.spacerContainer.getSubPartElement(args.getIndex(0));
- } else {
- return null;
- }
- }
-
- @Override
- @SuppressWarnings("deprecation")
- public String getSubPartName(
- com.google.gwt.user.client.Element subElement) {
-
- /*
- * The spacer check needs to be before table structure check, because
- * (for now) the table structure will take spacer elements into account
- * as well, when it shouldn't.
- */
-
- String spacer = getSubPartNameSpacer(subElement);
- if (spacer != null) {
- return spacer;
- }
-
- String tableStructure = getSubPartNameTableStructure(subElement);
- if (tableStructure != null) {
- return tableStructure;
- }
-
- return null;
- }
-
- private String getSubPartNameTableStructure(Element subElement) {
-
- List<RowContainer> containers = Arrays.asList(getHeader(), getBody(),
- getFooter());
- List<String> containerType = Arrays.asList("header", "cell", "footer");
-
- for (int i = 0; i < containers.size(); ++i) {
- RowContainer container = containers.get(i);
- boolean containerRow = (subElement.getTagName()
- .equalsIgnoreCase("tr")
- && subElement.getParentElement() == container.getElement());
- if (containerRow) {
- /*
- * Wanted SubPart is row that is a child of containers root to
- * get indices, we use a cell that is a child of this row
- */
- subElement = subElement.getFirstChildElement();
- }
-
- Cell cell = container.getCell(subElement);
- if (cell != null) {
- // Skip the column index if subElement was a child of root
- return containerType.get(i) + "[" + cell.getRow()
- + (containerRow ? "]" : "][" + cell.getColumn() + "]");
- }
- }
- return null;
- }
-
- private String getSubPartNameSpacer(Element subElement) {
- return body.spacerContainer.getSubPartName(subElement);
- }
-
- private void logWarning(String message) {
- getLogger().warning(message);
- }
-
- /**
- * This is an internal method for calculating minimum width for Column
- * resize.
- *
- * @return minimum width for column
- */
- double getMinCellWidth(int colIndex) {
- return columnConfiguration.getMinCellWidth(colIndex);
- }
-
- /**
- * Internal method for checking whether the browser is IE11 or Edge
- *
- * @return true only if the current browser is IE11, or Edge
- */
- private static boolean isCurrentBrowserIE11OrEdge() {
- return BrowserInfo.get().isIE11() || BrowserInfo.get().isEdge();
- }
- }
|