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

Escalator.java 344KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594
  1. /*
  2. * Copyright 2000-2021 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client.widgets;
  17. import java.util.ArrayList;
  18. import java.util.Arrays;
  19. import java.util.Collection;
  20. import java.util.Collections;
  21. import java.util.HashMap;
  22. import java.util.LinkedList;
  23. import java.util.List;
  24. import java.util.ListIterator;
  25. import java.util.Map;
  26. import java.util.Map.Entry;
  27. import java.util.Optional;
  28. import java.util.TreeMap;
  29. import java.util.function.Consumer;
  30. import java.util.logging.Level;
  31. import java.util.logging.Logger;
  32. import java.util.stream.Stream;
  33. import com.google.gwt.animation.client.Animation;
  34. import com.google.gwt.animation.client.AnimationScheduler;
  35. import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
  36. import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
  37. import com.google.gwt.core.client.Duration;
  38. import com.google.gwt.core.client.JavaScriptException;
  39. import com.google.gwt.core.client.JavaScriptObject;
  40. import com.google.gwt.core.client.JsArray;
  41. import com.google.gwt.core.client.Scheduler;
  42. import com.google.gwt.core.client.Scheduler.ScheduledCommand;
  43. import com.google.gwt.dom.client.DivElement;
  44. import com.google.gwt.dom.client.Document;
  45. import com.google.gwt.dom.client.Element;
  46. import com.google.gwt.dom.client.NativeEvent;
  47. import com.google.gwt.dom.client.Node;
  48. import com.google.gwt.dom.client.NodeList;
  49. import com.google.gwt.dom.client.Style;
  50. import com.google.gwt.dom.client.Style.Display;
  51. import com.google.gwt.dom.client.Style.Unit;
  52. import com.google.gwt.dom.client.TableCellElement;
  53. import com.google.gwt.dom.client.TableRowElement;
  54. import com.google.gwt.dom.client.TableSectionElement;
  55. import com.google.gwt.dom.client.Touch;
  56. import com.google.gwt.event.dom.client.KeyCodes;
  57. import com.google.gwt.event.shared.HandlerRegistration;
  58. import com.google.gwt.logging.client.LogConfiguration;
  59. import com.google.gwt.user.client.DOM;
  60. import com.google.gwt.user.client.Event;
  61. import com.google.gwt.user.client.Window;
  62. import com.google.gwt.user.client.ui.RequiresResize;
  63. import com.google.gwt.user.client.ui.RootPanel;
  64. import com.google.gwt.user.client.ui.UIObject;
  65. import com.google.gwt.user.client.ui.Widget;
  66. import com.vaadin.client.BrowserInfo;
  67. import com.vaadin.client.ComputedStyle;
  68. import com.vaadin.client.DeferredWorker;
  69. import com.vaadin.client.Profiler;
  70. import com.vaadin.client.WidgetUtil;
  71. import com.vaadin.client.ui.SubPartAware;
  72. import com.vaadin.client.widget.escalator.Cell;
  73. import com.vaadin.client.widget.escalator.ColumnConfiguration;
  74. import com.vaadin.client.widget.escalator.EscalatorUpdater;
  75. import com.vaadin.client.widget.escalator.FlyweightCell;
  76. import com.vaadin.client.widget.escalator.FlyweightRow;
  77. import com.vaadin.client.widget.escalator.PositionFunction;
  78. import com.vaadin.client.widget.escalator.PositionFunction.Translate3DPosition;
  79. import com.vaadin.client.widget.escalator.PositionFunction.TranslatePosition;
  80. import com.vaadin.client.widget.escalator.PositionFunction.WebkitTranslate3DPosition;
  81. import com.vaadin.client.widget.escalator.Row;
  82. import com.vaadin.client.widget.escalator.RowContainer;
  83. import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer;
  84. import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent;
  85. import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler;
  86. import com.vaadin.client.widget.escalator.ScrollbarBundle;
  87. import com.vaadin.client.widget.escalator.ScrollbarBundle.Direction;
  88. import com.vaadin.client.widget.escalator.ScrollbarBundle.HorizontalScrollbarBundle;
  89. import com.vaadin.client.widget.escalator.ScrollbarBundle.VerticalScrollbarBundle;
  90. import com.vaadin.client.widget.escalator.Spacer;
  91. import com.vaadin.client.widget.escalator.SpacerUpdater;
  92. import com.vaadin.client.widget.escalator.events.RowHeightChangedEvent;
  93. import com.vaadin.client.widget.escalator.events.SpacerIndexChangedEvent;
  94. import com.vaadin.client.widget.escalator.events.SpacerVisibilityChangedEvent;
  95. import com.vaadin.client.widget.grid.events.EscalatorSizeChangeHandler;
  96. import com.vaadin.client.widget.grid.events.EscalatorSizeChangeHandler.EscalatorSizeChangeEvent;
  97. import com.vaadin.client.widget.grid.events.ScrollEvent;
  98. import com.vaadin.client.widget.grid.events.ScrollHandler;
  99. import com.vaadin.client.widget.grid.events.VerticalScrollbarVisibilityChangeHandler;
  100. import com.vaadin.client.widget.grid.events.VerticalScrollbarVisibilityChangeHandler.VerticalScrollbarVisibilityChangeEvent;
  101. import com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle;
  102. import com.vaadin.shared.Range;
  103. import com.vaadin.shared.ui.grid.HeightMode;
  104. import com.vaadin.shared.ui.grid.ScrollDestination;
  105. import com.vaadin.shared.util.SharedUtil;
  106. /*-
  107. Maintenance Notes! Reading these might save your day.
  108. (note for editors: line width is 80 chars, including the
  109. one-space indentation)
  110. == Row Container Structure
  111. AbstractRowContainer
  112. |-- AbstractStaticRowContainer
  113. | |-- HeaderRowContainer
  114. | `-- FooterContainer
  115. `---- BodyRowContainerImpl
  116. AbstractRowContainer is intended to contain all common logic
  117. between RowContainers. It manages the bookkeeping of row
  118. count, makes sure that all individual cells are rendered
  119. the same way, and so on.
  120. AbstractStaticRowContainer has some special logic that is
  121. required by all RowContainers that don't scroll (hence the
  122. word "static"). HeaderRowContainer and FooterRowContainer
  123. are pretty thin special cases of a StaticRowContainer
  124. (mostly relating to positioning of the root element).
  125. BodyRowContainerImpl could also be split into an additional
  126. "AbstractScrollingRowContainer", but I felt that no more
  127. inner classes were needed. So it contains both logic
  128. required for making things scroll about, and equivalent
  129. special cases for layouting, as are found in
  130. Header/FooterRowContainers.
  131. == The Three Indices
  132. Each RowContainer can be thought to have three levels of
  133. indices for any given displayed row (but the distinction
  134. matters primarily for the BodyRowContainerImpl, because of
  135. the way it scrolls through data):
  136. - Logical index
  137. - Physical (or DOM) index
  138. - Visual index
  139. LOGICAL INDEX is the index that is linked to the data
  140. source. If you want your data source to represent a SQL
  141. database with 10 000 rows, the 7 000:th row in the SQL has a
  142. logical index of 6 999, since the index is 0-based (unless
  143. that data source does some funky logic).
  144. PHYSICAL INDEX is the index for a row that you see in a
  145. browser's DOM inspector. If your row is the second <tr>
  146. element within a <tbody> tag, it has a physical index of 1
  147. (because of 0-based indices). In Header and
  148. FooterRowContainers, you are safe to assume that the logical
  149. index is the same as the physical index. But because the
  150. BodyRowContainerImpl never displays large data sources
  151. entirely in the DOM, a physical index usually has no
  152. apparent direct relationship with its logical index.
  153. This is the sectionRowIndex in TableRowElements.
  154. RowIndex in TableRowElements displays the physical index
  155. of all row elements, headers and footers included.
  156. VISUAL INDEX is the index relating to the order that you
  157. see a row in, in the browser, as it is rendered. The
  158. topmost row is 0, the second is 1, and so on. The visual
  159. index is similar to the physical index in the sense that
  160. Header and FooterRowContainers can assume a 1:1
  161. relationship between visual index and logical index. And
  162. again, BodyRowContainerImpl has no such relationship. The
  163. body's visual index has additionally no apparent
  164. relationship with its physical index. Because the <tr> tags
  165. are reused in the body and visually repositioned with CSS
  166. as the user scrolls, the relationship between physical
  167. index and visual index is quickly broken. You can get an
  168. element's visual index via the field
  169. BodyRowContainerImpl.visualRowOrder.
  170. Currently, the physical and visual indices are kept in sync
  171. _most of the time_ by a deferred rearrangement of rows.
  172. They become desynced when scrolling. This is to help screen
  173. readers to read the contents from the DOM in a natural
  174. order. See BodyRowContainerImpl.DeferredDomSorter for more
  175. about that.
  176. It should be noted that the entire visual range is not
  177. necessarily in view at any given time, although it should be
  178. optimised to not exceed the maximum amount of rows that can
  179. theoretically fit within the viewport when their associated
  180. spacers have zero height, except by the two rows that are
  181. required for tab navigation to work.
  182. */
  183. /**
  184. * A workaround-class for GWT and JSNI.
  185. * <p>
  186. * GWT is unable to handle some method calls to Java methods in inner-classes
  187. * from within JSNI blocks. Having that inner class extend a non-inner-class (or
  188. * implement such an interface), makes it possible for JSNI to indirectly refer
  189. * to the inner class, by invoking methods and fields in the non-inner-class
  190. * API.
  191. *
  192. * @see Escalator.Scroller
  193. */
  194. abstract class JsniWorkaround {
  195. /**
  196. * A JavaScript function that handles the scroll DOM event, and passes it on
  197. * to Java code.
  198. *
  199. * @see #createScrollListenerFunction(Escalator)
  200. * @see Escalator.Scroller#onScroll()
  201. */
  202. protected final JavaScriptObject scrollListenerFunction;
  203. /**
  204. * A JavaScript function that handles the mousewheel DOM event, and passes
  205. * it on to Java code.
  206. *
  207. * @see #createMousewheelListenerFunction(Escalator)
  208. * @see Escalator.Scroller#onScroll()
  209. */
  210. protected final JavaScriptObject mousewheelListenerFunction;
  211. /**
  212. * A JavaScript function that handles the touch start DOM event, and passes
  213. * it on to Java code.
  214. *
  215. * @see TouchHandlerBundle#touchStart(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
  216. */
  217. protected JavaScriptObject touchStartFunction;
  218. /**
  219. * A JavaScript function that handles the touch move DOM event, and passes
  220. * it on to Java code.
  221. *
  222. * @see TouchHandlerBundle#touchMove(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
  223. */
  224. protected JavaScriptObject touchMoveFunction;
  225. /**
  226. * A JavaScript function that handles the touch end and cancel DOM events,
  227. * and passes them on to Java code.
  228. *
  229. * @see TouchHandlerBundle#touchEnd(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
  230. */
  231. protected JavaScriptObject touchEndFunction;
  232. protected TouchHandlerBundle touchHandlerBundle;
  233. protected JsniWorkaround(final Escalator escalator) {
  234. scrollListenerFunction = createScrollListenerFunction(escalator);
  235. mousewheelListenerFunction = createMousewheelListenerFunction(
  236. escalator);
  237. touchHandlerBundle = new TouchHandlerBundle(escalator);
  238. touchStartFunction = touchHandlerBundle.getTouchStartHandler();
  239. touchMoveFunction = touchHandlerBundle.getTouchMoveHandler();
  240. touchEndFunction = touchHandlerBundle.getTouchEndHandler();
  241. }
  242. /**
  243. * A method that constructs the JavaScript function that will be stored into
  244. * {@link #scrollListenerFunction}.
  245. *
  246. * @param esc
  247. * a reference to the current instance of {@link Escalator}
  248. * @see Escalator.Scroller#onScroll()
  249. */
  250. protected abstract JavaScriptObject createScrollListenerFunction(
  251. Escalator esc);
  252. /**
  253. * A method that constructs the JavaScript function that will be stored into
  254. * {@link #mousewheelListenerFunction}.
  255. *
  256. * @param esc
  257. * a reference to the current instance of {@link Escalator}
  258. * @see Escalator.Scroller#onScroll()
  259. */
  260. protected abstract JavaScriptObject createMousewheelListenerFunction(
  261. Escalator esc);
  262. }
  263. /**
  264. * A low-level table-like widget that features a scrolling virtual viewport and
  265. * lazily generated rows.
  266. *
  267. * @since 7.4
  268. * @author Vaadin Ltd
  269. */
  270. public class Escalator extends Widget
  271. implements RequiresResize, DeferredWorker, SubPartAware {
  272. // todo comments legend
  273. /*
  274. * [[optimize]]: There's an opportunity to rewrite the code in such a way
  275. * that it _might_ perform better (remember to measure, implement,
  276. * re-measure)
  277. */
  278. /*
  279. * [[mpixscroll]]: This code will require alterations that are relevant for
  280. * supporting the scrolling through more pixels than some browsers normally
  281. * would support. (i.e. when we support more than "a million" pixels in the
  282. * escalator DOM). NOTE: these bits can most often also be identified by
  283. * searching for code that call scrollElem.getScrollTop();.
  284. */
  285. /*
  286. * [[spacer]]: Code that is important to make spacers work.
  287. */
  288. /**
  289. * A utility class that contains utility methods that are usually called
  290. * from JSNI.
  291. * <p>
  292. * The methods are moved in this class to minimize the amount of JSNI code
  293. * as much as feasible.
  294. */
  295. static class JsniUtil {
  296. public static class TouchHandlerBundle {
  297. public static final String POINTER_EVENT_TYPE_TOUCH = "touch";
  298. public static final int SIGNIFICANT_MOVE_THRESHOLD = 3;
  299. /**
  300. * A <a href=
  301. * "http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsOverlay.html"
  302. * >JavaScriptObject overlay</a> for the
  303. * <a href="http://www.w3.org/TR/touch-events/">JavaScript
  304. * TouchEvent</a> object.
  305. * <p>
  306. * This needs to be used in the touch event handlers, since GWT's
  307. * {@link com.google.gwt.event.dom.client.TouchEvent TouchEvent}
  308. * can't be cast from the JSNI call, and the
  309. * {@link com.google.gwt.dom.client.NativeEvent NativeEvent} isn't
  310. * properly populated with the correct values.
  311. */
  312. private static final class CustomTouchEvent
  313. extends JavaScriptObject {
  314. protected CustomTouchEvent() {
  315. }
  316. public native NativeEvent getNativeEvent()
  317. /*-{
  318. return this;
  319. }-*/;
  320. public native int getPageX()
  321. /*-{
  322. return this.targetTouches[0].pageX;
  323. }-*/;
  324. public native int getPageY()
  325. /*-{
  326. return this.targetTouches[0].pageY;
  327. }-*/;
  328. public native String getPointerType()
  329. /*-{
  330. return this.pointerType;
  331. }-*/;
  332. }
  333. private final Escalator escalator;
  334. public TouchHandlerBundle(final Escalator escalator) {
  335. this.escalator = escalator;
  336. }
  337. public native JavaScriptObject getTouchStartHandler()
  338. /*-{
  339. // we need to store "this", since it won't be preserved on call.
  340. var self = this;
  341. return $entry(function (e) {
  342. self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchStart(*)(e);
  343. });
  344. }-*/;
  345. public native JavaScriptObject getTouchMoveHandler()
  346. /*-{
  347. // we need to store "this", since it won't be preserved on call.
  348. var self = this;
  349. return $entry(function (e) {
  350. self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchMove(*)(e);
  351. });
  352. }-*/;
  353. public native JavaScriptObject getTouchEndHandler()
  354. /*-{
  355. // we need to store "this", since it won't be preserved on call.
  356. var self = this;
  357. return $entry(function (e) {
  358. self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchEnd(*)(e);
  359. });
  360. }-*/;
  361. // Duration of the inertial scrolling simulation. Devices with
  362. // larger screens take longer durations.
  363. private static final int DURATION = Window.getClientHeight();
  364. // multiply scroll velocity with repeated touching
  365. private int acceleration = 1;
  366. private boolean touching = false;
  367. // Two movement objects for storing status and processing touches
  368. private Movement yMov, xMov;
  369. // true if moved significantly since touch start
  370. private boolean movedSignificantly = false;
  371. private double touchStartTime;
  372. final double MIN_VEL = 0.6, MAX_VEL = 4, F_VEL = 1500, F_ACC = 0.7,
  373. F_AXIS = 1;
  374. // The object to deal with one direction scrolling
  375. private class Movement {
  376. final List<Double> speeds = new ArrayList<>();
  377. final ScrollbarBundle scroll;
  378. double position, offset, velocity, prevPos, prevTime, delta;
  379. boolean run, vertical;
  380. public Movement(boolean vertical) {
  381. this.vertical = vertical;
  382. scroll = vertical ? escalator.verticalScrollbar
  383. : escalator.horizontalScrollbar;
  384. }
  385. public void startTouch(CustomTouchEvent event) {
  386. speeds.clear();
  387. prevPos = pagePosition(event);
  388. prevTime = Duration.currentTimeMillis();
  389. }
  390. public void moveTouch(CustomTouchEvent event) {
  391. double pagePosition = pagePosition(event);
  392. if (pagePosition > -1) {
  393. delta = prevPos - pagePosition;
  394. double now = Duration.currentTimeMillis();
  395. double ellapsed = now - prevTime;
  396. velocity = delta / ellapsed;
  397. // if last speed was so low, reset speeds and start
  398. // storing again
  399. if (!speeds.isEmpty() && !validSpeed(speeds.get(0))) {
  400. speeds.clear();
  401. run = true;
  402. }
  403. speeds.add(0, velocity);
  404. prevTime = now;
  405. prevPos = pagePosition;
  406. }
  407. }
  408. public void endTouch(CustomTouchEvent event) {
  409. // Compute average speed
  410. velocity = 0;
  411. for (double s : speeds) {
  412. velocity += s / speeds.size();
  413. }
  414. position = scroll.getScrollPos();
  415. // Compute offset, and adjust it with an easing curve so as
  416. // movement is smoother.
  417. offset = F_VEL * velocity * acceleration
  418. * easingInOutCos(velocity, MAX_VEL);
  419. // Enable or disable inertia movement in this axis
  420. run = validSpeed(velocity);
  421. if (run) {
  422. event.getNativeEvent().preventDefault();
  423. }
  424. }
  425. void validate(Movement other) {
  426. if (!run || other.velocity > 0
  427. && Math.abs(velocity / other.velocity) < F_AXIS) {
  428. delta = offset = 0;
  429. run = false;
  430. }
  431. }
  432. void stepAnimation(double progress) {
  433. scroll.setScrollPos(position + offset * progress);
  434. }
  435. int pagePosition(CustomTouchEvent event) {
  436. // Use native event's screen x and y for IE11 and Edge
  437. // since there is no touches for these browsers (#18737)
  438. if (isCurrentBrowserIE11OrEdge()) {
  439. return vertical
  440. ? event.getNativeEvent().getClientY()
  441. + Window.getScrollTop()
  442. : event.getNativeEvent().getClientX()
  443. + Window.getScrollLeft();
  444. }
  445. JsArray<Touch> a = event.getNativeEvent().getTouches();
  446. return vertical ? a.get(0).getPageY() : a.get(0).getPageX();
  447. }
  448. boolean validSpeed(double speed) {
  449. return Math.abs(speed) > MIN_VEL;
  450. }
  451. }
  452. // Using GWT animations which take care of native animation frames.
  453. private Animation animation = new Animation() {
  454. @Override
  455. public void onUpdate(double progress) {
  456. xMov.stepAnimation(progress);
  457. yMov.stepAnimation(progress);
  458. }
  459. @Override
  460. public double interpolate(double progress) {
  461. return easingOutCirc(progress);
  462. };
  463. @Override
  464. public void onComplete() {
  465. touching = false;
  466. escalator.body.domSorter.reschedule();
  467. };
  468. @Override
  469. public void run(int duration) {
  470. if (xMov.run || yMov.run) {
  471. super.run(duration);
  472. } else {
  473. onComplete();
  474. }
  475. };
  476. };
  477. public void touchStart(final CustomTouchEvent event) {
  478. if (allowTouch(event)) {
  479. if (yMov == null) {
  480. yMov = new Movement(true);
  481. xMov = new Movement(false);
  482. }
  483. if (animation.isRunning()) {
  484. acceleration += F_ACC;
  485. event.getNativeEvent().preventDefault();
  486. animation.cancel();
  487. } else {
  488. acceleration = 1;
  489. }
  490. xMov.startTouch(event);
  491. yMov.startTouch(event);
  492. touchStartTime = Duration.currentTimeMillis();
  493. touching = true;
  494. movedSignificantly = false;
  495. } else {
  496. touching = false;
  497. animation.cancel();
  498. acceleration = 1;
  499. }
  500. }
  501. public void touchMove(final CustomTouchEvent event) {
  502. if (touching) {
  503. if (!movedSignificantly) {
  504. double distanceSquared = Math.abs(xMov.delta)
  505. * Math.abs(xMov.delta)
  506. + Math.abs(yMov.delta) * Math.abs(yMov.delta);
  507. movedSignificantly = distanceSquared > SIGNIFICANT_MOVE_THRESHOLD
  508. * SIGNIFICANT_MOVE_THRESHOLD;
  509. }
  510. // allow handling long press differently, without triggering
  511. // scrolling
  512. if (escalator.getDelayToCancelTouchScroll() >= 0
  513. && !movedSignificantly
  514. && Duration.currentTimeMillis()
  515. - touchStartTime > escalator
  516. .getDelayToCancelTouchScroll()) {
  517. // cancel touch handling, don't prevent event
  518. touching = false;
  519. animation.cancel();
  520. acceleration = 1;
  521. return;
  522. }
  523. xMov.moveTouch(event);
  524. yMov.moveTouch(event);
  525. xMov.validate(yMov);
  526. yMov.validate(xMov);
  527. moveScrollFromEvent(escalator, xMov.delta, yMov.delta,
  528. event.getNativeEvent());
  529. }
  530. }
  531. public void touchEnd(final CustomTouchEvent event) {
  532. if (touching) {
  533. xMov.endTouch(event);
  534. yMov.endTouch(event);
  535. xMov.validate(yMov);
  536. yMov.validate(xMov);
  537. // Adjust duration so as longer movements take more duration
  538. boolean vert = !xMov.run || yMov.run
  539. && Math.abs(yMov.offset) > Math.abs(xMov.offset);
  540. double delta = Math.abs((vert ? yMov : xMov).offset);
  541. animation.run((int) (3 * DURATION * easingOutExp(delta)));
  542. }
  543. }
  544. // Allow touchStart for IE11 and Edge even though there is no touch
  545. // (#18737),
  546. // otherwise allow touch only if there is a single touch in the
  547. // event
  548. private boolean allowTouch(
  549. final TouchHandlerBundle.CustomTouchEvent event) {
  550. if (isCurrentBrowserIE11OrEdge()) {
  551. return (POINTER_EVENT_TYPE_TOUCH
  552. .equals(event.getPointerType()));
  553. } else {
  554. return (event.getNativeEvent().getTouches().length() == 1);
  555. }
  556. }
  557. private double easingInOutCos(double val, double max) {
  558. return 0.5 - 0.5 * Math.cos(Math.PI * Math.signum(val)
  559. * Math.min(Math.abs(val), max) / max);
  560. }
  561. private double easingOutExp(double delta) {
  562. return (1 - Math.pow(2, -delta / 1000));
  563. }
  564. private double easingOutCirc(double progress) {
  565. return Math.sqrt(1 - (progress - 1) * (progress - 1));
  566. }
  567. }
  568. public static void moveScrollFromEvent(final Escalator escalator,
  569. final double deltaX, final double deltaY,
  570. final NativeEvent event) {
  571. boolean scrollPosXChanged = false;
  572. boolean scrollPosYChanged = false;
  573. if (!Double.isNaN(deltaX)) {
  574. double oldScrollPosX = escalator.horizontalScrollbar
  575. .getScrollPos();
  576. escalator.horizontalScrollbar.setScrollPosByDelta(deltaX);
  577. if (oldScrollPosX != escalator.horizontalScrollbar
  578. .getScrollPos()) {
  579. scrollPosXChanged = true;
  580. }
  581. }
  582. if (!Double.isNaN(deltaY)) {
  583. double oldScrollPosY = escalator.verticalScrollbar
  584. .getScrollPos();
  585. escalator.verticalScrollbar.setScrollPosByDelta(deltaY);
  586. if (oldScrollPosY != escalator.verticalScrollbar
  587. .getScrollPos()) {
  588. scrollPosYChanged = true;
  589. }
  590. }
  591. /*
  592. * Only prevent if internal scrolling happened. If there's no more
  593. * room to scroll internally, allow the event to pass further.
  594. */
  595. final boolean warrantedYScroll = deltaY != 0 && scrollPosYChanged
  596. && escalator.verticalScrollbar.showsScrollHandle();
  597. final boolean warrantedXScroll = deltaX != 0 && scrollPosXChanged
  598. && escalator.horizontalScrollbar.showsScrollHandle();
  599. if (warrantedYScroll || warrantedXScroll) {
  600. event.preventDefault();
  601. }
  602. }
  603. }
  604. /**
  605. * ScrollDestination case-specific handling logic.
  606. */
  607. private static double getScrollPos(final ScrollDestination destination,
  608. final double targetStartPx, final double targetEndPx,
  609. final double viewportStartPx, final double viewportEndPx,
  610. final double padding) {
  611. final double viewportLength = viewportEndPx - viewportStartPx;
  612. switch (destination) {
  613. /*
  614. * Scroll as little as possible to show the target element. If the
  615. * element fits into view, this works as START or END depending on the
  616. * current scroll position. If the element does not fit into view, this
  617. * works as START.
  618. */
  619. case ANY: {
  620. final double startScrollPos = targetStartPx - padding;
  621. final double endScrollPos = targetEndPx + padding - viewportLength;
  622. if (startScrollPos < viewportStartPx) {
  623. return startScrollPos;
  624. } else if (targetEndPx + padding > viewportEndPx) {
  625. return endScrollPos;
  626. } else {
  627. // NOOP, it's already visible
  628. return viewportStartPx;
  629. }
  630. }
  631. /*
  632. * Scrolls so that the element is shown at the end of the viewport. The
  633. * viewport will, however, not scroll before its first element.
  634. */
  635. case END: {
  636. return targetEndPx + padding - viewportLength;
  637. }
  638. /*
  639. * Scrolls so that the element is shown in the middle of the viewport.
  640. * The viewport will, however, not scroll beyond its contents, given
  641. * more elements than what the viewport is able to show at once. Under
  642. * no circumstances will the viewport scroll before its first element.
  643. */
  644. case MIDDLE: {
  645. final double targetMiddle = targetStartPx
  646. + (targetEndPx - targetStartPx) / 2;
  647. return targetMiddle - viewportLength / 2;
  648. }
  649. /*
  650. * Scrolls so that the element is shown at the start of the viewport.
  651. * The viewport will, however, not scroll beyond its contents.
  652. */
  653. case START: {
  654. return targetStartPx - padding;
  655. }
  656. /*
  657. * Throw an error if we're here. This can only mean that
  658. * ScrollDestination has been carelessly amended..
  659. */
  660. default: {
  661. throw new IllegalArgumentException(
  662. "Internal: ScrollDestination has been modified, "
  663. + "but Escalator.getScrollPos has not been updated "
  664. + "to match new values.");
  665. }
  666. }
  667. }
  668. /** An inner class that handles all logic related to scrolling. */
  669. private class Scroller extends JsniWorkaround {
  670. private double lastScrollTop = 0;
  671. private double lastScrollLeft = 0;
  672. public Scroller() {
  673. super(Escalator.this);
  674. }
  675. @Override
  676. protected native JavaScriptObject createScrollListenerFunction(
  677. Escalator esc)
  678. /*-{
  679. var vScroll = esc.@com.vaadin.client.widgets.Escalator::verticalScrollbar;
  680. var vScrollElem = vScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::getElement()();
  681. var hScroll = esc.@com.vaadin.client.widgets.Escalator::horizontalScrollbar;
  682. var hScrollElem = hScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::getElement()();
  683. return $entry(function(e) {
  684. var target = e.target;
  685. // in case the scroll event was native (i.e. scrollbars were dragged, or
  686. // the scrollTop/Left was manually modified), the bundles have old cache
  687. // values. We need to make sure that the caches are kept up to date.
  688. if (target === vScrollElem) {
  689. vScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::updateScrollPosFromDom()();
  690. } else if (target === hScrollElem) {
  691. hScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::updateScrollPosFromDom()();
  692. } else {
  693. $wnd.console.error("unexpected scroll target: "+target);
  694. }
  695. });
  696. }-*/;
  697. @Override
  698. protected native JavaScriptObject createMousewheelListenerFunction(
  699. Escalator esc)
  700. /*-{
  701. return $entry(function(e) {
  702. var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
  703. var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
  704. // Delta mode 0 is in pixels; we don't need to do anything...
  705. // A delta mode of 1 means we're scrolling by lines instead of pixels
  706. // We need to scale the number of lines by the default line height
  707. if (e.deltaMode === 1) {
  708. var brc = esc.@com.vaadin.client.widgets.Escalator::body;
  709. deltaY *= brc.@com.vaadin.client.widgets.Escalator.AbstractRowContainer::getDefaultRowHeight()();
  710. }
  711. // Other delta modes aren't supported
  712. if ((e.deltaMode !== undefined) && (e.deltaMode >= 2 || e.deltaMode < 0)) {
  713. var msg = "Unsupported wheel delta mode \"" + e.deltaMode + "\"";
  714. // Print warning message
  715. esc.@com.vaadin.client.widgets.Escalator::logWarning(*)(msg);
  716. }
  717. // IE8 has only delta y
  718. if (isNaN(deltaY)) {
  719. deltaY = -0.5*e.wheelDelta;
  720. }
  721. @com.vaadin.client.widgets.Escalator.JsniUtil::moveScrollFromEvent(*)(esc, deltaX, deltaY, e);
  722. });
  723. }-*/;
  724. /**
  725. * Recalculates the virtual viewport represented by the scrollbars, so
  726. * that the sizes of the scroll handles appear correct in the browser
  727. */
  728. public void recalculateScrollbarsForVirtualViewport() {
  729. double scrollContentHeight = body.calculateTotalRowHeight()
  730. + body.spacerContainer.getSpacerHeightsSum();
  731. double scrollContentWidth = columnConfiguration.calculateRowWidth();
  732. double tableWrapperHeight = heightOfEscalator;
  733. double tableWrapperWidth = widthOfEscalator;
  734. boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
  735. + WidgetUtil.PIXEL_EPSILON - header.getHeightOfSection()
  736. - footer.getHeightOfSection();
  737. boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth
  738. + WidgetUtil.PIXEL_EPSILON;
  739. // One dimension got scrollbars, but not the other. Recheck time!
  740. if (verticalScrollNeeded != horizontalScrollNeeded) {
  741. if (!verticalScrollNeeded && horizontalScrollNeeded) {
  742. verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
  743. + WidgetUtil.PIXEL_EPSILON
  744. - header.getHeightOfSection()
  745. - footer.getHeightOfSection()
  746. - horizontalScrollbar.getScrollbarThickness();
  747. } else {
  748. horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth
  749. + WidgetUtil.PIXEL_EPSILON
  750. - verticalScrollbar.getScrollbarThickness();
  751. }
  752. }
  753. // let's fix the table wrapper size, since it's now stable.
  754. if (verticalScrollNeeded) {
  755. tableWrapperWidth -= verticalScrollbar.getScrollbarThickness();
  756. tableWrapperWidth = Math.max(0, tableWrapperWidth);
  757. }
  758. if (horizontalScrollNeeded) {
  759. tableWrapperHeight -= horizontalScrollbar
  760. .getScrollbarThickness();
  761. tableWrapperHeight = Math.max(0, tableWrapperHeight);
  762. }
  763. tableWrapper.getStyle().setHeight(tableWrapperHeight, Unit.PX);
  764. tableWrapper.getStyle().setWidth(tableWrapperWidth, Unit.PX);
  765. double footerHeight = footer.getHeightOfSection();
  766. double headerHeight = header.getHeightOfSection();
  767. double vScrollbarHeight = Math.max(0,
  768. tableWrapperHeight - footerHeight - headerHeight);
  769. verticalScrollbar.setOffsetSizeAndScrollSize(vScrollbarHeight,
  770. scrollContentHeight);
  771. /*
  772. * If decreasing the amount of frozen columns, and scrolled to the
  773. * right, the scroll position might reset. So we need to remember
  774. * the scroll position, and re-apply it once the scrollbar size has
  775. * been adjusted.
  776. */
  777. double prevScrollPos = horizontalScrollbar.getScrollPos();
  778. double unfrozenPixels = columnConfiguration
  779. .getCalculatedColumnsWidth(Range.between(
  780. columnConfiguration.getFrozenColumnCount(),
  781. columnConfiguration.getColumnCount()));
  782. double frozenPixels = scrollContentWidth - unfrozenPixels;
  783. double hScrollOffsetWidth = tableWrapperWidth - frozenPixels;
  784. horizontalScrollbar.setOffsetSizeAndScrollSize(hScrollOffsetWidth,
  785. unfrozenPixels);
  786. horizontalScrollbar.getElement().getStyle().setLeft(frozenPixels,
  787. Unit.PX);
  788. horizontalScrollbar.setScrollPos(prevScrollPos);
  789. /*
  790. * only show the scrollbar wrapper if the scrollbar itself is
  791. * visible.
  792. */
  793. if (horizontalScrollbar.showsScrollHandle()) {
  794. horizontalScrollbarDeco.getStyle().clearDisplay();
  795. } else {
  796. horizontalScrollbarDeco.getStyle().setDisplay(Display.NONE);
  797. }
  798. /*
  799. * only show corner background divs if the vertical scrollbar is
  800. * visible.
  801. */
  802. Style hCornerStyle = headerDeco.getStyle();
  803. Style fCornerStyle = footerDeco.getStyle();
  804. if (verticalScrollbar.showsScrollHandle()) {
  805. hCornerStyle.clearDisplay();
  806. fCornerStyle.clearDisplay();
  807. if (horizontalScrollbar.showsScrollHandle()) {
  808. double offset = horizontalScrollbar.getScrollbarThickness();
  809. fCornerStyle.setBottom(offset, Unit.PX);
  810. } else {
  811. fCornerStyle.clearBottom();
  812. }
  813. } else {
  814. hCornerStyle.setDisplay(Display.NONE);
  815. fCornerStyle.setDisplay(Display.NONE);
  816. }
  817. }
  818. /**
  819. * Logical scrolling event handler for the entire widget.
  820. */
  821. public void onScroll() {
  822. final double scrollTop = verticalScrollbar.getScrollPos();
  823. final double scrollLeft = horizontalScrollbar.getScrollPos();
  824. if (lastScrollLeft != scrollLeft) {
  825. for (int i = 0; i < columnConfiguration.frozenColumns; i++) {
  826. header.updateFreezePosition(i, scrollLeft);
  827. body.updateFreezePosition(i, scrollLeft);
  828. footer.updateFreezePosition(i, scrollLeft);
  829. }
  830. position.set(headElem, -scrollLeft, 0);
  831. position.set(footElem, -scrollLeft, 0);
  832. lastScrollLeft = scrollLeft;
  833. }
  834. body.setBodyScrollPosition(scrollLeft, scrollTop);
  835. lastScrollTop = scrollTop;
  836. body.updateEscalatorRowsOnScroll();
  837. body.spacerContainer.updateSpacerDecosVisibility();
  838. /*
  839. * TODO [[optimize]]: Might avoid a reflow by first calculating new
  840. * scrolltop and scrolleft, then doing the escalator magic based on
  841. * those numbers and only updating the positions after that.
  842. */
  843. }
  844. public native void attachScrollListener(Element element)
  845. /*
  846. * Attaching events with JSNI instead of the GWT event mechanism because
  847. * GWT didn't provide enough details in events, or triggering the event
  848. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  849. * and skill, it could be done with better success. JavaScript overlay
  850. * types might work. This might also get rid of the JsniWorkaround
  851. * class.
  852. */
  853. /*-{
  854. if (element.addEventListener) {
  855. element.addEventListener("scroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
  856. } else {
  857. element.attachEvent("onscroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
  858. }
  859. }-*/;
  860. public native void detachScrollListener(Element element)
  861. /*
  862. * Detaching events with JSNI instead of the GWT event mechanism because
  863. * GWT didn't provide enough details in events, or triggering the event
  864. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  865. * and skill, it could be done with better success. JavaScript overlay
  866. * types might work. This might also get rid of the JsniWorkaround
  867. * class.
  868. */
  869. /*-{
  870. if (element.addEventListener) {
  871. element.removeEventListener("scroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
  872. } else {
  873. element.detachEvent("onscroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
  874. }
  875. }-*/;
  876. public native void attachMousewheelListener(Element element)
  877. /*
  878. * Attaching events with JSNI instead of the GWT event mechanism because
  879. * GWT didn't provide enough details in events, or triggering the event
  880. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  881. * and skill, it could be done with better success. JavaScript overlay
  882. * types might work. This might also get rid of the JsniWorkaround
  883. * class.
  884. */
  885. /*-{
  886. // firefox likes "wheel", while others use "mousewheel"
  887. var eventName = 'onmousewheel' in element ? 'mousewheel' : 'wheel';
  888. element.addEventListener(eventName, this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
  889. }-*/;
  890. public native void detachMousewheelListener(Element element)
  891. /*
  892. * Detaching events with JSNI instead of the GWT event mechanism because
  893. * GWT didn't provide enough details in events, or triggering the event
  894. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  895. * and skill, it could be done with better success. JavaScript overlay
  896. * types might work. This might also get rid of the JsniWorkaround
  897. * class.
  898. */
  899. /*-{
  900. // firefox likes "wheel", while others use "mousewheel"
  901. var eventName = element.onwheel===undefined?"mousewheel":"wheel";
  902. element.removeEventListener(eventName, this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
  903. }-*/;
  904. public native void attachTouchListeners(Element element)
  905. /*
  906. * Detaching events with JSNI instead of the GWT event mechanism because
  907. * GWT didn't provide enough details in events, or triggering the event
  908. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  909. * and skill, it could be done with better success. JavaScript overlay
  910. * types might work. This might also get rid of the JsniWorkaround
  911. * class.
  912. */
  913. /*-{
  914. element.addEventListener("touchstart", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
  915. element.addEventListener("touchmove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
  916. element.addEventListener("touchend", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  917. element.addEventListener("touchcancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  918. }-*/;
  919. public native void detachTouchListeners(Element element)
  920. /*
  921. * Detaching events with JSNI instead of the GWT event mechanism because
  922. * GWT didn't provide enough details in events, or triggering the event
  923. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  924. * and skill, it could be done with better success. JavaScript overlay
  925. * types might work. This might also get rid of the JsniWorkaround
  926. * class.
  927. */
  928. /*-{
  929. element.removeEventListener("touchstart", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
  930. element.removeEventListener("touchmove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
  931. element.removeEventListener("touchend", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  932. element.removeEventListener("touchcancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  933. }-*/;
  934. /**
  935. * Using pointerdown, pointermove, pointerup, and pointercancel for IE11
  936. * and Edge instead of touch* listeners (#18737)
  937. *
  938. * @param element
  939. */
  940. public native void attachPointerEventListeners(Element element)
  941. /*
  942. * Attaching events with JSNI instead of the GWT event mechanism because
  943. * GWT didn't provide enough details in events, or triggering the event
  944. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  945. * and skill, it could be done with better success. JavaScript overlay
  946. * types might work. This might also get rid of the JsniWorkaround
  947. * class.
  948. */
  949. /*-{
  950. element.addEventListener("pointerdown", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
  951. element.addEventListener("pointermove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
  952. element.addEventListener("pointerup", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  953. element.addEventListener("pointercancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  954. }-*/;
  955. /**
  956. * Using pointerdown, pointermove, pointerup, and pointercancel for IE11
  957. * and Edge instead of touch* listeners (#18737)
  958. *
  959. * @param element
  960. */
  961. public native void detachPointerEventListeners(Element element)
  962. /*
  963. * Detaching events with JSNI instead of the GWT event mechanism because
  964. * GWT didn't provide enough details in events, or triggering the event
  965. * handlers with GWT bindings was unsuccessful. Maybe, with more time
  966. * and skill, it could be done with better success. JavaScript overlay
  967. * types might work. This might also get rid of the JsniWorkaround
  968. * class.
  969. */
  970. /*-{
  971. element.removeEventListener("pointerdown", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
  972. element.removeEventListener("pointermove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
  973. element.removeEventListener("pointerup", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  974. element.removeEventListener("pointercancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
  975. }-*/;
  976. public void scrollToColumn(final int columnIndex,
  977. final ScrollDestination destination, final int padding) {
  978. assert columnIndex >= columnConfiguration.frozenColumns : "Can't scroll to a frozen column";
  979. /*
  980. * To cope with frozen columns, we just pretend those columns are
  981. * not there at all when calculating the position of the target
  982. * column and the boundaries of the viewport. The resulting
  983. * scrollLeft will be correct without compensation since the DOM
  984. * structure effectively means that scrollLeft also ignores the
  985. * frozen columns.
  986. */
  987. final double frozenPixels = columnConfiguration
  988. .getCalculatedColumnsWidth(Range.withLength(0,
  989. columnConfiguration.frozenColumns));
  990. final double targetStartPx = columnConfiguration
  991. .getCalculatedColumnsWidth(Range.withLength(0, columnIndex))
  992. - frozenPixels;
  993. final double targetEndPx = targetStartPx
  994. + columnConfiguration.getColumnWidthActual(columnIndex);
  995. final double viewportStartPx = getScrollLeft();
  996. double viewportEndPx = viewportStartPx
  997. + getBoundingWidth(getElement()) - frozenPixels;
  998. if (verticalScrollbar.showsScrollHandle()) {
  999. viewportEndPx -= WidgetUtil.getNativeScrollbarSize();
  1000. }
  1001. final double scrollLeft = getScrollPos(destination, targetStartPx,
  1002. targetEndPx, viewportStartPx, viewportEndPx, padding);
  1003. /*
  1004. * note that it doesn't matter if the scroll would go beyond the
  1005. * content, since the browser will adjust for that, and everything
  1006. * fall into line accordingly.
  1007. */
  1008. setScrollLeft(scrollLeft);
  1009. }
  1010. public void scrollToRow(final int rowIndex,
  1011. final ScrollDestination destination, final double padding) {
  1012. body.scrollToRowSpacerOrBoth(rowIndex, destination, padding,
  1013. ScrollType.ROW);
  1014. }
  1015. }
  1016. /**
  1017. * Helper class that helps to implement the WAI-ARIA functionality for the
  1018. * Grid and TreeGrid component.
  1019. * <p>
  1020. * The following WAI-ARIA attributes are added through this class:
  1021. *
  1022. * <ul>
  1023. * <li>aria-rowcount (since 8.2)</li>
  1024. * <li>roles provided by {@link AriaGridRole} (since 8.2)</li>
  1025. * </ul>
  1026. *
  1027. * @since 8.2
  1028. */
  1029. public class AriaGridHelper {
  1030. /**
  1031. * This field contains the total number of rows from the grid including
  1032. * rows from thead, tbody and tfoot.
  1033. *
  1034. * @since 8.2
  1035. */
  1036. private int allRows;
  1037. /**
  1038. * Adds the given numberOfRows to allRows and calls
  1039. * {@link #updateAriaRowCount()}.
  1040. *
  1041. * @param numberOfRows
  1042. * number of rows that were added to the grid
  1043. *
  1044. * @since 8.2
  1045. */
  1046. public void addRows(int numberOfRows) {
  1047. allRows += numberOfRows;
  1048. updateAriaRowCount();
  1049. }
  1050. /**
  1051. * Removes the given numberOfRows from allRows and calls
  1052. * {@link #updateAriaRowCount()}.
  1053. *
  1054. * @param numberOfRows
  1055. * number of rows that were removed from the grid
  1056. *
  1057. * @since 8.2
  1058. */
  1059. public void removeRows(int numberOfRows) {
  1060. allRows -= numberOfRows;
  1061. updateAriaRowCount();
  1062. }
  1063. /**
  1064. * Sets the aria-rowcount attribute with the current value of
  1065. * {@link AriaGridHelper#allRows} if the grid is attached and
  1066. * {@link AriaGridHelper#allRows} > 0.
  1067. *
  1068. * @since 8.2
  1069. */
  1070. public void updateAriaRowCount() {
  1071. if (!isAttached() || 0 > allRows) {
  1072. return;
  1073. }
  1074. getTable().setAttribute("aria-rowcount", String.valueOf(allRows));
  1075. }
  1076. /**
  1077. * Sets the {@code role} attribute to the given element.
  1078. *
  1079. * @param element
  1080. * element that should get the role attribute
  1081. * @param role
  1082. * role to be added
  1083. *
  1084. * @since 8.2
  1085. */
  1086. public void updateRole(final Element element, AriaGridRole role) {
  1087. element.setAttribute("role", role.getName());
  1088. }
  1089. }
  1090. /**
  1091. * Holds the currently used aria roles within the grid for rows and cells.
  1092. *
  1093. * @since 8.2
  1094. */
  1095. public enum AriaGridRole {
  1096. ROW("row"), ROWHEADER("rowheader"), ROWGROUP("rowgroup"), GRIDCELL(
  1097. "gridcell"), COLUMNHEADER("columnheader");
  1098. private final String name;
  1099. AriaGridRole(String name) {
  1100. this.name = name;
  1101. }
  1102. /**
  1103. * Return the name of the {@link AriaGridRole}.
  1104. *
  1105. * @return String name to be used as role attribute
  1106. */
  1107. public String getName() {
  1108. return name;
  1109. }
  1110. }
  1111. public abstract class AbstractRowContainer implements RowContainer {
  1112. private EscalatorUpdater updater = EscalatorUpdater.NULL;
  1113. private int rows;
  1114. /**
  1115. * The table section element ({@code <thead>}, {@code <tbody>} or
  1116. * {@code <tfoot>}) the rows (i.e. <code>&lt;tr&gt;</code> tags) are
  1117. * contained in.
  1118. */
  1119. protected final TableSectionElement root;
  1120. /**
  1121. * The primary style name of the escalator. Most commonly provided by
  1122. * Escalator as "v-escalator".
  1123. */
  1124. private String primaryStyleName = null;
  1125. private boolean defaultRowHeightShouldBeAutodetected = true;
  1126. private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT;
  1127. private boolean initialColumnSizesCalculated = false;
  1128. private boolean autodetectingRowHeightLater = false;
  1129. public AbstractRowContainer(
  1130. final TableSectionElement rowContainerElement) {
  1131. root = rowContainerElement;
  1132. ariaGridHelper.updateRole(root, AriaGridRole.ROWGROUP);
  1133. }
  1134. @Override
  1135. public TableSectionElement getElement() {
  1136. return root;
  1137. }
  1138. /**
  1139. * Gets the tag name of an element to represent a cell in a row.
  1140. * <p>
  1141. * Usually {@code "th"} or {@code "td"}.
  1142. * <p>
  1143. * <em>Note:</em> To actually <em>create</em> such an element, use
  1144. * {@link #createCellElement(double)} instead.
  1145. *
  1146. * @return the tag name for the element to represent cells as
  1147. * @see #createCellElement(double)
  1148. */
  1149. protected abstract String getCellElementTagName();
  1150. /**
  1151. * Gets the role attribute of an element to represent a cell in a row.
  1152. * <p>
  1153. * Usually {@link AriaGridRole#GRIDCELL} except for a cell in the
  1154. * header.
  1155. *
  1156. * @return the role attribute for the element to represent cells
  1157. *
  1158. * @since 8.2
  1159. */
  1160. protected AriaGridRole getCellElementRole() {
  1161. return AriaGridRole.GRIDCELL;
  1162. }
  1163. /**
  1164. * Gets the role attribute of an element to represent a row in a grid.
  1165. * <p>
  1166. * Usually {@link AriaGridRole#ROW} except for a row in the header.
  1167. *
  1168. * @return the role attribute for the element to represent rows
  1169. *
  1170. * @since 8.2
  1171. */
  1172. protected AriaGridRole getRowElementRole() {
  1173. return AriaGridRole.ROW;
  1174. }
  1175. @Override
  1176. public EscalatorUpdater getEscalatorUpdater() {
  1177. return updater;
  1178. }
  1179. /**
  1180. * {@inheritDoc}
  1181. * <p>
  1182. * <em>Implementation detail:</em> This method does no DOM modifications
  1183. * (i.e. is very cheap to call) if there is no data for rows or columns
  1184. * when this method is called.
  1185. *
  1186. * @see #hasColumnAndRowData()
  1187. */
  1188. @Override
  1189. public void setEscalatorUpdater(
  1190. final EscalatorUpdater escalatorUpdater) {
  1191. if (escalatorUpdater == null) {
  1192. throw new IllegalArgumentException(
  1193. "escalator updater cannot be null");
  1194. }
  1195. updater = escalatorUpdater;
  1196. if (hasColumnAndRowData() && getRowCount() > 0) {
  1197. refreshRows(0, getRowCount());
  1198. }
  1199. }
  1200. /**
  1201. * {@inheritDoc}
  1202. * <p>
  1203. * <em>Implementation detail:</em> This method does no DOM modifications
  1204. * (i.e. is very cheap to call) if there are no rows in the DOM when
  1205. * this method is called.
  1206. *
  1207. * @see #hasSomethingInDom()
  1208. */
  1209. @Override
  1210. public void removeRows(final int index, final int numberOfRows) {
  1211. assertArgumentsAreValidAndWithinRange(index, numberOfRows);
  1212. rows -= numberOfRows;
  1213. ariaGridHelper.removeRows(numberOfRows);
  1214. if (!isAttached()) {
  1215. return;
  1216. }
  1217. if (hasSomethingInDom()) {
  1218. paintRemoveRows(index, numberOfRows);
  1219. }
  1220. }
  1221. /**
  1222. * Removes those row elements from the DOM that correspond to the given
  1223. * range of logical indices. This may be fewer than {@code numberOfRows}
  1224. * , even zero, if not all the removed rows are actually visible.
  1225. * <p>
  1226. * The implementation must call
  1227. * {@link #paintRemoveRow(TableRowElement, int)} for each row that is
  1228. * removed from the DOM.
  1229. *
  1230. * @param index
  1231. * the logical index of the first removed row
  1232. * @param numberOfRows
  1233. * number of logical rows to remove
  1234. */
  1235. protected abstract void paintRemoveRows(final int index,
  1236. final int numberOfRows);
  1237. /**
  1238. * Removes a row element from the DOM, invoking
  1239. * {@link #getEscalatorUpdater()}
  1240. * {@link EscalatorUpdater#preDetach(Row, Iterable) preDetach} and
  1241. * {@link EscalatorUpdater#postDetach(Row, Iterable) postDetach} before
  1242. * and after removing the row, respectively.
  1243. * <p>
  1244. * This method must be called for each removed DOM row by any
  1245. * {@link #paintRemoveRows(int, int)} implementation.
  1246. *
  1247. * @param tr
  1248. * the row element to remove.
  1249. * @param logicalRowIndex
  1250. * logical index of the row that is to be removed
  1251. */
  1252. protected void paintRemoveRow(final TableRowElement tr,
  1253. final int logicalRowIndex) {
  1254. flyweightRow.setup(tr, logicalRowIndex,
  1255. columnConfiguration.getCalculatedColumnWidths());
  1256. getEscalatorUpdater().preDetach(flyweightRow,
  1257. flyweightRow.getCells());
  1258. tr.removeFromParent();
  1259. getEscalatorUpdater().postDetach(flyweightRow,
  1260. flyweightRow.getCells());
  1261. /*
  1262. * the "assert" guarantees that this code is run only during
  1263. * development/debugging.
  1264. */
  1265. assert flyweightRow.teardown();
  1266. }
  1267. protected void assertArgumentsAreValidAndWithinRange(final int index,
  1268. final int numberOfRows)
  1269. throws IllegalArgumentException, IndexOutOfBoundsException {
  1270. if (numberOfRows < 1) {
  1271. throw new IllegalArgumentException(
  1272. "Number of rows must be 1 or greater (was "
  1273. + numberOfRows + ")");
  1274. }
  1275. if (index < 0 || index + numberOfRows > getRowCount()) {
  1276. throw new IndexOutOfBoundsException("The given " + "row range ("
  1277. + index + ".." + (index + numberOfRows)
  1278. + ") was outside of the current number of rows ("
  1279. + getRowCount() + ")");
  1280. }
  1281. }
  1282. @Override
  1283. public int getRowCount() {
  1284. return rows;
  1285. }
  1286. /**
  1287. * This method calculates the current row count directly from the DOM.
  1288. * <p>
  1289. * While Escalator is stable, this value should equal to
  1290. * {@link #getRowCount()}, but while row counts are being updated, these
  1291. * two values might differ for a short while.
  1292. * <p>
  1293. * Any extra content, such as spacers for the body, should not be
  1294. * included in this count.
  1295. *
  1296. * @since 7.5.0
  1297. *
  1298. * @return the actual DOM count of rows
  1299. */
  1300. public abstract int getDomRowCount();
  1301. /**
  1302. * {@inheritDoc}
  1303. * <p>
  1304. * <em>Implementation detail:</em> This method does no DOM modifications
  1305. * (i.e. is very cheap to call) if there is no data for columns when
  1306. * this method is called.
  1307. *
  1308. * @see #hasColumnAndRowData()
  1309. */
  1310. @Override
  1311. public void insertRows(final int index, final int numberOfRows) {
  1312. if (index < 0 || index > getRowCount()) {
  1313. throw new IndexOutOfBoundsException("The given index (" + index
  1314. + ") was outside of the current number of rows (0.."
  1315. + getRowCount() + ")");
  1316. }
  1317. if (numberOfRows < 1) {
  1318. throw new IllegalArgumentException(
  1319. "Number of rows must be 1 or greater (was "
  1320. + numberOfRows + ")");
  1321. }
  1322. rows += numberOfRows;
  1323. ariaGridHelper.addRows(numberOfRows);
  1324. /*
  1325. * only add items in the DOM if the widget itself is attached to the
  1326. * DOM. We can't calculate sizes otherwise.
  1327. */
  1328. if (isAttached()) {
  1329. paintInsertRows(index, numberOfRows);
  1330. /*
  1331. * We are inserting the first rows in this container. We
  1332. * potentially need to set the widths for the cells for the
  1333. * first time.
  1334. */
  1335. if (rows == numberOfRows) {
  1336. Scheduler.get().scheduleFinally(() -> {
  1337. if (initialColumnSizesCalculated) {
  1338. return;
  1339. }
  1340. initialColumnSizesCalculated = true;
  1341. Map<Integer, Double> colWidths = new HashMap<>();
  1342. for (int i = 0; i < getColumnConfiguration()
  1343. .getColumnCount(); i++) {
  1344. Double width = Double.valueOf(
  1345. getColumnConfiguration().getColumnWidth(i));
  1346. Integer col = Integer.valueOf(i);
  1347. colWidths.put(col, width);
  1348. }
  1349. getColumnConfiguration().setColumnWidths(colWidths,
  1350. true);
  1351. });
  1352. }
  1353. }
  1354. }
  1355. /**
  1356. * Actually add rows into the DOM, now that everything can be
  1357. * calculated.
  1358. *
  1359. * @param visualIndex
  1360. * the DOM index to add rows into
  1361. * @param numberOfRows
  1362. * the number of rows to insert
  1363. */
  1364. protected abstract void paintInsertRows(final int visualIndex,
  1365. final int numberOfRows);
  1366. protected List<TableRowElement> paintInsertStaticRows(
  1367. final int visualIndex, final int numberOfRows) {
  1368. assert isAttached() : "Can't paint rows if Escalator is not attached";
  1369. final List<TableRowElement> addedRows = new ArrayList<>();
  1370. if (numberOfRows < 1) {
  1371. return addedRows;
  1372. }
  1373. Node referenceRow;
  1374. if (root.getChildCount() != 0 && visualIndex != 0) {
  1375. // get the row node we're inserting stuff after
  1376. referenceRow = root.getChild(visualIndex - 1);
  1377. } else {
  1378. // index is 0, so just prepend.
  1379. referenceRow = null;
  1380. }
  1381. for (int row = visualIndex; row < visualIndex
  1382. + numberOfRows; row++) {
  1383. final TableRowElement tr = TableRowElement.as(DOM.createTR());
  1384. addedRows.add(tr);
  1385. tr.addClassName(getStylePrimaryName() + "-row");
  1386. ariaGridHelper.updateRole(tr, getRowElementRole());
  1387. for (int col = 0; col < columnConfiguration
  1388. .getColumnCount(); col++) {
  1389. final double colWidth = columnConfiguration
  1390. .getColumnWidthActual(col);
  1391. final TableCellElement cellElem = createCellElement(
  1392. colWidth);
  1393. tr.appendChild(cellElem);
  1394. // Set stylename and position if new cell is frozen
  1395. if (col < columnConfiguration.frozenColumns) {
  1396. cellElem.addClassName("frozen");
  1397. position.set(cellElem, scroller.lastScrollLeft, 0);
  1398. }
  1399. if (columnConfiguration.frozenColumns > 0
  1400. && col == columnConfiguration.frozenColumns - 1) {
  1401. cellElem.addClassName("last-frozen");
  1402. }
  1403. }
  1404. referenceRow = paintInsertRow(referenceRow, tr, row);
  1405. }
  1406. reapplyRowWidths();
  1407. recalculateSectionHeight();
  1408. return addedRows;
  1409. }
  1410. /**
  1411. * Inserts a single row into the DOM, invoking
  1412. * {@link #getEscalatorUpdater()}
  1413. * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and
  1414. * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before
  1415. * and after inserting the row, respectively. The row should have its
  1416. * cells already inserted.
  1417. *
  1418. * @param referenceRow
  1419. * the row after which to insert or null if insert as first
  1420. * @param tr
  1421. * the row to be inserted
  1422. * @param logicalRowIndex
  1423. * the logical index of the inserted row
  1424. * @return the inserted row to be used as the new reference
  1425. */
  1426. protected Node paintInsertRow(Node referenceRow,
  1427. final TableRowElement tr, int logicalRowIndex) {
  1428. flyweightRow.setup(tr, logicalRowIndex,
  1429. columnConfiguration.getCalculatedColumnWidths());
  1430. getEscalatorUpdater().preAttach(flyweightRow,
  1431. flyweightRow.getCells());
  1432. referenceRow = insertAfterReferenceAndUpdateIt(root, tr,
  1433. referenceRow);
  1434. getEscalatorUpdater().postAttach(flyweightRow,
  1435. flyweightRow.getCells());
  1436. updater.update(flyweightRow, flyweightRow.getCells());
  1437. /*
  1438. * the "assert" guarantees that this code is run only during
  1439. * development/debugging.
  1440. */
  1441. assert flyweightRow.teardown();
  1442. return referenceRow;
  1443. }
  1444. private Node insertAfterReferenceAndUpdateIt(final Element parent,
  1445. final Element elem, final Node referenceNode) {
  1446. if (referenceNode != null) {
  1447. parent.insertAfter(elem, referenceNode);
  1448. } else {
  1449. /*
  1450. * referencenode being null means we have offset 0, i.e. make it
  1451. * the first row
  1452. */
  1453. /*
  1454. * TODO [[optimize]]: Is insertFirst or append faster for an
  1455. * empty root?
  1456. */
  1457. parent.insertFirst(elem);
  1458. }
  1459. return elem;
  1460. }
  1461. protected abstract void recalculateSectionHeight();
  1462. /**
  1463. * Returns the height of all rows in the row container.
  1464. */
  1465. protected double calculateTotalRowHeight() {
  1466. return getDefaultRowHeight() * getRowCount();
  1467. }
  1468. /**
  1469. * {@inheritDoc}
  1470. * <p>
  1471. * <em>Implementation detail:</em> This method does no DOM modifications
  1472. * (i.e. is very cheap to call) if there is no data for columns when
  1473. * this method is called.
  1474. *
  1475. * @see #hasColumnAndRowData()
  1476. */
  1477. @Override
  1478. public void refreshRows(final int index, final int numberOfRows) {
  1479. Range rowRange = Range.withLength(index, numberOfRows);
  1480. Range colRange = Range.withLength(0,
  1481. getColumnConfiguration().getColumnCount());
  1482. refreshCells(rowRange, colRange);
  1483. }
  1484. protected abstract void refreshCells(Range logicalRowRange,
  1485. Range colRange);
  1486. void refreshRow(TableRowElement tr, int logicalRowIndex) {
  1487. refreshRow(tr, logicalRowIndex, Range.withLength(0,
  1488. getColumnConfiguration().getColumnCount()));
  1489. }
  1490. void refreshRow(final TableRowElement tr, final int logicalRowIndex,
  1491. Range colRange) {
  1492. flyweightRow.setup(tr, logicalRowIndex,
  1493. columnConfiguration.getCalculatedColumnWidths());
  1494. Iterable<FlyweightCell> cellsToUpdate = flyweightRow
  1495. .getCells(colRange.getStart(), colRange.length());
  1496. updater.update(flyweightRow, cellsToUpdate);
  1497. /*
  1498. * the "assert" guarantees that this code is run only during
  1499. * development/debugging.
  1500. */
  1501. assert flyweightRow.teardown();
  1502. }
  1503. /**
  1504. * Create and setup an empty cell element.
  1505. *
  1506. * @param width
  1507. * the width of the cell, in pixels
  1508. *
  1509. * @return a set-up empty cell element
  1510. */
  1511. public TableCellElement createCellElement(final double width) {
  1512. final TableCellElement cellElem = TableCellElement
  1513. .as(DOM.createElement(getCellElementTagName()));
  1514. final double height = getDefaultRowHeight();
  1515. assert height >= 0 : "defaultRowHeight was negative. There's a setter leak somewhere.";
  1516. cellElem.getStyle().setHeight(height, Unit.PX);
  1517. if (width >= 0) {
  1518. cellElem.getStyle().setWidth(width, Unit.PX);
  1519. }
  1520. cellElem.addClassName(getStylePrimaryName() + "-cell");
  1521. ariaGridHelper.updateRole(cellElem, getCellElementRole());
  1522. return cellElem;
  1523. }
  1524. @Override
  1525. public TableRowElement getRowElement(int index) {
  1526. return getTrByVisualIndex(index);
  1527. }
  1528. /**
  1529. * Gets the child element that is visually at a certain index.
  1530. *
  1531. * @param index
  1532. * the index of the element to retrieve
  1533. * @return the element at position {@code index}
  1534. * @throws IndexOutOfBoundsException
  1535. * if {@code index} is not valid within {@link #root}
  1536. */
  1537. protected abstract TableRowElement getTrByVisualIndex(int index)
  1538. throws IndexOutOfBoundsException;
  1539. protected void paintRemoveColumns(final int offset,
  1540. final int numberOfColumns) {
  1541. for (int i = 0; i < getDomRowCount(); i++) {
  1542. TableRowElement row = getTrByVisualIndex(i);
  1543. flyweightRow.setup(row, i,
  1544. columnConfiguration.getCalculatedColumnWidths());
  1545. Iterable<FlyweightCell> attachedCells = flyweightRow
  1546. .getCells(offset, numberOfColumns);
  1547. getEscalatorUpdater().preDetach(flyweightRow, attachedCells);
  1548. for (int j = 0; j < numberOfColumns; j++) {
  1549. row.getCells().getItem(offset).removeFromParent();
  1550. }
  1551. Iterable<FlyweightCell> detachedCells = flyweightRow
  1552. .getUnattachedCells(offset, numberOfColumns);
  1553. getEscalatorUpdater().postDetach(flyweightRow, detachedCells);
  1554. assert flyweightRow.teardown();
  1555. }
  1556. }
  1557. protected void paintInsertColumns(final int offset,
  1558. final int numberOfColumns, boolean frozen) {
  1559. for (int row = 0; row < getDomRowCount(); row++) {
  1560. final TableRowElement tr = getTrByVisualIndex(row);
  1561. int logicalRowIndex = getLogicalRowIndex(tr);
  1562. paintInsertCells(tr, logicalRowIndex, offset, numberOfColumns);
  1563. }
  1564. reapplyRowWidths();
  1565. if (frozen) {
  1566. for (int col = offset; col < offset + numberOfColumns; col++) {
  1567. setColumnFrozen(col, true);
  1568. }
  1569. }
  1570. }
  1571. /**
  1572. * Inserts new cell elements into a single row element, invoking
  1573. * {@link #getEscalatorUpdater()}
  1574. * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and
  1575. * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before
  1576. * and after inserting the cells, respectively.
  1577. * <p>
  1578. * Precondition: The row must be already attached to the DOM and the
  1579. * FlyweightCell instances corresponding to the new columns added to
  1580. * {@code flyweightRow}.
  1581. *
  1582. * @param tr
  1583. * the row in which to insert the cells
  1584. * @param logicalRowIndex
  1585. * the index of the row
  1586. * @param offset
  1587. * the index of the first cell
  1588. * @param numberOfCells
  1589. * the number of cells to insert
  1590. */
  1591. private void paintInsertCells(final TableRowElement tr,
  1592. int logicalRowIndex, final int offset,
  1593. final int numberOfCells) {
  1594. assert root.isOrHasChild(
  1595. tr) : "The row must be attached to the document";
  1596. flyweightRow.setup(tr, logicalRowIndex,
  1597. columnConfiguration.getCalculatedColumnWidths());
  1598. Iterable<FlyweightCell> cells = flyweightRow
  1599. .getUnattachedCells(offset, numberOfCells);
  1600. for (FlyweightCell cell : cells) {
  1601. final double colWidth = columnConfiguration
  1602. .getColumnWidthActual(cell.getColumn());
  1603. final TableCellElement cellElem = createCellElement(colWidth);
  1604. cell.setElement(cellElem);
  1605. }
  1606. getEscalatorUpdater().preAttach(flyweightRow, cells);
  1607. Node referenceCell;
  1608. if (offset != 0) {
  1609. referenceCell = tr.getChild(offset - 1);
  1610. } else {
  1611. referenceCell = null;
  1612. }
  1613. for (FlyweightCell cell : cells) {
  1614. referenceCell = insertAfterReferenceAndUpdateIt(tr,
  1615. cell.getElement(), referenceCell);
  1616. }
  1617. getEscalatorUpdater().postAttach(flyweightRow, cells);
  1618. getEscalatorUpdater().update(flyweightRow, cells);
  1619. assert flyweightRow.teardown();
  1620. }
  1621. public void setColumnFrozen(int column, boolean frozen) {
  1622. toggleFrozenColumnClass(column, frozen, "frozen");
  1623. if (frozen) {
  1624. updateFreezePosition(column, scroller.lastScrollLeft);
  1625. }
  1626. }
  1627. private void toggleFrozenColumnClass(int column, boolean frozen,
  1628. String className) {
  1629. final NodeList<TableRowElement> childRows = root.getRows();
  1630. for (int row = 0; row < childRows.getLength(); row++) {
  1631. final TableRowElement tr = childRows.getItem(row);
  1632. if (!rowCanBeFrozen(tr)) {
  1633. continue;
  1634. }
  1635. TableCellElement cell = tr.getCells().getItem(column);
  1636. if (frozen) {
  1637. cell.addClassName(className);
  1638. } else {
  1639. cell.removeClassName(className);
  1640. position.reset(cell);
  1641. }
  1642. }
  1643. }
  1644. public void setColumnLastFrozen(int column, boolean lastFrozen) {
  1645. toggleFrozenColumnClass(column, lastFrozen, "last-frozen");
  1646. }
  1647. public void updateFreezePosition(int column, double scrollLeft) {
  1648. final NodeList<TableRowElement> childRows = root.getRows();
  1649. for (int row = 0; row < childRows.getLength(); row++) {
  1650. final TableRowElement tr = childRows.getItem(row);
  1651. if (rowCanBeFrozen(tr)) {
  1652. TableCellElement cell = tr.getCells().getItem(column);
  1653. position.set(cell, scrollLeft, 0);
  1654. }
  1655. }
  1656. }
  1657. /**
  1658. * Checks whether a row is an element, or contains such elements, that
  1659. * can be frozen.
  1660. * <p>
  1661. * In practice, this applies for all header and footer rows. For body
  1662. * rows, it applies for all rows except spacer rows.
  1663. *
  1664. * @since 7.5.0
  1665. *
  1666. * @param tr
  1667. * the row element to check whether it, or any of its its
  1668. * descendants can be frozen
  1669. * @return <code>true</code> if the given element, or any of its
  1670. * descendants, can be frozen
  1671. */
  1672. protected abstract boolean rowCanBeFrozen(TableRowElement tr);
  1673. /**
  1674. * Iterates through all the cells in a column and returns the width of
  1675. * the widest element in this RowContainer.
  1676. *
  1677. * @param index
  1678. * the index of the column to inspect
  1679. * @return the pixel width of the widest element in the indicated column
  1680. */
  1681. public double calculateMaxColWidth(int index) {
  1682. TableRowElement row = TableRowElement
  1683. .as(root.getFirstChildElement());
  1684. double maxWidth = 0;
  1685. while (row != null) {
  1686. final TableCellElement cell = row.getCells().getItem(index);
  1687. final boolean isVisible = !cell.getStyle().getDisplay()
  1688. .equals(Display.NONE.getCssName());
  1689. if (isVisible) {
  1690. maxWidth = Math.max(maxWidth, getBoundingWidth(cell));
  1691. }
  1692. row = TableRowElement.as(row.getNextSiblingElement());
  1693. }
  1694. return maxWidth;
  1695. }
  1696. /**
  1697. * Reapplies all the cells' widths according to the calculated widths in
  1698. * the column configuration.
  1699. */
  1700. public void reapplyColumnWidths() {
  1701. Element row = root.getFirstChildElement();
  1702. while (row != null) {
  1703. // Only handle non-spacer rows
  1704. if (!body.spacerContainer.isSpacer(row)) {
  1705. Element cell = row.getFirstChildElement();
  1706. int columnIndex = 0;
  1707. while (cell != null) {
  1708. final double width = getCalculatedColumnWidthWithColspan(
  1709. cell, columnIndex);
  1710. /*
  1711. * TODO Should Escalator implement ProvidesResize at
  1712. * some point, this is where we need to do that.
  1713. */
  1714. cell.getStyle().setWidth(width, Unit.PX);
  1715. cell = cell.getNextSiblingElement();
  1716. columnIndex++;
  1717. }
  1718. }
  1719. row = row.getNextSiblingElement();
  1720. }
  1721. reapplyRowWidths();
  1722. }
  1723. private double getCalculatedColumnWidthWithColspan(final Element cell,
  1724. final int columnIndex) {
  1725. final int colspan = cell.getPropertyInt(FlyweightCell.COLSPAN_ATTR);
  1726. Range spannedColumns = Range.withLength(columnIndex, colspan);
  1727. /*
  1728. * Since browsers don't explode with overflowing colspans, escalator
  1729. * shouldn't either.
  1730. */
  1731. if (spannedColumns.getEnd() > columnConfiguration
  1732. .getColumnCount()) {
  1733. spannedColumns = Range.between(columnIndex,
  1734. columnConfiguration.getColumnCount());
  1735. }
  1736. return columnConfiguration
  1737. .getCalculatedColumnsWidth(spannedColumns);
  1738. }
  1739. /**
  1740. * Applies the total length of the columns to each row element.
  1741. * <p>
  1742. * <em>Note:</em> In contrast to {@link #reapplyColumnWidths()}, this
  1743. * method only modifies the width of the {@code
  1744. *
  1745. <tr>
  1746. * } element, not the cells within.
  1747. */
  1748. protected void reapplyRowWidths() {
  1749. double rowWidth = columnConfiguration.calculateRowWidth();
  1750. if (rowWidth < 0) {
  1751. return;
  1752. }
  1753. Element row = root.getFirstChildElement();
  1754. while (row != null) {
  1755. // IF there is a rounding error when summing the columns, we
  1756. // need to round the tr width up to ensure that columns fit and
  1757. // do not wrap
  1758. // E.g.122.95+123.25+103.75+209.25+83.52+88.57+263.45+131.21+126.85+113.13=1365.9299999999998
  1759. // For this we must set 1365.93 or the last column will wrap
  1760. row.getStyle().setWidth(WidgetUtil.roundSizeUp(rowWidth),
  1761. Unit.PX);
  1762. row = row.getNextSiblingElement();
  1763. }
  1764. }
  1765. /**
  1766. * The primary style name for the container.
  1767. *
  1768. * @param primaryStyleName
  1769. * the style name to use as prefix for all row and cell style
  1770. * names.
  1771. */
  1772. protected void setStylePrimaryName(String primaryStyleName) {
  1773. String oldStyle = getStylePrimaryName();
  1774. if (SharedUtil.equals(oldStyle, primaryStyleName)) {
  1775. return;
  1776. }
  1777. this.primaryStyleName = primaryStyleName;
  1778. // Update already rendered rows and cells
  1779. Element row = root.getRows().getItem(0);
  1780. while (row != null) {
  1781. UIObject.setStylePrimaryName(row, primaryStyleName + "-row");
  1782. Element cell = TableRowElement.as(row).getCells().getItem(0);
  1783. while (cell != null) {
  1784. assert TableCellElement.is(cell);
  1785. UIObject.setStylePrimaryName(cell,
  1786. primaryStyleName + "-cell");
  1787. cell = cell.getNextSiblingElement();
  1788. }
  1789. row = row.getNextSiblingElement();
  1790. }
  1791. }
  1792. /**
  1793. * Returns the primary style name of the container.
  1794. *
  1795. * @return The primary style name or <code>null</code> if not set.
  1796. */
  1797. protected String getStylePrimaryName() {
  1798. return primaryStyleName;
  1799. }
  1800. @Override
  1801. public void setDefaultRowHeight(double px)
  1802. throws IllegalArgumentException {
  1803. if (px < 1) {
  1804. throw new IllegalArgumentException(
  1805. "Height must be positive. " + px + " was given.");
  1806. }
  1807. defaultRowHeightShouldBeAutodetected = false;
  1808. defaultRowHeight = px;
  1809. reapplyDefaultRowHeights();
  1810. applyHeightByRows();
  1811. }
  1812. @Override
  1813. public double getDefaultRowHeight() {
  1814. return defaultRowHeight;
  1815. }
  1816. /**
  1817. * The default height of rows has (most probably) changed.
  1818. * <p>
  1819. * Make sure that the displayed rows with a default height are updated
  1820. * in height and top position.
  1821. * <p>
  1822. * <em>Note:</em>This implementation should not call
  1823. * {@link Escalator#recalculateElementSizes()} - it is done by the
  1824. * discretion of the caller of this method.
  1825. */
  1826. protected abstract void reapplyDefaultRowHeights();
  1827. protected void reapplyRowHeight(final TableRowElement tr,
  1828. final double heightPx) {
  1829. assert heightPx >= 0 : "Height must not be negative";
  1830. Element cellElem = tr.getFirstChildElement();
  1831. while (cellElem != null) {
  1832. cellElem.getStyle().setHeight(heightPx, Unit.PX);
  1833. cellElem = cellElem.getNextSiblingElement();
  1834. }
  1835. /*
  1836. * no need to apply height to tr-element, it'll be resized
  1837. * implicitly.
  1838. */
  1839. }
  1840. protected void setRowPosition(final TableRowElement tr, final int x,
  1841. final double y) {
  1842. positions.set(tr, x, y);
  1843. }
  1844. /**
  1845. * Returns <em>the assigned</em> top position for the given element.
  1846. * <p>
  1847. * <em>Note:</em> This method does not calculate what a row's top
  1848. * position should be. It just returns an assigned value, correct or
  1849. * not.
  1850. *
  1851. * @param tr
  1852. * the table row element to measure
  1853. * @return the current top position for {@code tr}
  1854. * @see BodyRowContainerImpl#getRowTop(int)
  1855. */
  1856. protected double getRowTop(final TableRowElement tr) {
  1857. return positions.getTop(tr);
  1858. }
  1859. protected void removeRowPosition(TableRowElement tr) {
  1860. positions.remove(tr);
  1861. }
  1862. /**
  1863. * Triggers delayed auto-detection of default row height if it hasn't
  1864. * been set by that point and the Escalator is both attached and
  1865. * displayed.
  1866. */
  1867. public void autodetectRowHeightLater() {
  1868. autodetectingRowHeightLater = true;
  1869. Scheduler.get().scheduleFinally(() -> {
  1870. if (defaultRowHeightShouldBeAutodetected && isAttached()
  1871. && WidgetUtil.isDisplayed(getElement())) {
  1872. autodetectRowHeightNow();
  1873. defaultRowHeightShouldBeAutodetected = false;
  1874. }
  1875. autodetectingRowHeightLater = false;
  1876. });
  1877. }
  1878. @Override
  1879. public boolean isAutodetectingRowHeightLater() {
  1880. return autodetectingRowHeightLater;
  1881. }
  1882. private void fireRowHeightChangedEventFinally() {
  1883. if (!rowHeightChangedEventFired) {
  1884. rowHeightChangedEventFired = true;
  1885. Scheduler.get().scheduleFinally(() -> {
  1886. fireEvent(new RowHeightChangedEvent());
  1887. rowHeightChangedEventFired = false;
  1888. });
  1889. }
  1890. }
  1891. /**
  1892. * Auto-detect row height immediately, if possible. If Escalator isn't
  1893. * attached and displayed yet, auto-detecting cannot be performed
  1894. * correctly. In such cases auto-detecting is left to wait for these
  1895. * conditions to change, and will be performed when they do.
  1896. */
  1897. public void autodetectRowHeightNow() {
  1898. if (!isAttached() || !WidgetUtil.isDisplayed(getElement())) {
  1899. // Run again when attached and displayed
  1900. defaultRowHeightShouldBeAutodetected = true;
  1901. return;
  1902. }
  1903. final double oldRowHeight = defaultRowHeight;
  1904. final Element detectionTr = DOM.createTR();
  1905. detectionTr.setClassName(getStylePrimaryName() + "-row");
  1906. final Element cellElem = DOM.createElement(getCellElementTagName());
  1907. cellElem.setClassName(getStylePrimaryName() + "-cell");
  1908. cellElem.setInnerText("Ij");
  1909. detectionTr.appendChild(cellElem);
  1910. root.appendChild(detectionTr);
  1911. double boundingHeight = getBoundingHeight(cellElem);
  1912. defaultRowHeight = Math.max(1.0d, boundingHeight);
  1913. root.removeChild(detectionTr);
  1914. if (root.hasChildNodes()) {
  1915. reapplyDefaultRowHeights();
  1916. applyHeightByRows();
  1917. }
  1918. if (oldRowHeight != defaultRowHeight) {
  1919. fireRowHeightChangedEventFinally();
  1920. }
  1921. }
  1922. @Override
  1923. public Cell getCell(final Element element) {
  1924. if (element == null) {
  1925. throw new IllegalArgumentException("Element cannot be null");
  1926. }
  1927. /*
  1928. * Ensure that element is not root nor the direct descendant of root
  1929. * (a row or spacer) and ensure the element is inside the dom
  1930. * hierarchy of the root element. If not, return null.
  1931. */
  1932. if (root == element || element.getParentElement() == root
  1933. || !root.isOrHasChild(element)) {
  1934. return null;
  1935. }
  1936. /*
  1937. * Ensure element is the cell element by iterating up the DOM
  1938. * hierarchy until reaching cell element.
  1939. */
  1940. Element cellElementCandidate = element;
  1941. while (cellElementCandidate.getParentElement()
  1942. .getParentElement() != root) {
  1943. cellElementCandidate = cellElementCandidate.getParentElement();
  1944. }
  1945. final TableCellElement cellElement = TableCellElement
  1946. .as(cellElementCandidate);
  1947. // Find dom column
  1948. int domColumnIndex = -1;
  1949. for (Element e = cellElement; e != null; e = e
  1950. .getPreviousSiblingElement()) {
  1951. domColumnIndex++;
  1952. }
  1953. // Find dom row
  1954. int domRowIndex = -1;
  1955. for (Element e = cellElement.getParentElement(); e != null; e = e
  1956. .getPreviousSiblingElement()) {
  1957. domRowIndex++;
  1958. }
  1959. return new Cell(domRowIndex, domColumnIndex, cellElement);
  1960. }
  1961. double measureCellWidth(TableCellElement cell, boolean withContent) {
  1962. /*
  1963. * To get the actual width of the contents, we need to get the cell
  1964. * content without any hardcoded height or width.
  1965. *
  1966. * But we don't want to modify the existing column, because that
  1967. * might trigger some unnecessary listeners and whatnot. So,
  1968. * instead, we make a deep clone of that cell, but without any
  1969. * explicit dimensions, and measure that instead.
  1970. */
  1971. TableCellElement cellClone = TableCellElement
  1972. .as((Element) cell.cloneNode(withContent));
  1973. if (!withContent || columnConfiguration
  1974. .getColumnWidth(cell.getCellIndex()) < 0) {
  1975. clearRelativeWidthContents(cellClone);
  1976. }
  1977. cellClone.getStyle().clearHeight();
  1978. cellClone.getStyle().clearWidth();
  1979. cell.getParentElement().insertBefore(cellClone, cell);
  1980. double requiredWidth = getBoundingWidth(cellClone);
  1981. if (requiredWidth > 0) {
  1982. // add one pixel to avoid subpixel issues
  1983. // (overflow, unnecessary ellipsis...)
  1984. requiredWidth += 1;
  1985. // round up to a fraction that the current browser can handle
  1986. requiredWidth = WidgetUtil.roundSizeUp(requiredWidth);
  1987. }
  1988. cellClone.removeFromParent();
  1989. return requiredWidth;
  1990. }
  1991. /**
  1992. * Contents of an element that is configured to have relative width
  1993. * shouldn't be taken into consideration when measuring minimum widths.
  1994. * Thus any such contents within the element hierarchy need to be
  1995. * cleared out for accurate results. The element itself should remain,
  1996. * however, in case it has styles that affect the end results.
  1997. *
  1998. * @param elem
  1999. * an element that might have unnecessary content that
  2000. * interferes with minimum width calculations
  2001. */
  2002. private void clearRelativeWidthContents(Element elem) {
  2003. try {
  2004. String width = elem.getStyle().getWidth();
  2005. if (width != null && width.endsWith("%")) {
  2006. if (elem.hasChildNodes()) {
  2007. elem.removeAllChildren();
  2008. // add a fake child so that :empty behavior doesn't
  2009. // change
  2010. elem.setInnerHTML("<a/>");
  2011. } else {
  2012. elem.setInnerHTML(null);
  2013. }
  2014. }
  2015. } catch (JavaScriptException e) {
  2016. // no width set, move on
  2017. }
  2018. for (int i = 0; i < elem.getChildCount(); ++i) {
  2019. Node node = elem.getChild(i);
  2020. if (node instanceof Element) {
  2021. clearRelativeWidthContents((Element) node);
  2022. }
  2023. }
  2024. }
  2025. /**
  2026. * Gets the minimum width needed to display the cell properly.
  2027. *
  2028. * @param colIndex
  2029. * index of column to measure
  2030. * @param withContent
  2031. * <code>true</code> if content is taken into account,
  2032. * <code>false</code> if not
  2033. * @return cell width needed for displaying correctly
  2034. */
  2035. double measureMinCellWidth(int colIndex, boolean withContent) {
  2036. assert isAttached() : "Can't measure max width of cell, since Escalator is not attached to the DOM.";
  2037. double minCellWidth = -1;
  2038. NodeList<TableRowElement> rows = root.getRows();
  2039. for (int row = 0; row < rows.getLength(); row++) {
  2040. TableCellElement cell = rows.getItem(row).getCells()
  2041. .getItem(colIndex);
  2042. if (cell != null && !cellIsPartOfSpan(cell)) {
  2043. double cellWidth = measureCellWidth(cell, withContent);
  2044. minCellWidth = Math.max(minCellWidth, cellWidth);
  2045. }
  2046. }
  2047. return minCellWidth;
  2048. }
  2049. private boolean cellIsPartOfSpan(TableCellElement cell) {
  2050. boolean cellHasColspan = cell.getColSpan() > 1;
  2051. boolean cellIsHidden = Display.NONE.getCssName()
  2052. .equals(cell.getStyle().getDisplay());
  2053. return cellHasColspan || cellIsHidden;
  2054. }
  2055. void refreshColumns(int index, int numberOfColumns) {
  2056. if (getRowCount() > 0) {
  2057. Range rowRange = Range.withLength(0, getRowCount());
  2058. Range colRange = Range.withLength(index, numberOfColumns);
  2059. refreshCells(rowRange, colRange);
  2060. }
  2061. }
  2062. /**
  2063. * The height of this table section.
  2064. * <p>
  2065. * Note that {@link Escalator#getBody() the body} will calculate its
  2066. * height, while the others will return a precomputed value.
  2067. *
  2068. * @since 7.5.0
  2069. *
  2070. * @return the height of this table section
  2071. */
  2072. protected abstract double getHeightOfSection();
  2073. /**
  2074. * Gets the logical row index for the given table row element.
  2075. *
  2076. * @param tr
  2077. * the table row element inside this container.
  2078. * @return the logical index of the given element
  2079. */
  2080. public int getLogicalRowIndex(final TableRowElement tr) {
  2081. // Note: BodyRowContainerImpl overrides this behaviour, since the
  2082. // physical index and logical index don't match there. For header
  2083. // and footer there is a match.
  2084. return tr.getSectionRowIndex();
  2085. };
  2086. }
  2087. private abstract class AbstractStaticRowContainer
  2088. extends AbstractRowContainer {
  2089. /** The height of the combined rows in the DOM. Never negative. */
  2090. private double heightOfSection = 0;
  2091. public AbstractStaticRowContainer(
  2092. final TableSectionElement headElement) {
  2093. super(headElement);
  2094. }
  2095. @Override
  2096. public int getDomRowCount() {
  2097. return root.getChildCount();
  2098. }
  2099. @Override
  2100. protected void paintRemoveRows(final int index,
  2101. final int numberOfRows) {
  2102. for (int i = index; i < index + numberOfRows; i++) {
  2103. final TableRowElement tr = root.getRows().getItem(index);
  2104. paintRemoveRow(tr, index);
  2105. }
  2106. recalculateSectionHeight();
  2107. }
  2108. @Override
  2109. protected TableRowElement getTrByVisualIndex(final int index)
  2110. throws IndexOutOfBoundsException {
  2111. if (index >= 0 && index < root.getChildCount()) {
  2112. return root.getRows().getItem(index);
  2113. } else {
  2114. throw new IndexOutOfBoundsException(
  2115. "No such visual index: " + index);
  2116. }
  2117. }
  2118. @Override
  2119. public void insertRows(int index, int numberOfRows) {
  2120. super.insertRows(index, numberOfRows);
  2121. recalculateElementSizes();
  2122. applyHeightByRows();
  2123. }
  2124. @Override
  2125. public void removeRows(int index, int numberOfRows) {
  2126. /*
  2127. * While the rows in a static section are removed, the scrollbar is
  2128. * temporarily shrunk and then re-expanded. This leads to the fact
  2129. * that the scroll position is scooted up a bit. This means that we
  2130. * need to reset the position here.
  2131. *
  2132. * If Escalator, at some point, gets a JIT evaluation functionality,
  2133. * this re-setting is a strong candidate for removal.
  2134. */
  2135. double oldScrollPos = verticalScrollbar.getScrollPos();
  2136. super.removeRows(index, numberOfRows);
  2137. recalculateElementSizes();
  2138. applyHeightByRows();
  2139. verticalScrollbar.setScrollPos(oldScrollPos);
  2140. }
  2141. @Override
  2142. protected void reapplyDefaultRowHeights() {
  2143. if (root.getChildCount() == 0) {
  2144. return;
  2145. }
  2146. Profiler.enter(
  2147. "Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
  2148. Element tr = root.getRows().getItem(0);
  2149. while (tr != null) {
  2150. reapplyRowHeight(TableRowElement.as(tr), getDefaultRowHeight());
  2151. tr = tr.getNextSiblingElement();
  2152. }
  2153. /*
  2154. * Because all rows are immediately displayed in the static row
  2155. * containers, the section's overall height has most probably
  2156. * changed.
  2157. */
  2158. recalculateSectionHeight();
  2159. Profiler.leave(
  2160. "Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
  2161. }
  2162. @Override
  2163. protected void recalculateSectionHeight() {
  2164. Profiler.enter(
  2165. "Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
  2166. double newHeight = calculateTotalRowHeight();
  2167. if (newHeight != heightOfSection) {
  2168. heightOfSection = newHeight;
  2169. sectionHeightCalculated();
  2170. /*
  2171. * We need to update the scrollbar dimension at this point. If
  2172. * we are scrolled too far down and the static section shrinks,
  2173. * the body will try to render rows that don't exist during
  2174. * body.verifyEscalatorCount. This is because the logical row
  2175. * indices are calculated from the scrollbar position.
  2176. */
  2177. verticalScrollbar.setOffsetSize(
  2178. heightOfEscalator - header.getHeightOfSection()
  2179. - footer.getHeightOfSection() + 1);
  2180. body.verifyEscalatorCount();
  2181. body.spacerContainer.updateSpacerDecosVisibility();
  2182. }
  2183. Profiler.leave(
  2184. "Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
  2185. }
  2186. /**
  2187. * Informs the row container that the height of its respective table
  2188. * section has changed.
  2189. * <p>
  2190. * These calculations might affect some layouting logic, such as the
  2191. * body is being offset by the footer, the footer needs to be readjusted
  2192. * according to its height, and so on.
  2193. * <p>
  2194. * A table section is either header, body or footer.
  2195. */
  2196. protected abstract void sectionHeightCalculated();
  2197. @Override
  2198. protected void refreshCells(Range logicalRowRange, Range colRange) {
  2199. assertArgumentsAreValidAndWithinRange(logicalRowRange.getStart(),
  2200. logicalRowRange.length());
  2201. if (!isAttached()) {
  2202. return;
  2203. }
  2204. Profiler.enter("Escalator.AbstractStaticRowContainer.refreshCells");
  2205. if (hasColumnAndRowData()) {
  2206. for (int row = logicalRowRange.getStart(); row < logicalRowRange
  2207. .getEnd(); row++) {
  2208. final TableRowElement tr = getTrByVisualIndex(row);
  2209. refreshRow(tr, row, colRange);
  2210. }
  2211. }
  2212. Profiler.leave("Escalator.AbstractStaticRowContainer.refreshCells");
  2213. }
  2214. @Override
  2215. protected void paintInsertRows(int visualIndex, int numberOfRows) {
  2216. paintInsertStaticRows(visualIndex, numberOfRows);
  2217. }
  2218. @Override
  2219. protected boolean rowCanBeFrozen(TableRowElement tr) {
  2220. assert root.isOrHasChild(
  2221. tr) : "Row does not belong to this table section";
  2222. return true;
  2223. }
  2224. @Override
  2225. protected double getHeightOfSection() {
  2226. return Math.max(0, heightOfSection);
  2227. }
  2228. }
  2229. private class HeaderRowContainer extends AbstractStaticRowContainer {
  2230. public HeaderRowContainer(final TableSectionElement headElement) {
  2231. super(headElement);
  2232. }
  2233. @Override
  2234. protected void sectionHeightCalculated() {
  2235. double heightOfSection = getHeightOfSection();
  2236. bodyElem.getStyle().setMarginTop(heightOfSection, Unit.PX);
  2237. spacerDecoContainer.getStyle().setMarginTop(heightOfSection,
  2238. Unit.PX);
  2239. verticalScrollbar.getElement().getStyle().setTop(heightOfSection,
  2240. Unit.PX);
  2241. headerDeco.getStyle().setHeight(heightOfSection, Unit.PX);
  2242. }
  2243. @Override
  2244. protected String getCellElementTagName() {
  2245. return "th";
  2246. }
  2247. @Override
  2248. protected AriaGridRole getRowElementRole() {
  2249. return AriaGridRole.ROWHEADER;
  2250. }
  2251. @Override
  2252. protected AriaGridRole getCellElementRole() {
  2253. return AriaGridRole.COLUMNHEADER;
  2254. }
  2255. @Override
  2256. public void setStylePrimaryName(String primaryStyleName) {
  2257. super.setStylePrimaryName(primaryStyleName);
  2258. UIObject.setStylePrimaryName(root, primaryStyleName + "-header");
  2259. }
  2260. }
  2261. private class FooterRowContainer extends AbstractStaticRowContainer {
  2262. public FooterRowContainer(final TableSectionElement footElement) {
  2263. super(footElement);
  2264. }
  2265. @Override
  2266. public void setStylePrimaryName(String primaryStyleName) {
  2267. super.setStylePrimaryName(primaryStyleName);
  2268. UIObject.setStylePrimaryName(root, primaryStyleName + "-footer");
  2269. }
  2270. @Override
  2271. protected String getCellElementTagName() {
  2272. return "td";
  2273. }
  2274. @Override
  2275. protected void sectionHeightCalculated() {
  2276. footerDeco.getStyle().setHeight(footer.getHeightOfSection(),
  2277. Unit.PX);
  2278. }
  2279. }
  2280. private class BodyRowContainerImpl extends AbstractRowContainer
  2281. implements BodyRowContainer {
  2282. /*
  2283. * TODO [[optimize]]: check whether a native JsArray might be faster
  2284. * than LinkedList
  2285. */
  2286. /**
  2287. * The order in which row elements are rendered visually in the browser,
  2288. * with the help of CSS tricks. Usually has nothing to do with the DOM
  2289. * order.
  2290. *
  2291. * @see #sortDomElements()
  2292. */
  2293. private final LinkedList<TableRowElement> visualRowOrder = new LinkedList<>();
  2294. /**
  2295. * The logical index of the topmost row.
  2296. *
  2297. * @deprecated Use the accessors {@link #setTopRowLogicalIndex(int)},
  2298. * {@link #updateTopRowLogicalIndex(int)} and
  2299. * {@link #getTopRowLogicalIndex()} instead
  2300. */
  2301. @Deprecated
  2302. private int topRowLogicalIndex = 0;
  2303. /**
  2304. * A callback function to be executed after new rows are added to the
  2305. * escalator.
  2306. */
  2307. private Consumer<List<TableRowElement>> newEscalatorRowCallback;
  2308. /**
  2309. * Set the logical index of the first dom row in visual order.
  2310. * <p>
  2311. * NOTE: this is not necessarily the first dom row in the dom tree, just
  2312. * the one positioned to the top with CSS. See maintenance notes at the
  2313. * top of this class for further information.
  2314. *
  2315. * @param topRowLogicalIndex
  2316. * logical index of the first dom row in visual order, might
  2317. * not match the dom tree order
  2318. */
  2319. private void setTopRowLogicalIndex(int topRowLogicalIndex) {
  2320. if (LogConfiguration.loggingIsEnabled(Level.INFO)) {
  2321. Logger.getLogger("Escalator.BodyRowContainer")
  2322. .fine("topRowLogicalIndex: " + this.topRowLogicalIndex
  2323. + " -> " + topRowLogicalIndex);
  2324. }
  2325. assert topRowLogicalIndex >= 0 : "topRowLogicalIndex became negative (top left cell contents: "
  2326. + visualRowOrder.getFirst().getCells().getItem(0)
  2327. .getInnerText()
  2328. + ") ";
  2329. /*
  2330. * if there's a smart way of evaluating and asserting the max index,
  2331. * this would be a nice place to put it. I haven't found out an
  2332. * effective and generic solution.
  2333. */
  2334. this.topRowLogicalIndex = topRowLogicalIndex;
  2335. }
  2336. /**
  2337. * Returns the logical index of the first dom row in visual order. This
  2338. * also gives the offset between the logical and visual indexes.
  2339. * <p>
  2340. * NOTE: this is not necessarily the first dom row in the dom tree, just
  2341. * the one positioned to the top with CSS. See maintenance notes at the
  2342. * top of this class for further information.
  2343. *
  2344. * @return logical index of the first dom row in visual order, might not
  2345. * match the dom tree order
  2346. */
  2347. public int getTopRowLogicalIndex() {
  2348. return topRowLogicalIndex;
  2349. }
  2350. /**
  2351. * Updates the logical index of the first dom row in visual order with
  2352. * the given difference.
  2353. * <p>
  2354. * NOTE: this is not necessarily the first dom row in the dom tree, just
  2355. * the one positioned to the top with CSS. See maintenance notes at the
  2356. * top of this class for further information.
  2357. *
  2358. * @param diff
  2359. * the amount to increase or decrease the logical index of
  2360. * the first dom row in visual order
  2361. */
  2362. private void updateTopRowLogicalIndex(int diff) {
  2363. setTopRowLogicalIndex(topRowLogicalIndex + diff);
  2364. }
  2365. private class DeferredDomSorter {
  2366. private static final int SORT_DELAY_MILLIS = 50;
  2367. // as it happens, 3 frames = 50ms @ 60fps.
  2368. private static final int REQUIRED_FRAMES_PASSED = 3;
  2369. private final AnimationCallback frameCounter = new AnimationCallback() {
  2370. @Override
  2371. public void execute(double timestamp) {
  2372. framesPassed++;
  2373. boolean domWasSorted = sortIfConditionsMet();
  2374. if (!domWasSorted) {
  2375. animationHandle = AnimationScheduler.get()
  2376. .requestAnimationFrame(this);
  2377. } else {
  2378. waiting = false;
  2379. }
  2380. }
  2381. };
  2382. private int framesPassed;
  2383. private double startTime;
  2384. private AnimationHandle animationHandle;
  2385. /** <code>true</code> if a sort is scheduled */
  2386. public boolean waiting = false;
  2387. public void reschedule() {
  2388. waiting = true;
  2389. resetConditions();
  2390. animationHandle = AnimationScheduler.get()
  2391. .requestAnimationFrame(frameCounter);
  2392. }
  2393. private boolean sortIfConditionsMet() {
  2394. boolean enoughFramesHavePassed = framesPassed >= REQUIRED_FRAMES_PASSED;
  2395. boolean enoughTimeHasPassed = (Duration.currentTimeMillis()
  2396. - startTime) >= SORT_DELAY_MILLIS;
  2397. boolean notTouchActivity = !scroller.touchHandlerBundle.touching;
  2398. boolean conditionsMet = enoughFramesHavePassed
  2399. && enoughTimeHasPassed && notTouchActivity;
  2400. if (conditionsMet) {
  2401. resetConditions();
  2402. sortDomElements();
  2403. }
  2404. return conditionsMet;
  2405. }
  2406. private void resetConditions() {
  2407. if (animationHandle != null) {
  2408. animationHandle.cancel();
  2409. animationHandle = null;
  2410. }
  2411. startTime = Duration.currentTimeMillis();
  2412. framesPassed = 0;
  2413. }
  2414. }
  2415. private DeferredDomSorter domSorter = new DeferredDomSorter();
  2416. private final SpacerContainer spacerContainer = new SpacerContainer();
  2417. private boolean insertingOrRemoving = false;
  2418. public BodyRowContainerImpl(final TableSectionElement bodyElement) {
  2419. super(bodyElement);
  2420. }
  2421. @Override
  2422. public void setStylePrimaryName(String primaryStyleName) {
  2423. super.setStylePrimaryName(primaryStyleName);
  2424. UIObject.setStylePrimaryName(root, primaryStyleName + "-body");
  2425. spacerContainer.setStylePrimaryName(primaryStyleName);
  2426. }
  2427. public void updateEscalatorRowsOnScroll() {
  2428. if (visualRowOrder.isEmpty()) {
  2429. return;
  2430. }
  2431. boolean rowsWereMoved = false;
  2432. final double topElementPosition;
  2433. final double nextRowBottomOffset;
  2434. SpacerContainer.SpacerImpl topSpacer = spacerContainer
  2435. .getSpacer(getTopRowLogicalIndex() - 1);
  2436. if (topSpacer != null) {
  2437. topElementPosition = topSpacer.getTop();
  2438. nextRowBottomOffset = topSpacer.getHeight()
  2439. + getDefaultRowHeight();
  2440. } else {
  2441. topElementPosition = getRowTop(visualRowOrder.getFirst());
  2442. nextRowBottomOffset = getDefaultRowHeight();
  2443. }
  2444. // TODO [[mpixscroll]]
  2445. final double scrollTop = tBodyScrollTop;
  2446. final double sectionHeight = getHeightOfSection();
  2447. /*
  2448. * Calculate how the visual range is situated in relation to the
  2449. * viewport. Negative value means part of visual range is hidden
  2450. * above or below the viewport, positive value means there is a gap
  2451. * at the top or the bottom of the viewport, zero means exact match.
  2452. * If there is a gap, some rows that are out of view may need to be
  2453. * recycled from the opposite end.
  2454. */
  2455. final double viewportOffsetTop = topElementPosition - scrollTop;
  2456. final double viewportOffsetBottom = scrollTop + sectionHeight
  2457. - getRowTop(
  2458. getTopRowLogicalIndex() + visualRowOrder.size());
  2459. /*
  2460. * You can only scroll far enough to leave a gap if visualRowOrder
  2461. * contains a maximal amount of rows and there is at least one more
  2462. * outside of the visual range. Consequently there can only be a gap
  2463. * in one end of the viewport at a time.
  2464. */
  2465. if (viewportOffsetTop > 0 || (viewportOffsetTop == 0
  2466. && getTopRowLogicalIndex() > 0)) {
  2467. /*
  2468. * Scrolling up. Either there's empty room on top, or there
  2469. * should be a buffer row for tab navigation on top, but there
  2470. * isn't.
  2471. */
  2472. recycleRowsUpOnScroll(viewportOffsetTop);
  2473. rowsWereMoved = true;
  2474. } else if ((viewportOffsetBottom > 0
  2475. && (viewportOffsetTop + nextRowBottomOffset <= 0))
  2476. || (viewportOffsetBottom == 0 && (getTopRowLogicalIndex()
  2477. + visualRowOrder.size() < getRowCount() - 2))) {
  2478. /*
  2479. * Scrolling down. Either there's empty room at the bottom and
  2480. * the viewport has been scrolled more than the topmost visual
  2481. * row, or there should be a buffer row at the bottom to ensure
  2482. * tab navigation works, but there isn't.
  2483. */
  2484. recycleRowsDownOnScroll(topElementPosition, scrollTop);
  2485. // Moving rows may have removed more spacers and created another
  2486. // gap, this time the scroll position needs adjusting. The last
  2487. // row within visual range should be just below the viewport as
  2488. // a buffer for helping with tab navigation, unless it's the
  2489. // last row altogether.
  2490. int lastRowInVisualRange = getTopRowLogicalIndex()
  2491. + visualRowOrder.size() - 1;
  2492. double expectedBottom = getRowTop(lastRowInVisualRange);
  2493. if (lastRowInVisualRange == getRowCount() - 1) {
  2494. expectedBottom += getDefaultRowHeight() + spacerContainer
  2495. .getSpacerHeight(lastRowInVisualRange);
  2496. }
  2497. if (expectedBottom < scrollTop + sectionHeight) {
  2498. double expectedTop = Math.max(0,
  2499. expectedBottom - sectionHeight);
  2500. setBodyScrollPosition(tBodyScrollLeft, expectedTop);
  2501. setScrollTop(expectedTop);
  2502. }
  2503. rowsWereMoved = true;
  2504. }
  2505. if (rowsWereMoved) {
  2506. fireRowVisibilityChangeEvent();
  2507. // schedule updating of the physical indexes
  2508. domSorter.reschedule();
  2509. }
  2510. }
  2511. /**
  2512. * Recycling rows up for {@link #updateEscalatorRowsOnScroll()}.
  2513. * <p>
  2514. * NOTE: This method should not be called directly from anywhere else.
  2515. *
  2516. * @param viewportOffsetTop
  2517. */
  2518. private void recycleRowsUpOnScroll(double viewportOffsetTop) {
  2519. /*
  2520. * We can ignore spacers here, because we keep enough rows within
  2521. * the visual range to fill the viewport completely whether or not
  2522. * any spacers are shown. There is a small tradeoff of having some
  2523. * rows rendered even if they are outside of the viewport, but this
  2524. * simplifies the handling significantly (we can't know what height
  2525. * any individual spacer has before it has been rendered, which
  2526. * happens with a delay) and keeps the visual range size stable
  2527. * while scrolling. Consequently, even if there are spacers within
  2528. * the current visual range, repositioning this many rows won't
  2529. * cause us to run out of rows at the bottom.
  2530. *
  2531. * The viewportOffsetTop is positive and we round up, and
  2532. * visualRowOrder can't be empty since we are scrolling, so there is
  2533. * always going to be at least one row to move. There should also be
  2534. * one buffer row that actually falls outside of the viewport, in
  2535. * order to ensure that tabulator navigation works if the rows have
  2536. * components in them. The buffer row is only needed if filling the
  2537. * gap doesn't bring us to the top row already.
  2538. */
  2539. int rowsToFillTheGap = (int) Math
  2540. .ceil(viewportOffsetTop / getDefaultRowHeight());
  2541. // ensure we don't try to move more rows than are available
  2542. // above
  2543. rowsToFillTheGap = Math.min(rowsToFillTheGap,
  2544. getTopRowLogicalIndex());
  2545. // add the buffer row if there is room for it
  2546. if (rowsToFillTheGap < getTopRowLogicalIndex()) {
  2547. ++rowsToFillTheGap;
  2548. }
  2549. // we may have scrolled up past all the rows and beyond, can
  2550. // only recycle as many rows as we have
  2551. int rowsToRecycle = Math.min(rowsToFillTheGap,
  2552. visualRowOrder.size());
  2553. // select the rows to recycle from the end of the visual range
  2554. int end = visualRowOrder.size();
  2555. int start = end - rowsToRecycle;
  2556. /*
  2557. * Calculate the logical index for insertion point based on how many
  2558. * rows would be needed to fill the gap. Because we are recycling
  2559. * rows to the top the insertion index will also be the new top row
  2560. * logical index.
  2561. */
  2562. int newTopRowLogicalIndex = getTopRowLogicalIndex()
  2563. - rowsToFillTheGap;
  2564. // recycle the rows and move them to their new positions
  2565. moveAndUpdateEscalatorRows(Range.between(start, end), 0,
  2566. newTopRowLogicalIndex);
  2567. setTopRowLogicalIndex(newTopRowLogicalIndex);
  2568. }
  2569. /**
  2570. * Recycling rows down for {@link #updateEscalatorRowsOnScroll()}.
  2571. * <p>
  2572. * NOTE: This method should not be called directly from anywhere else.
  2573. *
  2574. * @param topElementPosition
  2575. * @param scrollTop
  2576. */
  2577. private void recycleRowsDownOnScroll(double topElementPosition,
  2578. double scrollTop) {
  2579. /*
  2580. * It's better to have any extra rows below than above, so move as
  2581. * many of them as possible regardless of how many are needed to
  2582. * fill the gap, as long as one buffer row remains at the top. It
  2583. * should not be possible to scroll down enough to create a gap
  2584. * without it being possible to recycle rows to fill the gap, so
  2585. * viewport itself doesn't need adjusting no matter what.
  2586. */
  2587. // we already have the rows and spacers here and we don't want
  2588. // to recycle rows that are going to stay visible, so the
  2589. // spacers have to be taken into account
  2590. double extraRowPxAbove = getRowHeightsSumBetweenPxExcludingSpacers(
  2591. topElementPosition, scrollTop);
  2592. // how many rows fit within that extra space and can be
  2593. // recycled, rounded towards zero to avoid moving any partially
  2594. // visible rows
  2595. int rowsToCoverTheExtra = (int) Math
  2596. .floor(extraRowPxAbove / getDefaultRowHeight());
  2597. // leave one to ensure there is a buffer row to help with tab
  2598. // navigation
  2599. if (rowsToCoverTheExtra > 0) {
  2600. --rowsToCoverTheExtra;
  2601. }
  2602. /*
  2603. * Don't move more rows than there are to move, but also don't move
  2604. * more rows than should exist at the bottom. However, it's not
  2605. * possible to scroll down beyond available rows, so there is always
  2606. * at least one row to recycle.
  2607. */
  2608. int rowsToRecycle = Math.min(
  2609. Math.min(rowsToCoverTheExtra, visualRowOrder.size()),
  2610. getRowCount() - getTopRowLogicalIndex()
  2611. - visualRowOrder.size());
  2612. // are only some of the rows getting recycled instead of all
  2613. // of them
  2614. boolean partialMove = rowsToRecycle < visualRowOrder.size();
  2615. // calculate the logical index where the rows should be moved
  2616. int logicalTargetIndex;
  2617. if (partialMove) {
  2618. /*
  2619. * We scroll so little that we can just keep adding the rows
  2620. * immediately below the current escalator.
  2621. */
  2622. logicalTargetIndex = getTopRowLogicalIndex()
  2623. + visualRowOrder.size();
  2624. } else {
  2625. /*
  2626. * Since all escalator rows are getting recycled all spacers are
  2627. * going to get removed and the calculations have to ignore the
  2628. * spacers again in order to figure out which rows are to be
  2629. * displayed. In practice we may end up scrolling further down
  2630. * than the scroll position indicated initially as the spacers
  2631. * that get removed give room for more rows than expected.
  2632. *
  2633. * We can rely on calculations here because there won't be any
  2634. * old rows left to end up mismatched with.
  2635. */
  2636. logicalTargetIndex = (int) Math
  2637. .floor(scrollTop / getDefaultRowHeight());
  2638. /*
  2639. * Make sure we don't try to move rows below the actual row
  2640. * count, even if some of the rows end up hidden at the top as a
  2641. * result. This won't leave us with any old rows in any case,
  2642. * because we already checked earlier that there is room to
  2643. * recycle all the rows. It's only a question of how the new
  2644. * visual range gets positioned in relation to the viewport.
  2645. */
  2646. if (logicalTargetIndex
  2647. + visualRowOrder.size() > getRowCount()) {
  2648. logicalTargetIndex = getRowCount() - visualRowOrder.size();
  2649. }
  2650. }
  2651. /*
  2652. * Recycle the rows and move them to their new positions. Since we
  2653. * are moving the viewport downwards, the visual target index is
  2654. * always at the bottom and matches the length of the visual range.
  2655. * Note: Due to how moveAndUpdateEscalatorRows works, this will work
  2656. * out even if we move all the rows, and try to place them
  2657. * "at the end".
  2658. */
  2659. moveAndUpdateEscalatorRows(Range.between(0, rowsToRecycle),
  2660. visualRowOrder.size(), logicalTargetIndex);
  2661. // top row logical index needs to be updated differently
  2662. // depending on which update strategy was used, since the rows
  2663. // are being moved down
  2664. if (partialMove) {
  2665. // move down by the amount of recycled rows
  2666. updateTopRowLogicalIndex(rowsToRecycle);
  2667. } else {
  2668. // the insertion index is the new top row logical index
  2669. setTopRowLogicalIndex(logicalTargetIndex);
  2670. }
  2671. }
  2672. /**
  2673. * Calculates how much of the given range contains only rows with
  2674. * spacers excluded.
  2675. *
  2676. * @param y1
  2677. * start position
  2678. * @param y2
  2679. * end position
  2680. * @return position difference excluding any space taken up by spacers
  2681. */
  2682. private double getRowHeightsSumBetweenPxExcludingSpacers(double y1,
  2683. double y2) {
  2684. assert y1 < y2 : "y1 must be smaller than y2";
  2685. double viewportPx = y2 - y1;
  2686. double spacerPx = spacerContainer.getSpacerHeightsSumBetweenPx(y1,
  2687. SpacerInclusionStrategy.PARTIAL, y2,
  2688. SpacerInclusionStrategy.PARTIAL);
  2689. return viewportPx - spacerPx;
  2690. }
  2691. @Override
  2692. public void insertRows(int index, int numberOfRows) {
  2693. insertingOrRemoving = true;
  2694. super.insertRows(index, numberOfRows);
  2695. insertingOrRemoving = false;
  2696. if (heightMode == HeightMode.UNDEFINED) {
  2697. setHeightByRows(getRowCount());
  2698. }
  2699. }
  2700. @Override
  2701. public void removeRows(int index, int numberOfRows) {
  2702. insertingOrRemoving = true;
  2703. super.removeRows(index, numberOfRows);
  2704. insertingOrRemoving = false;
  2705. if (heightMode == HeightMode.UNDEFINED) {
  2706. setHeightByRows(getRowCount());
  2707. }
  2708. }
  2709. @Override
  2710. protected void paintInsertRows(final int index,
  2711. final int numberOfRows) {
  2712. assert index >= 0
  2713. && index < getRowCount() : "Attempting to insert a row "
  2714. + "outside of the available range.";
  2715. assert numberOfRows > 0 : "Attempting to insert a non-positive "
  2716. + "amount of rows, something must be wrong.";
  2717. if (numberOfRows <= 0) {
  2718. return;
  2719. }
  2720. /*
  2721. * NOTE: this method handles and manipulates logical, visual, and
  2722. * physical indexes a lot. If you don't remember what those mean and
  2723. * how they relate to each other, see the top of this class for
  2724. * Maintenance Notes.
  2725. *
  2726. * At the beginning of this method the logical index of the data
  2727. * provider has already been updated to include the new rows, but
  2728. * visual and physical indexes have not, nor has the spacer indexing
  2729. * been updated, and the topRowLogicalIndex may be out of date as
  2730. * well.
  2731. */
  2732. // top of visible area before any rows are actually added
  2733. double scrollTop = getScrollTop();
  2734. // logical index of the first row within the visual range before any
  2735. // rows are actually added
  2736. int oldTopRowLogicalIndex = getTopRowLogicalIndex();
  2737. // length of the visual range before any rows are actually added
  2738. int oldVisualRangeLength = visualRowOrder.size();
  2739. /*
  2740. * If there is room for more dom rows within the maximum visual
  2741. * range, add them. Calling this method repositions all the rows and
  2742. * spacers below the insertion point and updates the spacer indexes
  2743. * accordingly.
  2744. *
  2745. * TODO: Details rows should be added and populated here, since they
  2746. * have variable heights and affect the position calculations.
  2747. * Currently that's left to be triggered at the end and with a
  2748. * delay. If any new spacers exist, everything below them is going
  2749. * to be repositioned again for every spacer addition.
  2750. */
  2751. final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
  2752. index - oldTopRowLogicalIndex, index, numberOfRows);
  2753. // is the insertion point for new rows below visual range (viewport
  2754. // is irrelevant)
  2755. final boolean newRowsInsertedBelowVisualRange = index >= oldVisualRangeLength
  2756. + oldTopRowLogicalIndex;
  2757. // is the insertion point for new rows above initial visual range
  2758. final boolean newRowsInsertedAboveVisualRange = index <= oldTopRowLogicalIndex;
  2759. // is the insertion point for new rows above viewport
  2760. final boolean newRowsInsertedAboveCurrentViewport = getRowTop(
  2761. index) < scrollTop;
  2762. if (newRowsInsertedBelowVisualRange) {
  2763. /*
  2764. * There is no change to scroll position, and all other changes
  2765. * to positioning and indexing are out of visual range or
  2766. * already done (if addedRows is not empty).
  2767. */
  2768. } else if (newRowsInsertedAboveVisualRange && addedRows.isEmpty()
  2769. && newRowsInsertedAboveCurrentViewport) {
  2770. /*
  2771. * This section can only be reached if the insertion point is
  2772. * above the visual range, the visual range already covers a
  2773. * maximal amount of rows, and we are scrolled down enough that
  2774. * the top row is either partially or completely hidden. The
  2775. * last two points happen by default if the first row of the
  2776. * visual range has any other logical index than zero. Any other
  2777. * use cases involving the top row within the visual range need
  2778. * different handling.
  2779. */
  2780. paintInsertRowsAboveViewPort(index, numberOfRows,
  2781. oldTopRowLogicalIndex);
  2782. } else if (newRowsInsertedAboveCurrentViewport) {
  2783. /*
  2784. * Rows were inserted within the visual range but above the
  2785. * viewport. This includes the use case where the insertion
  2786. * point is just above the visual range and we are scrolled down
  2787. * a bit but the visual range doesn't have maximal amount of
  2788. * rows yet (can only happen with spacers in play), so more rows
  2789. * were added to the visual range but no rows need to be
  2790. * recycled.
  2791. */
  2792. paintInsertRowsWithinVisualRangeButAboveViewport(index,
  2793. numberOfRows, oldTopRowLogicalIndex, addedRows.size());
  2794. } else {
  2795. /*
  2796. * Rows added within visual range and either within or below the
  2797. * viewport. Recycled rows come from the END of the visual
  2798. * range.
  2799. */
  2800. paintInsertRowsWithinVisualRangeAndWithinOrBelowViewport(index,
  2801. numberOfRows, oldTopRowLogicalIndex, addedRows.size());
  2802. }
  2803. /*
  2804. * Calling insertRows will always change the number of rows - update
  2805. * the scrollbar sizes. This calculation isn't affected by actual
  2806. * dom rows amount or contents except for spacer heights. Spacers
  2807. * that don't fit the visual range are considered to have no height
  2808. * and might affect scrollbar calculations aversely, but that can't
  2809. * be avoided since they have unknown and variable heights.
  2810. */
  2811. scroller.recalculateScrollbarsForVirtualViewport();
  2812. }
  2813. /**
  2814. * Row insertion handling for {@link #paintInsertRows(int, int)} when
  2815. * the range will be inserted above the visual range.
  2816. * <p>
  2817. * NOTE: This method should not be called directly from anywhere else.
  2818. *
  2819. * @param index
  2820. * @param numberOfRows
  2821. * @param oldTopRowLogicalIndex
  2822. */
  2823. private void paintInsertRowsAboveViewPort(int index, int numberOfRows,
  2824. int oldTopRowLogicalIndex) {
  2825. /*
  2826. * Because there is no need to expand the visual range, no row or
  2827. * spacer contents get updated. All rows, spacers, and scroll
  2828. * position simply need to be shifted down accordingly and the
  2829. * spacer indexes need updating.
  2830. */
  2831. spacerContainer.updateSpacerIndexesForRowAndAfter(index,
  2832. oldTopRowLogicalIndex + visualRowOrder.size(),
  2833. numberOfRows);
  2834. // height of a single row
  2835. double defaultRowHeight = getDefaultRowHeight();
  2836. // height of new rows, out of visual range so spacers assumed to
  2837. // have no height
  2838. double newRowsHeight = numberOfRows * defaultRowHeight;
  2839. // update the positions
  2840. moveViewportAndContent(index, newRowsHeight, newRowsHeight,
  2841. newRowsHeight);
  2842. // top row logical index moves down by the number of new rows
  2843. updateTopRowLogicalIndex(numberOfRows);
  2844. }
  2845. /**
  2846. * Row insertion handling for {@link #paintInsertRows(int, int)} when
  2847. * the range will be inserted within the visual range above the
  2848. * viewport.
  2849. * <p>
  2850. * NOTE: This method should not be called directly from anywhere else.
  2851. *
  2852. * @param index
  2853. * @param numberOfRows
  2854. * @param oldTopRowLogicalIndex
  2855. * @param addedRowCount
  2856. */
  2857. private void paintInsertRowsWithinVisualRangeButAboveViewport(int index,
  2858. int numberOfRows, int oldTopRowLogicalIndex,
  2859. int addedRowCount) {
  2860. /*
  2861. * Unless we are scrolled all the way to the top the visual range is
  2862. * always out of view because we need a buffer row for tabulator
  2863. * navigation. Depending on the scroll position and spacers there
  2864. * might even be several rendered rows above the viewport,
  2865. * especially when we are scrolled all the way to the bottom.
  2866. *
  2867. * Even though the new rows will be initially out of view they still
  2868. * need to be correctly populated and positioned. Their contents
  2869. * won't be refreshed if they become visible later on (e.g. when a
  2870. * spacer gets hidden, which causes more rows to fit within the
  2871. * viewport) because they are expected to be already up to date.
  2872. *
  2873. * Note that it's not possible to insert content so that it's
  2874. * partially visible at the top. A partially visible row at top will
  2875. * still be the exact same partially visible row after the
  2876. * insertion, no matter which side of that row the new content gets
  2877. * inserted to. This section handles the use case where the new
  2878. * content is inserted above the partially visible row.
  2879. *
  2880. * Because the insertion point is out of view above the viewport,
  2881. * the only thing that should change for the end user visually is
  2882. * the scroll handle, which gets a new position and possibly turns a
  2883. * bit smaller if a lot of rows got inserted.
  2884. *
  2885. * From a technical point of view this also means that any rows that
  2886. * might need to get recycled should be taken from the BEGINNING of
  2887. * the visual range, above the insertion point. There might still be
  2888. * some "extra" rows below the viewport as well, but those should be
  2889. * left alone. They are going to be needed where they are if any
  2890. * spacers get closed or reduced in size.
  2891. *
  2892. * On a practical level we need to tweak the virtual viewport --
  2893. * scroll handle positions, row and spacer positions, and ensure the
  2894. * scroll area height is calculated correctly. Viewport should
  2895. * remain in a fixed position in relation to the existing rows and
  2896. * display no new rows. If any rows get recycled and have spacers
  2897. * either before or after the update the height of those spacers
  2898. * affects the position calculations.
  2899. *
  2900. * Insertion point can be anywhere from just before the previous
  2901. * first row of the visual range to just before the first actually
  2902. * visible row. The insertion shifts down the content below
  2903. * insertion point, which excludes any dom rows that remain above
  2904. * the insertion point after recycling is finished. After the rows
  2905. * below insertion point have been moved the viewport needs to be
  2906. * shifted down a similar amount to regain its old relative position
  2907. * again.
  2908. *
  2909. * The visual range only ever contains at most as many rows as would
  2910. * fit within the viewport without any spacers with one extra row on
  2911. * both at the top and at the bottom as buffer rows, so the amount
  2912. * of rows that needs to be checked is always reasonably limited.
  2913. */
  2914. // insertion index within the visual range
  2915. int visualTargetIndex = index - oldTopRowLogicalIndex;
  2916. // how many dom rows before insertion point versus how many new
  2917. // rows didn't get their own dom rows -- smaller amount
  2918. // determines how many rows can and need to be recycled
  2919. int rowsToUpdate = Math.min(visualTargetIndex,
  2920. numberOfRows - addedRowCount);
  2921. // height of a single row
  2922. double defaultRowHeight = getDefaultRowHeight();
  2923. boolean rowVisibilityChanged = false;
  2924. if (rowsToUpdate > 0) {
  2925. // recycle the rows and update the positions, adjust
  2926. // logical index for inserted rows that won't fit within
  2927. // visual range
  2928. int logicalIndex = index + numberOfRows - rowsToUpdate;
  2929. if (visualTargetIndex > 0) {
  2930. // move after any added dom rows
  2931. moveAndUpdateEscalatorRows(Range.between(0, rowsToUpdate),
  2932. visualTargetIndex + addedRowCount, logicalIndex);
  2933. } else {
  2934. // move before any added dom rows
  2935. moveAndUpdateEscalatorRows(Range.between(0, rowsToUpdate),
  2936. visualTargetIndex, logicalIndex);
  2937. }
  2938. // adjust viewport down to maintain the initial position
  2939. double newRowsHeight = numberOfRows * defaultRowHeight;
  2940. double newSpacerHeights = spacerContainer
  2941. .getSpacerHeightsSumUntilIndex(
  2942. logicalIndex + rowsToUpdate)
  2943. - spacerContainer.getSpacerHeightsSumUntilIndex(index);
  2944. /*
  2945. * FIXME: spacers haven't been added yet and they can cause
  2946. * escalator contents to shift after the fact in a way that
  2947. * can't be countered for here.
  2948. *
  2949. * FIXME: verticalScrollbar internal state causes this update to
  2950. * fail partially and the next attempt at scrolling causes
  2951. * things to jump.
  2952. *
  2953. * Couldn't find a quick fix to either problem and this use case
  2954. * is somewhat marginal so left them here for now.
  2955. */
  2956. moveViewportAndContent(null, 0, 0,
  2957. newSpacerHeights + newRowsHeight);
  2958. rowVisibilityChanged = true;
  2959. } else {
  2960. // no rows to recycle but update the spacer indexes
  2961. spacerContainer.updateSpacerIndexesForRowAndAfter(index,
  2962. index + numberOfRows - addedRowCount,
  2963. numberOfRows - addedRowCount);
  2964. double newRowsHeight = numberOfRows * defaultRowHeight;
  2965. if (addedRowCount > 0) {
  2966. // update the viewport, rows and spacers were
  2967. // repositioned already by the method for adding dom
  2968. // rows
  2969. moveViewportAndContent(null, 0, 0, newRowsHeight);
  2970. rowVisibilityChanged = true;
  2971. } else {
  2972. // all changes are actually above the viewport after
  2973. // all, update all positions
  2974. moveViewportAndContent(index, newRowsHeight, newRowsHeight,
  2975. newRowsHeight);
  2976. }
  2977. }
  2978. if (numberOfRows > addedRowCount) {
  2979. /*
  2980. * If there are more new rows than how many new dom rows got
  2981. * added, the top row logical index necessarily gets shifted
  2982. * down by that difference because recycling doesn't replace any
  2983. * logical rows, just shifts them off the visual range, and the
  2984. * inserted rows that don't fit to the visual range also push
  2985. * the other rows down. If every new row got new dom rows as
  2986. * well the top row logical index doesn't change, because the
  2987. * insertion point was within the visual range.
  2988. */
  2989. updateTopRowLogicalIndex(numberOfRows - addedRowCount);
  2990. }
  2991. if (rowVisibilityChanged) {
  2992. fireRowVisibilityChangeEvent();
  2993. }
  2994. if (rowsToUpdate > 0) {
  2995. // update the physical index
  2996. sortDomElements();
  2997. }
  2998. }
  2999. /**
  3000. * Row insertion handling for {@link #paintInsertRows(int, int)} when
  3001. * the range will be inserted within the visual range either within or
  3002. * below the viewport.
  3003. * <p>
  3004. * NOTE: This method should not be called directly from anywhere else.
  3005. *
  3006. * @param index
  3007. * @param numberOfRows
  3008. * @param oldTopRowLogicalIndex
  3009. * @param addedRowCount
  3010. */
  3011. private void paintInsertRowsWithinVisualRangeAndWithinOrBelowViewport(
  3012. int index, int numberOfRows, int oldTopRowLogicalIndex,
  3013. int addedRowCount) {
  3014. // insertion index within the visual range
  3015. int visualIndex = index - oldTopRowLogicalIndex;
  3016. // how many dom rows after insertion point versus how many new
  3017. // rows to add -- smaller amount determines how many rows can or
  3018. // need to be recycled, excluding the rows that already got new
  3019. // dom rows
  3020. int rowsToUpdate = Math.max(
  3021. Math.min(visualRowOrder.size() - visualIndex, numberOfRows)
  3022. - addedRowCount,
  3023. 0);
  3024. if (rowsToUpdate > 0) {
  3025. moveAndUpdateEscalatorRows(
  3026. Range.between(visualRowOrder.size() - rowsToUpdate,
  3027. visualRowOrder.size()),
  3028. visualIndex + addedRowCount, index + addedRowCount);
  3029. fireRowVisibilityChangeEvent();
  3030. // update the physical index
  3031. sortDomElements();
  3032. }
  3033. }
  3034. /**
  3035. * Move escalator rows around, and make sure everything gets
  3036. * appropriately repositioned and repainted. In the case of insertion or
  3037. * removal, following spacer indexes get updated as well.
  3038. *
  3039. * @param visualSourceRange
  3040. * the range of rows to move to a new place
  3041. * @param visualTargetIndex
  3042. * the visual index where the rows will be placed to
  3043. * @param logicalTargetIndex
  3044. * the logical index to be assigned to the first moved row
  3045. */
  3046. private void moveAndUpdateEscalatorRows(final Range visualSourceRange,
  3047. final int visualTargetIndex, final int logicalTargetIndex)
  3048. throws IllegalArgumentException {
  3049. if (visualSourceRange.isEmpty()) {
  3050. return;
  3051. }
  3052. int sourceRangeLength = visualSourceRange.length();
  3053. int domRowCount = getDomRowCount();
  3054. int rowCount = getRowCount();
  3055. assert visualSourceRange.getStart() >= 0 : "Visual source start "
  3056. + "must be 0 or greater (was "
  3057. + visualSourceRange.getStart() + ")";
  3058. assert logicalTargetIndex >= 0 : "Logical target must be 0 or "
  3059. + "greater (was " + logicalTargetIndex + ")";
  3060. assert visualTargetIndex >= 0 : "Visual target must be 0 or greater (was "
  3061. + visualTargetIndex + ")";
  3062. assert visualTargetIndex <= domRowCount : "Visual target "
  3063. + "must not be greater than the number of escalator rows (was "
  3064. + visualTargetIndex + ", escalator rows " + domRowCount
  3065. + ")";
  3066. assert logicalTargetIndex
  3067. + sourceRangeLength <= rowCount : "Logical "
  3068. + "target leads to rows outside of the data range ("
  3069. + Range.withLength(logicalTargetIndex,
  3070. sourceRangeLength)
  3071. + " goes beyond " + Range.withLength(0, rowCount)
  3072. + ")";
  3073. /*
  3074. * Since we move a range into another range, the indices might move
  3075. * about. Having 10 rows, if we move 0..1 to index 10 (to the end of
  3076. * the collection), the target range will end up being 8..9, instead
  3077. * of 10..11.
  3078. *
  3079. * This applies only if we move elements forward in the collection,
  3080. * not backward.
  3081. */
  3082. final int adjustedVisualTargetIndex;
  3083. if (visualSourceRange.getStart() < visualTargetIndex) {
  3084. adjustedVisualTargetIndex = visualTargetIndex
  3085. - sourceRangeLength;
  3086. } else {
  3087. adjustedVisualTargetIndex = visualTargetIndex;
  3088. }
  3089. int oldTopRowLogicalIndex = getTopRowLogicalIndex();
  3090. // first moved row's logical index before move
  3091. int oldSourceRangeLogicalStart = oldTopRowLogicalIndex
  3092. + visualSourceRange.getStart();
  3093. // new top row logical index
  3094. int newTopRowLogicalIndex = logicalTargetIndex
  3095. - adjustedVisualTargetIndex;
  3096. // variables for update types that require special handling
  3097. boolean recycledToTop = logicalTargetIndex < oldTopRowLogicalIndex;
  3098. boolean recycledFromTop = visualSourceRange.getStart() == 0;
  3099. boolean scrollingUp = recycledToTop
  3100. && visualSourceRange.getEnd() == visualRowOrder.size();
  3101. boolean scrollingDown = recycledFromTop
  3102. && logicalTargetIndex >= oldTopRowLogicalIndex
  3103. + visualRowOrder.size();
  3104. if (visualSourceRange.getStart() != adjustedVisualTargetIndex) {
  3105. /*
  3106. * Reorder the rows to their correct places within
  3107. * visualRowOrder (unless rows are moved back to their original
  3108. * places)
  3109. */
  3110. /*
  3111. * TODO [[optimize]]: move whichever set is smaller: the ones
  3112. * explicitly moved, or the others. So, with 10 escalator rows,
  3113. * if we are asked to move idx[0..8] to the end of the list,
  3114. * it's faster to just move idx[9] to the beginning.
  3115. */
  3116. final List<TableRowElement> removedRows = new ArrayList<>(
  3117. sourceRangeLength);
  3118. for (int i = 0; i < sourceRangeLength; i++) {
  3119. final TableRowElement tr = visualRowOrder
  3120. .remove(visualSourceRange.getStart());
  3121. removedRows.add(tr);
  3122. }
  3123. visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows);
  3124. }
  3125. // refresh contents of rows to be recycled, returns the combined
  3126. // height of the spacers that got removed from visual range
  3127. double spacerHeightsOfRecycledRowsBefore = refreshRecycledRowContents(
  3128. logicalTargetIndex, adjustedVisualTargetIndex,
  3129. sourceRangeLength, oldSourceRangeLogicalStart);
  3130. boolean movedDown = adjustedVisualTargetIndex != visualTargetIndex;
  3131. boolean recycledToOrFromTop = recycledToTop || recycledFromTop;
  3132. // update spacer indexes unless we are scrolling -- with scrolling
  3133. // the remaining spacers are where they belong, the recycled ones
  3134. // were already removed, and new ones will be added with delay
  3135. if (!(scrollingUp || scrollingDown)) {
  3136. if (recycledToOrFromTop) {
  3137. updateSpacerIndexesForMoveWhenRecycledToOrFromTop(
  3138. oldSourceRangeLogicalStart, sourceRangeLength,
  3139. oldTopRowLogicalIndex, newTopRowLogicalIndex,
  3140. recycledFromTop);
  3141. } else {
  3142. updateSpacerIndexesForMoveWhenNotRecycledToOrFromTop(
  3143. logicalTargetIndex, oldSourceRangeLogicalStart,
  3144. sourceRangeLength, movedDown);
  3145. }
  3146. }
  3147. // Would be useful if new spacer heights could be determined
  3148. // here already but their contents are populated with delay.
  3149. // If the heights ever become available immediately, the
  3150. // handling that follows needs to be updated to take the new
  3151. // spacer heights into account.
  3152. repositionMovedRows(adjustedVisualTargetIndex, sourceRangeLength,
  3153. newTopRowLogicalIndex);
  3154. // variables for reducing the amount of necessary parameters
  3155. boolean scrollingDownAndNoSpacersRemoved = scrollingDown
  3156. && spacerHeightsOfRecycledRowsBefore <= 0d;
  3157. boolean spacerHeightsChanged = spacerHeightsOfRecycledRowsBefore > 0d;
  3158. repositionRowsShiftedByTheMove(visualSourceRange, visualTargetIndex,
  3159. adjustedVisualTargetIndex, newTopRowLogicalIndex,
  3160. scrollingDownAndNoSpacersRemoved, scrollingUp,
  3161. recycledToTop);
  3162. repositionRowsBelowMovedAndShiftedIfNeeded(visualSourceRange,
  3163. visualTargetIndex, adjustedVisualTargetIndex,
  3164. newTopRowLogicalIndex, (scrollingUp || scrollingDown),
  3165. recycledToOrFromTop, spacerHeightsChanged);
  3166. }
  3167. /**
  3168. * Refresh the contents of the affected rows for
  3169. * {@link #moveAndUpdateEscalatorRows(Range, int, int)}
  3170. * <p>
  3171. * NOTE: This method should not be called directly from anywhere else.
  3172. *
  3173. * @param logicalTargetIndex
  3174. * @param adjustedVisualTargetIndex
  3175. * @param sourceRangeLength
  3176. * @param spacerHeightsBeforeMoveTotal
  3177. * @param oldSourceRangeLogicalStart
  3178. * @return the combined height of any removed spacers
  3179. */
  3180. private double refreshRecycledRowContents(int logicalTargetIndex,
  3181. int adjustedVisualTargetIndex, int sourceRangeLength,
  3182. int oldSourceRangeLogicalStart) {
  3183. final ListIterator<TableRowElement> iter = visualRowOrder
  3184. .listIterator(adjustedVisualTargetIndex);
  3185. double removedSpacerHeights = 0d;
  3186. for (int i = 0; i < sourceRangeLength; ++i) {
  3187. final TableRowElement tr = iter.next();
  3188. int logicalIndex = logicalTargetIndex + i;
  3189. // clear old spacer
  3190. SpacerContainer.SpacerImpl spacer = spacerContainer
  3191. .getSpacer(oldSourceRangeLogicalStart + i);
  3192. if (spacer != null) {
  3193. double spacerHeight = spacer.getHeight();
  3194. removedSpacerHeights += spacerHeight;
  3195. spacerContainer
  3196. .removeSpacer(oldSourceRangeLogicalStart + i);
  3197. }
  3198. refreshRow(tr, logicalIndex);
  3199. }
  3200. return removedSpacerHeights;
  3201. }
  3202. /**
  3203. * Update the spacer indexes to correspond with logical indexes for
  3204. * {@link #moveAndUpdateEscalatorRows(Range, int, int)} when the move
  3205. * recycles rows to or from top
  3206. * <p>
  3207. * NOTE: This method should not be called directly from anywhere else.
  3208. *
  3209. * @param oldSourceRangeLogicalStart
  3210. * @param sourceRangeLength
  3211. * @param oldTopRowLogicalIndex
  3212. * @param newTopRowLogicalIndex
  3213. * @param recycledFromTop
  3214. */
  3215. private void updateSpacerIndexesForMoveWhenRecycledToOrFromTop(
  3216. int oldSourceRangeLogicalStart, int sourceRangeLength,
  3217. int oldTopRowLogicalIndex, int newTopRowLogicalIndex,
  3218. boolean recycledFromTop) {
  3219. if (recycledFromTop) {
  3220. // first rows are getting recycled thanks to insertion or
  3221. // removal, all the indexes below need to be updated
  3222. // accordingly
  3223. int indexesToShift;
  3224. if (newTopRowLogicalIndex != oldTopRowLogicalIndex) {
  3225. indexesToShift = newTopRowLogicalIndex
  3226. - oldTopRowLogicalIndex;
  3227. } else {
  3228. indexesToShift = -sourceRangeLength;
  3229. }
  3230. spacerContainer.updateSpacerIndexesForRowAndAfter(
  3231. oldSourceRangeLogicalStart + sourceRangeLength,
  3232. oldTopRowLogicalIndex + visualRowOrder.size(),
  3233. indexesToShift);
  3234. } else {
  3235. // rows recycled to the top, move the remaining spacer
  3236. // indexes up
  3237. spacerContainer.updateSpacerIndexesForRowAndAfter(
  3238. oldSourceRangeLogicalStart + sourceRangeLength,
  3239. getRowCount() + sourceRangeLength, -sourceRangeLength);
  3240. }
  3241. }
  3242. /**
  3243. * Update the spacer indexes to correspond with logical indexes for
  3244. * {@link #moveAndUpdateEscalatorRows(Range, int, int)} when the move
  3245. * does not recycle rows to or from top
  3246. * <p>
  3247. * NOTE: This method should not be called directly from anywhere else.
  3248. *
  3249. * @param logicalTargetIndex
  3250. * @param oldSourceRangeLogicalStart
  3251. * @param sourceRangeLength
  3252. * @param movedDown
  3253. */
  3254. private void updateSpacerIndexesForMoveWhenNotRecycledToOrFromTop(
  3255. int logicalTargetIndex, int oldSourceRangeLogicalStart,
  3256. int sourceRangeLength, boolean movedDown) {
  3257. if (movedDown) {
  3258. // move the shifted spacer indexes up to fill the freed
  3259. // space
  3260. spacerContainer.updateSpacerIndexesForRowAndAfter(
  3261. oldSourceRangeLogicalStart + sourceRangeLength,
  3262. logicalTargetIndex + sourceRangeLength,
  3263. -sourceRangeLength);
  3264. } else {
  3265. // move the shifted spacer indexes down to fill the freed
  3266. // space
  3267. spacerContainer.updateSpacerIndexesForRowAndAfter(
  3268. logicalTargetIndex, oldSourceRangeLogicalStart,
  3269. sourceRangeLength);
  3270. }
  3271. }
  3272. /**
  3273. * Reposition the rows that were moved for
  3274. * {@link #moveAndUpdateEscalatorRows(Range, int, int)}
  3275. * <p>
  3276. * NOTE: This method should not be called directly from anywhere else.
  3277. *
  3278. * @param adjustedVisualTargetIndex
  3279. * @param sourceRangeLength
  3280. * @param newTopRowLogicalIndex
  3281. */
  3282. private void repositionMovedRows(int adjustedVisualTargetIndex,
  3283. int sourceRangeLength, int newTopRowLogicalIndex) {
  3284. int start = adjustedVisualTargetIndex;
  3285. updateRowPositions(newTopRowLogicalIndex + start, start,
  3286. sourceRangeLength);
  3287. }
  3288. /**
  3289. * Reposition the rows that were shifted by the move for
  3290. * {@link #moveAndUpdateEscalatorRows(Range, int, int)}
  3291. * <p>
  3292. * NOTE: This method should not be called directly from anywhere else.
  3293. *
  3294. * @param visualSourceRange
  3295. * @param visualTargetIndex
  3296. * @param adjustedVisualTargetIndex
  3297. * @param newTopRowLogicalIndex
  3298. * @param scrollingDownAndNoSpacersRemoved
  3299. * @param scrollingUp
  3300. * @param recycledToTop
  3301. */
  3302. private void repositionRowsShiftedByTheMove(Range visualSourceRange,
  3303. int visualTargetIndex, int adjustedVisualTargetIndex,
  3304. int newTopRowLogicalIndex,
  3305. boolean scrollingDownAndNoSpacersRemoved, boolean scrollingUp,
  3306. boolean recycledToTop) {
  3307. if (visualSourceRange.length() == visualRowOrder.size()) {
  3308. // all rows got updated and were repositioned already
  3309. return;
  3310. }
  3311. if (scrollingDownAndNoSpacersRemoved || scrollingUp) {
  3312. // scrolling, no spacers got removed from or added above any
  3313. // remaining rows so everything is where it belongs already
  3314. // (there is no check for added spacers because adding happens
  3315. // with delay, whether any spacers are coming or not they don't
  3316. // exist yet and thus can't be taken into account here)
  3317. return;
  3318. }
  3319. if (adjustedVisualTargetIndex != visualTargetIndex) {
  3320. // rows moved down, shifted rows need to be moved up
  3321. int start = visualSourceRange.getStart();
  3322. updateRowPositions(newTopRowLogicalIndex + start, start,
  3323. adjustedVisualTargetIndex - start);
  3324. } else {
  3325. // rows moved up, shifted rows need to be repositioned
  3326. // unless it's just a recycling and no spacer heights
  3327. // above got updated
  3328. if (recycledToTop) {
  3329. // rows below the shifted ones need to be moved up (which is
  3330. // done in the next helper method) but the shifted rows
  3331. // themselves are already where they belong
  3332. // (this should only be done if no spacers were added, but
  3333. // we can't know that yet so we'll have to adjust for them
  3334. // afterwards if any do appear)
  3335. return;
  3336. }
  3337. int start = adjustedVisualTargetIndex
  3338. + visualSourceRange.length();
  3339. updateRowPositions(newTopRowLogicalIndex + start, start,
  3340. visualSourceRange.getEnd() - start);
  3341. }
  3342. }
  3343. /**
  3344. * If necessary, reposition the rows that are below those rows that got
  3345. * moved or shifted for
  3346. * {@link #moveAndUpdateEscalatorRows(Range, int, int)}
  3347. * <p>
  3348. * NOTE: This method should not be called directly from anywhere else.
  3349. *
  3350. * @param visualSourceRange
  3351. * @param visualTargetIndex
  3352. * @param adjustedVisualTargetIndex
  3353. * @param newTopRowLogicalIndex
  3354. * @param scrolling
  3355. * @param recycledToOrFromTop
  3356. * @param spacerHeightsChanged
  3357. */
  3358. private void repositionRowsBelowMovedAndShiftedIfNeeded(
  3359. Range visualSourceRange, int visualTargetIndex,
  3360. int adjustedVisualTargetIndex, int newTopRowLogicalIndex,
  3361. boolean scrolling, boolean recycledToOrFromTop,
  3362. boolean spacerHeightsChanged) {
  3363. /*
  3364. * There is no need to check if any rows preceding the source and
  3365. * target range need their positions adjusted, but rows below both
  3366. * may very well need it if spacer heights changed or rows got
  3367. * inserted or removed instead of just moved around.
  3368. *
  3369. * When scrolling to either direction all the rows already got
  3370. * processed by earlier stages, there are no unprocessed rows left
  3371. * either above or below.
  3372. */
  3373. if (!scrolling && (recycledToOrFromTop || spacerHeightsChanged)) {
  3374. int firstBelow;
  3375. if (adjustedVisualTargetIndex != visualTargetIndex) {
  3376. // rows moved down
  3377. firstBelow = adjustedVisualTargetIndex
  3378. + visualSourceRange.length();
  3379. } else {
  3380. // rows moved up
  3381. firstBelow = visualSourceRange.getEnd();
  3382. }
  3383. updateRowPositions(newTopRowLogicalIndex + firstBelow,
  3384. firstBelow, visualRowOrder.size() - firstBelow);
  3385. }
  3386. }
  3387. @Override
  3388. public void updateRowPositions(int index, int numberOfRows) {
  3389. Range visibleRowRange = getVisibleRowRange();
  3390. Range rangeToUpdate = Range.withLength(index, numberOfRows);
  3391. Range intersectingRange = visibleRowRange
  3392. .partitionWith(rangeToUpdate)[1];
  3393. if (intersectingRange.isEmpty()) {
  3394. // no overlap with the visual range, ignore the positioning
  3395. return;
  3396. }
  3397. int adjustedIndex = intersectingRange.getStart();
  3398. int adjustedVisualIndex = adjustedIndex - getTopRowLogicalIndex();
  3399. updateRowPositions(adjustedIndex, adjustedVisualIndex,
  3400. intersectingRange.length());
  3401. // make sure there is no unnecessary gap
  3402. adjustScrollPositionIfNeeded();
  3403. scroller.recalculateScrollbarsForVirtualViewport();
  3404. }
  3405. /**
  3406. * Re-calculates and updates the positions of rows and spacers within
  3407. * the given range. Doesn't touch the scroll positions.
  3408. *
  3409. * @param logicalIndex
  3410. * logical index of the first row to reposition
  3411. * @param visualIndex
  3412. * visual index of the first row to reposition
  3413. * @param numberOfRows
  3414. * the number of rows to reposition
  3415. */
  3416. private void updateRowPositions(int logicalIndex, int visualIndex,
  3417. int numberOfRows) {
  3418. double newRowTop = getRowTop(logicalIndex);
  3419. for (int i = 0; i < numberOfRows; ++i) {
  3420. TableRowElement tr = visualRowOrder.get(visualIndex + i);
  3421. setRowPosition(tr, 0, newRowTop);
  3422. newRowTop += getDefaultRowHeight();
  3423. SpacerContainer.SpacerImpl spacer = spacerContainer
  3424. .getSpacer(logicalIndex + i);
  3425. if (spacer != null) {
  3426. spacer.setPosition(0, newRowTop);
  3427. newRowTop += spacer.getHeight();
  3428. }
  3429. }
  3430. }
  3431. /**
  3432. * Checks whether there is an unexpected gap below the visible rows and
  3433. * adjusts the viewport if necessary.
  3434. */
  3435. private void adjustScrollPositionIfNeeded() {
  3436. double scrollTop = getScrollTop();
  3437. int firstBelowVisualRange = getTopRowLogicalIndex()
  3438. + visualRowOrder.size();
  3439. double gapBelow = scrollTop + getHeightOfSection()
  3440. - getRowTop(firstBelowVisualRange);
  3441. boolean bufferRowNeeded = gapBelow == 0
  3442. && firstBelowVisualRange < getRowCount();
  3443. if (scrollTop > 0 && (gapBelow > 0 || bufferRowNeeded)) {
  3444. /*
  3445. * This situation can be reached e.g. by removing a spacer.
  3446. * Scroll position must be adjusted accordingly but no more than
  3447. * there is room to scroll up. If a buffer row is needed make
  3448. * sure the last row ends up at least slightly below the
  3449. * viewport.
  3450. */
  3451. double adjustedGap = Math.max(gapBelow,
  3452. bufferRowNeeded ? 1 : 0);
  3453. double yDeltaScroll = Math.min(adjustedGap, scrollTop);
  3454. moveViewportAndContent(null, 0, 0, -yDeltaScroll);
  3455. }
  3456. }
  3457. /**
  3458. * Adjust the scroll position and move the contained rows.
  3459. * <p>
  3460. * The difference between using this method and simply scrolling is that
  3461. * this method "takes the rows and spacers with it" and renders them
  3462. * appropriately. The viewport may be scrolled any arbitrary amount, and
  3463. * the contents are moved appropriately, but always snapped into a
  3464. * plausible place.
  3465. * <p>
  3466. * <dl>
  3467. * <dt>Example 1</dt>
  3468. * <dd>An Escalator with default row height 20px. Adjusting the scroll
  3469. * position with 7.5px will move the viewport 7.5px down, but leave the
  3470. * row where it is.</dd>
  3471. * <dt>Example 2</dt>
  3472. * <dd>An Escalator with default row height 20px. Adjusting the scroll
  3473. * position with 27.5px will move the viewport 27.5px down, and place
  3474. * the row at 20px.</dd>
  3475. * </dl>
  3476. *
  3477. * @deprecated This method isn't used by Escalator anymore since Vaadin
  3478. * 8.9 and the general row handling logic has been
  3479. * rewritten, so attempting to call this method may lead to
  3480. * unexpected consequences. This method is likely to get
  3481. * removed soon.
  3482. * @param yDelta
  3483. * the delta of pixels by which to move the viewport and
  3484. * content. A positive value moves everything downwards,
  3485. * while a negative value moves everything upwards
  3486. */
  3487. @Deprecated
  3488. public void moveViewportAndContent(final double yDelta) {
  3489. if (yDelta == 0) {
  3490. return;
  3491. }
  3492. double newTop = tBodyScrollTop + yDelta;
  3493. verticalScrollbar.setScrollPos(newTop);
  3494. final double defaultRowHeight = getDefaultRowHeight();
  3495. double rowPxDelta = yDelta - (yDelta % defaultRowHeight);
  3496. int rowIndexDelta = (int) (yDelta / defaultRowHeight);
  3497. if (!WidgetUtil.pixelValuesEqual(rowPxDelta, 0)) {
  3498. Collection<SpacerContainer.SpacerImpl> spacers = spacerContainer
  3499. .getSpacersAfterPx(tBodyScrollTop,
  3500. SpacerInclusionStrategy.PARTIAL);
  3501. for (SpacerContainer.SpacerImpl spacer : spacers) {
  3502. spacer.setPositionDiff(0, rowPxDelta);
  3503. spacer.setRowIndex(spacer.getRow() + rowIndexDelta);
  3504. }
  3505. for (TableRowElement tr : visualRowOrder) {
  3506. setRowPosition(tr, 0, getRowTop(tr) + rowPxDelta);
  3507. }
  3508. }
  3509. setBodyScrollPosition(tBodyScrollLeft, newTop);
  3510. }
  3511. /**
  3512. * Move rows, spacers, and/or viewport up or down. For rows and spacers
  3513. * either everything within visual range is affected (index
  3514. * {@code null}) or only those from the given row index forward.
  3515. * <p>
  3516. * This method does not update spacer indexes.
  3517. *
  3518. * @param index
  3519. * the logical index from which forward the rows and spacers
  3520. * should be updated, or {@code null} if all of them
  3521. * @param yDeltaRows
  3522. * how much rows should be shifted in pixels
  3523. * @param yDeltaSpacers
  3524. * how much spacers should be shifted in pixels
  3525. * @param yDeltaScroll
  3526. * how much scroll position should be shifted in pixels
  3527. */
  3528. private void moveViewportAndContent(Integer index,
  3529. final double yDeltaRows, final double yDeltaSpacers,
  3530. final double yDeltaScroll) {
  3531. if (!WidgetUtil.pixelValuesEqual(yDeltaScroll, 0d)) {
  3532. double newTop = tBodyScrollTop + yDeltaScroll;
  3533. verticalScrollbar.setScrollPos(newTop);
  3534. setBodyScrollPosition(tBodyScrollLeft, newTop);
  3535. }
  3536. if (!WidgetUtil.pixelValuesEqual(yDeltaSpacers, 0d)) {
  3537. Collection<SpacerContainer.SpacerImpl> spacers;
  3538. if (index == null) {
  3539. spacers = spacerContainer.getSpacersAfterPx(tBodyScrollTop,
  3540. SpacerInclusionStrategy.PARTIAL);
  3541. } else {
  3542. spacers = spacerContainer.getSpacersForRowAndAfter(index);
  3543. }
  3544. for (SpacerContainer.SpacerImpl spacer : spacers) {
  3545. spacer.setPositionDiff(0, yDeltaSpacers);
  3546. }
  3547. }
  3548. if (!WidgetUtil.pixelValuesEqual(yDeltaRows, 0d)) {
  3549. if (index == null) {
  3550. // move all visible rows to the desired direction
  3551. for (TableRowElement tr : visualRowOrder) {
  3552. setRowPosition(tr, 0, getRowTop(tr) + yDeltaRows);
  3553. }
  3554. } else {
  3555. // move all visible rows, including the index row, to the
  3556. // desired direction
  3557. shiftRowPositions(index - 1, yDeltaRows);
  3558. }
  3559. }
  3560. }
  3561. /**
  3562. * Adds new physical escalator rows to the DOM at the given visual index
  3563. * if there's still a need for more escalator rows.
  3564. * <p>
  3565. * If Escalator already is at (or beyond) max capacity, this method does
  3566. * nothing to the DOM.
  3567. * <p>
  3568. * Calling this method repositions all the rows and spacers below the
  3569. * insertion point.
  3570. *
  3571. * @param visualIndex
  3572. * the index at which to add new escalator rows to DOM
  3573. * @param logicalIndex
  3574. * the logical index that corresponds with the first new
  3575. * escalator row, should usually be the same as visual index
  3576. * because there is still need for new rows, but this is not
  3577. * always the case e.g. if row height is changed
  3578. * @param numberOfRows
  3579. * the number of rows to add at <code>index</code>
  3580. * @return a list of the added rows
  3581. */
  3582. private List<TableRowElement> fillAndPopulateEscalatorRowsIfNeeded(
  3583. final int visualIndex, final int logicalIndex,
  3584. final int numberOfRows) {
  3585. /*
  3586. * We want to maintain enough rows to fill the entire viewport even
  3587. * if their spacers have no height. If their spacers do have height
  3588. * some of these rows may end up outside of the viewport, but that's
  3589. * ok.
  3590. */
  3591. final int escalatorRowsStillFit = getMaxVisibleRowCount()
  3592. - getDomRowCount();
  3593. final int escalatorRowsNeeded = Math.min(numberOfRows,
  3594. escalatorRowsStillFit);
  3595. if (escalatorRowsNeeded > 0) {
  3596. int rowsBeforeAddition = visualRowOrder.size();
  3597. // this is AbstractRowContainer method and not easily overridden
  3598. // to consider logical indexes separately from visual indexes,
  3599. // so as a workaround we create the rows as if those two were
  3600. // the same and then update the contents if needed
  3601. final List<TableRowElement> addedRows = paintInsertStaticRows(
  3602. visualIndex, escalatorRowsNeeded);
  3603. visualRowOrder.addAll(visualIndex, addedRows);
  3604. if (visualIndex != logicalIndex) {
  3605. // row got populated with wrong contents, need to update
  3606. int adjustedLogicalIndex = 0;
  3607. if (visualIndex == 0) {
  3608. // added to the beginning of visual range, use the
  3609. // end of insertion range because the beginning might
  3610. // not fit completely
  3611. adjustedLogicalIndex = logicalIndex + numberOfRows
  3612. - addedRows.size();
  3613. } else {
  3614. // added anywhere else, use the beginning of
  3615. // insertion range and the rest of the rows get
  3616. // recycled below if there is room for them
  3617. adjustedLogicalIndex = logicalIndex;
  3618. }
  3619. for (int i = 0; i < addedRows.size(); ++i) {
  3620. TableRowElement tr = addedRows.get(i);
  3621. refreshRow(tr, adjustedLogicalIndex + i);
  3622. }
  3623. }
  3624. // if something is getting inserted instead of just being
  3625. // brought to visual range, the rows below the insertion point
  3626. // need to have their spacer indexes updated accordingly
  3627. if (logicalIndex >= getTopRowLogicalIndex()
  3628. && visualIndex < rowsBeforeAddition) {
  3629. spacerContainer.updateSpacerIndexesForRowAndAfter(
  3630. logicalIndex, getRowCount(), addedRows.size());
  3631. }
  3632. // update the positions of the added rows and the rows below
  3633. // them
  3634. // TODO: this can lead to moving things around twice in case
  3635. // some rows didn't get new dom rows (e.g. when expanding a
  3636. // TreeGrid node with more children than can fit within the max
  3637. // visual range size), consider moving this update elsewhere
  3638. double rowTop = getRowTop(logicalIndex);
  3639. for (int i = visualIndex; i < visualRowOrder.size(); i++) {
  3640. final TableRowElement tr = visualRowOrder.get(i);
  3641. setRowPosition(tr, 0, rowTop);
  3642. rowTop += getDefaultRowHeight();
  3643. SpacerContainer.SpacerImpl spacer = spacerContainer
  3644. .getSpacer(logicalIndex - visualIndex + i);
  3645. if (spacer != null) {
  3646. spacer.setPosition(0, rowTop);
  3647. rowTop += spacer.getHeight();
  3648. }
  3649. }
  3650. // Execute the registered callback function for newly created
  3651. // rows
  3652. Optional.ofNullable(newEscalatorRowCallback)
  3653. .ifPresent(callback -> callback.accept(addedRows));
  3654. return addedRows;
  3655. } else {
  3656. return Collections.emptyList();
  3657. }
  3658. }
  3659. private int getMaxVisibleRowCount() {
  3660. double heightOfSection = getHeightOfSection();
  3661. // By including the possibly shown scrollbar height, we get a
  3662. // consistent count and do not add/remove rows whenever a scrollbar
  3663. // is shown. Make sure that two extra rows are included for
  3664. // assisting with tab navigation on both sides of the viewport.
  3665. heightOfSection += horizontalScrollbarDeco.getOffsetHeight();
  3666. double defaultRowHeight = getDefaultRowHeight();
  3667. final int maxVisibleRowCount = (int) Math
  3668. .ceil(heightOfSection / defaultRowHeight) + 2;
  3669. /*
  3670. * maxVisibleRowCount can become negative if the headers and footers
  3671. * start to overlap. This is a crazy situation, but Vaadin blinks
  3672. * the components a lot, so it's feasible.
  3673. */
  3674. return Math.max(0, maxVisibleRowCount);
  3675. }
  3676. @Override
  3677. protected void paintRemoveRows(final int index,
  3678. final int numberOfRows) {
  3679. if (numberOfRows == 0) {
  3680. return;
  3681. }
  3682. /*
  3683. * NOTE: this method handles and manipulates logical, visual, and
  3684. * physical indexes a lot. If you don't remember what those mean and
  3685. * how they relate to each other, see the top of this class for
  3686. * Maintenance Notes.
  3687. *
  3688. * At the beginning of this method the logical index of the data
  3689. * provider has already been updated to include the new rows, but
  3690. * visual and physical indexes have not, nor has the spacer indexing
  3691. * been updated, and the topRowLogicalIndex may be out of date as
  3692. * well.
  3693. */
  3694. // logical index of the first old row, also the difference between
  3695. // logical index and visual index before any rows have been removed
  3696. final int oldTopRowLogicalIndex = getTopRowLogicalIndex();
  3697. // length of the visual range before anything gets removed
  3698. final int oldVisualRangeLength = visualRowOrder.size();
  3699. // logical range of the removed rows
  3700. final Range removedRowsLogicalRange = Range.withLength(index,
  3701. numberOfRows);
  3702. // check which parts of the removed range fall within or beyond the
  3703. // visual range
  3704. final Range[] partitions = removedRowsLogicalRange
  3705. .partitionWith(Range.withLength(oldTopRowLogicalIndex,
  3706. oldVisualRangeLength));
  3707. final Range removedLogicalAbove = partitions[0];
  3708. final Range removedLogicalBelow = partitions[2];
  3709. final Range removedLogicalWithin = partitions[1];
  3710. if (removedLogicalBelow.length() == numberOfRows) {
  3711. /*
  3712. * Rows were removed entirely from below the visual range. No
  3713. * rows to recycle or scroll position to adjust, just need to
  3714. * recalculate scrollbar height. No need to touch the spacer
  3715. * indexing or the physical index.
  3716. */
  3717. scroller.recalculateScrollbarsForVirtualViewport();
  3718. // Visual range contents remain the same, no need to fire a
  3719. // RowVisibilityChangeEvent.
  3720. } else if (removedLogicalAbove.length() == numberOfRows) {
  3721. /*
  3722. * Rows were removed entirely from above the visual range. No
  3723. * rows to recycle, just need to update the spacer indexing and
  3724. * the content positions. No need to touch the physical index.
  3725. */
  3726. // update the logical indexes of remaining spacers
  3727. spacerContainer.updateSpacerIndexesForRowAndAfter(
  3728. oldTopRowLogicalIndex,
  3729. oldTopRowLogicalIndex + oldVisualRangeLength,
  3730. -numberOfRows);
  3731. // default height of a single row
  3732. final double defaultRowHeight = getDefaultRowHeight();
  3733. // how much viewport, rows, and spacers should be shifted based
  3734. // on the removed rows, assume there were no spacers to remove
  3735. final double yDelta = numberOfRows * defaultRowHeight;
  3736. // shift everything up
  3737. moveViewportAndContent(null, -yDelta, -yDelta, -yDelta);
  3738. // update the top row logical index according to any removed
  3739. // rows
  3740. updateTopRowLogicalIndex(-numberOfRows);
  3741. // update scrollbar
  3742. scroller.recalculateScrollbarsForVirtualViewport();
  3743. // Visual range contents remain the same, no need to fire a
  3744. // RowVisibilityChangeEvent.
  3745. } else {
  3746. /*
  3747. * Rows are being removed at least partially from within the
  3748. * visual range. This is where things get tricky. We might have
  3749. * to scroll up or down or nowhere at all, depending on the
  3750. * situation.
  3751. */
  3752. // Visual range contents changed, RowVisibilityChangeEvent will
  3753. // be triggered within this method
  3754. paintRemoveRowsWithinVisualRange(index, numberOfRows,
  3755. oldTopRowLogicalIndex, oldVisualRangeLength,
  3756. removedLogicalAbove.length(), removedLogicalWithin);
  3757. }
  3758. }
  3759. /**
  3760. * Row removal handling for {@link #paintRemoveRows(int, int)} when the
  3761. * removed range intersects the visual range at least partially.
  3762. * <p>
  3763. * NOTE: This method should not be called directly from anywhere else.
  3764. *
  3765. * @param index
  3766. * @param numberOfRows
  3767. * @param oldTopRowLogicalIndex
  3768. * @param oldVisualRangeLength
  3769. * @param removedAboveLength
  3770. * @param removedLogicalWithin
  3771. */
  3772. private void paintRemoveRowsWithinVisualRange(int index,
  3773. int numberOfRows, int oldTopRowLogicalIndex,
  3774. int oldVisualRangeLength, int removedAboveLength,
  3775. Range removedLogicalWithin) {
  3776. /*
  3777. * Calculating where the visual range should start after the
  3778. * removals is not entirely trivial.
  3779. *
  3780. * Initially, any rows removed from within the visual range won't
  3781. * affect the top index, even if they are removed from the
  3782. * beginning, as the rows are also removed from the logical index.
  3783. * Likewise we don't need to care about rows removed from below the
  3784. * visual range. On the other hand, any rows removed from above the
  3785. * visual range do shift the index down.
  3786. *
  3787. * However, in all of these cases, if there aren't enough rows below
  3788. * the visual range to replace the content removed from within the
  3789. * visual range, more rows need to be brought in from above the old
  3790. * visual range in turn. This shifts the index down even further.
  3791. */
  3792. // scroll position before any rows or spacers are removed
  3793. double scrollTop = getScrollTop();
  3794. Range removedVisualWithin = convertToVisual(removedLogicalWithin);
  3795. int remainingVisualRangeRowCount = visualRowOrder.size()
  3796. - removedVisualWithin.length();
  3797. int newTopRowLogicalIndex = oldTopRowLogicalIndex
  3798. - removedAboveLength;
  3799. int rowsToIncludeFromBelow = Math.min(
  3800. getRowCount() - newTopRowLogicalIndex
  3801. - remainingVisualRangeRowCount,
  3802. removedLogicalWithin.length());
  3803. int rowsToIncludeFromAbove = removedLogicalWithin.length()
  3804. - rowsToIncludeFromBelow;
  3805. int rowsToRemoveFromDom = 0;
  3806. if (rowsToIncludeFromAbove > 0) {
  3807. // don't try to bring in more rows than exist, it's possible
  3808. // to remove enough rows that visual range won't be full
  3809. // anymore
  3810. rowsToRemoveFromDom = Math
  3811. .max(rowsToIncludeFromAbove - newTopRowLogicalIndex, 0);
  3812. rowsToIncludeFromAbove -= rowsToRemoveFromDom;
  3813. newTopRowLogicalIndex -= rowsToIncludeFromAbove;
  3814. }
  3815. int visualIndexToRemove = Math.max(index - oldTopRowLogicalIndex,
  3816. 0);
  3817. // remove extra dom rows and their spacers if any
  3818. double removedFromDomSpacerHeights = 0d;
  3819. if (rowsToRemoveFromDom > 0) {
  3820. for (int i = 0; i < rowsToRemoveFromDom; ++i) {
  3821. TableRowElement tr = visualRowOrder
  3822. .remove(visualIndexToRemove);
  3823. // logical index of this row before anything got removed
  3824. int logicalRowIndex = oldTopRowLogicalIndex
  3825. + visualIndexToRemove + i;
  3826. double spacerHeight = spacerContainer
  3827. .getSpacerHeight(logicalRowIndex);
  3828. removedFromDomSpacerHeights += spacerHeight;
  3829. spacerContainer.removeSpacer(logicalRowIndex);
  3830. paintRemoveRow(tr, removedVisualWithin.getStart());
  3831. removeRowPosition(tr);
  3832. }
  3833. // update the associated row indexes for remaining spacers,
  3834. // even for those rows that are going to get recycled
  3835. spacerContainer.updateSpacerIndexesForRowAndAfter(
  3836. oldTopRowLogicalIndex + visualIndexToRemove
  3837. + rowsToRemoveFromDom,
  3838. oldTopRowLogicalIndex + oldVisualRangeLength,
  3839. -rowsToRemoveFromDom);
  3840. }
  3841. // add new content from below visual range, if there is any
  3842. if (rowsToIncludeFromBelow > 0) {
  3843. // removed rows are recycled to just below the old visual
  3844. // range, calculate the logical index of the insertion
  3845. // point that is just below the existing rows, taking into
  3846. // account that the indexing has changed with the removal
  3847. int firstBelow = newTopRowLogicalIndex + rowsToIncludeFromAbove
  3848. + remainingVisualRangeRowCount;
  3849. moveAndUpdateEscalatorRows(
  3850. Range.withLength(visualIndexToRemove,
  3851. rowsToIncludeFromBelow),
  3852. visualRowOrder.size(), firstBelow);
  3853. }
  3854. // add new content from above visual range, if there is any
  3855. // -- this is left last because most of the time it isn't even
  3856. // needed
  3857. if (rowsToIncludeFromAbove > 0) {
  3858. moveAndUpdateEscalatorRows(
  3859. Range.withLength(visualIndexToRemove,
  3860. rowsToIncludeFromAbove),
  3861. 0, newTopRowLogicalIndex);
  3862. }
  3863. // recycling updates all relevant row and spacer positions but
  3864. // if we only removed DOM rows and didn't recycle any we still
  3865. // need to shift up the rows below the removal point
  3866. if (rowsToIncludeFromAbove <= 0 && rowsToIncludeFromBelow <= 0) {
  3867. // update the positions for the rows and spacers below the
  3868. // removed ones, assume there is no need to update scroll
  3869. // position since the final check adjusts that if needed
  3870. double yDelta = numberOfRows * getDefaultRowHeight()
  3871. + removedFromDomSpacerHeights;
  3872. moveViewportAndContent(
  3873. newTopRowLogicalIndex + visualIndexToRemove, -yDelta,
  3874. -yDelta, 0);
  3875. }
  3876. setTopRowLogicalIndex(newTopRowLogicalIndex);
  3877. scroller.recalculateScrollbarsForVirtualViewport();
  3878. // calling this method also triggers adding new spacers to the
  3879. // recycled rows, if any are needed
  3880. fireRowVisibilityChangeEvent();
  3881. // populating the spacers might take a while, delay calculations
  3882. // or the viewport might get adjusted too high
  3883. Scheduler.get().scheduleFinally(() -> {
  3884. // make sure there isn't a gap at the bottom after removal
  3885. // and adjust the viewport if there is
  3886. // FIXME: this should be doable with
  3887. // adjustScrollPositionIfNeeded() but it uses current
  3888. // scrollTop, which may have ended in wrong position and
  3889. // results in assuming too big gap and consequently
  3890. // scrolling up too much
  3891. double extraSpaceAtBottom = scrollTop + getHeightOfSection()
  3892. - getRowTop(getTopRowLogicalIndex()
  3893. + visualRowOrder.size());
  3894. if (extraSpaceAtBottom > 0 && scrollTop > 0) {
  3895. // we need to move the viewport up to adjust, while the
  3896. // rows and spacers can remain where they are
  3897. double yDeltaScroll = Math.min(extraSpaceAtBottom,
  3898. scrollTop);
  3899. moveViewportAndContent(null, 0, 0, -yDeltaScroll);
  3900. }
  3901. });
  3902. // update physical index
  3903. sortDomElements();
  3904. }
  3905. @Override
  3906. public int getLogicalRowIndex(final TableRowElement tr) {
  3907. assert tr
  3908. .getParentNode() == root : "The given element isn't a row element in the body";
  3909. int internalIndex = visualRowOrder.indexOf(tr);
  3910. return getTopRowLogicalIndex() + internalIndex;
  3911. }
  3912. @Override
  3913. protected void recalculateSectionHeight() {
  3914. // NOOP for body, since it doesn't make any sense.
  3915. }
  3916. /**
  3917. * Adjusts the row index and number to be relevant for the current
  3918. * virtual viewport.
  3919. * <p>
  3920. * It converts a logical range of rows index to the matching visual
  3921. * range, truncating the resulting range with the viewport.
  3922. * <p>
  3923. * <ul>
  3924. * <li>Escalator contains logical rows 0..100
  3925. * <li>Current viewport showing logical rows 20..29
  3926. * <li>convertToVisual([20..29]) &rarr; [0..9]
  3927. * <li>convertToVisual([15..24]) &rarr; [0..4]
  3928. * <li>convertToVisual([25..29]) &rarr; [5..9]
  3929. * <li>convertToVisual([26..39]) &rarr; [6..9]
  3930. * <li>convertToVisual([0..5]) &rarr; [0..-1] <em>(empty)</em>
  3931. * <li>convertToVisual([35..1]) &rarr; [0..-1] <em>(empty)</em>
  3932. * <li>convertToVisual([0..100]) &rarr; [0..9]
  3933. * </ul>
  3934. *
  3935. * @return a logical range converted to a visual range, truncated to the
  3936. * current viewport. The first visual row has the index 0.
  3937. */
  3938. private Range convertToVisual(final Range logicalRange) {
  3939. if (logicalRange.isEmpty()) {
  3940. return logicalRange;
  3941. } else if (visualRowOrder.isEmpty()) {
  3942. // empty range
  3943. return Range.withLength(0, 0);
  3944. }
  3945. final int currentTopRowIndex = getTopRowLogicalIndex();
  3946. final Range[] partitions = logicalRange
  3947. .partitionWith(getVisibleRowRange());
  3948. final Range insideRange = partitions[1];
  3949. return insideRange.offsetBy(-currentTopRowIndex);
  3950. }
  3951. @Override
  3952. protected String getCellElementTagName() {
  3953. return "td";
  3954. }
  3955. @Override
  3956. protected double getHeightOfSection() {
  3957. final int tableHeight = tableWrapper.getOffsetHeight();
  3958. final double footerHeight = footer.getHeightOfSection();
  3959. final double headerHeight = header.getHeightOfSection();
  3960. double heightOfSection = tableHeight - footerHeight - headerHeight;
  3961. return Math.max(0, heightOfSection);
  3962. }
  3963. @Override
  3964. protected void refreshCells(Range logicalRowRange, Range colRange) {
  3965. Profiler.enter("Escalator.BodyRowContainer.refreshRows");
  3966. final Range visualRange = convertToVisual(logicalRowRange);
  3967. if (!visualRange.isEmpty()) {
  3968. final int firstLogicalRowIndex = getLogicalRowIndex(
  3969. visualRowOrder.getFirst());
  3970. for (int rowNumber = visualRange
  3971. .getStart(); rowNumber < visualRange
  3972. .getEnd(); rowNumber++) {
  3973. refreshRow(visualRowOrder.get(rowNumber),
  3974. firstLogicalRowIndex + rowNumber, colRange);
  3975. }
  3976. }
  3977. Profiler.leave("Escalator.BodyRowContainer.refreshRows");
  3978. }
  3979. @Override
  3980. protected TableRowElement getTrByVisualIndex(final int index)
  3981. throws IndexOutOfBoundsException {
  3982. if (index >= 0 && index < visualRowOrder.size()) {
  3983. return visualRowOrder.get(index);
  3984. } else {
  3985. throw new IndexOutOfBoundsException(
  3986. "No such visual index: " + index);
  3987. }
  3988. }
  3989. @Override
  3990. public TableRowElement getRowElement(int index) {
  3991. if (index < 0 || index >= getRowCount()) {
  3992. throw new IndexOutOfBoundsException(
  3993. "No such logical index: " + index);
  3994. }
  3995. int visualIndex = index
  3996. - getLogicalRowIndex(visualRowOrder.getFirst());
  3997. if (visualIndex >= 0 && visualIndex < visualRowOrder.size()) {
  3998. return super.getRowElement(visualIndex);
  3999. } else {
  4000. throw new IllegalStateException("Row with logical index "
  4001. + index + " is currently not available in the DOM");
  4002. }
  4003. }
  4004. private void setBodyScrollPosition(final double scrollLeft,
  4005. final double scrollTop) {
  4006. tBodyScrollLeft = scrollLeft;
  4007. tBodyScrollTop = scrollTop;
  4008. position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop);
  4009. position.set(spacerDecoContainer, 0, -tBodyScrollTop);
  4010. }
  4011. /**
  4012. * Make sure that there is a correct amount of escalator rows: Add more
  4013. * if needed, or remove any superfluous ones.
  4014. * <p>
  4015. * This method should be called when e.g. the height of the Escalator
  4016. * changes.
  4017. * <p>
  4018. * <em>Note:</em> This method will make sure that the escalator rows are
  4019. * placed in the proper places. By default new rows are added below, but
  4020. * if the content is scrolled down, the rows are populated on top
  4021. * instead.
  4022. */
  4023. public void verifyEscalatorCount() {
  4024. /*
  4025. * This method indeed has a smell very similar to paintRemoveRows
  4026. * and paintInsertRows.
  4027. *
  4028. * Unfortunately, the code of those can't trivially be shared, since
  4029. * there are some slight differences in the respective
  4030. * responsibilities. The "paint" methods fake the addition and
  4031. * removal of rows, and make sure to either push existing data out
  4032. * of view, or draw new data into view. Only in some special cases
  4033. * will the DOM element count change.
  4034. *
  4035. * This method, however, has the explicit responsibility to verify
  4036. * that when "something" happens, we still have the correct amount
  4037. * of escalator rows in the DOM, and if not, we make sure to modify
  4038. * that count. Only in some special cases do we need to take into
  4039. * account other things than simply modifying the DOM element count.
  4040. */
  4041. Profiler.enter("Escalator.BodyRowContainer.verifyEscalatorCount");
  4042. if (!isAttached()) {
  4043. return;
  4044. }
  4045. int oldTopRowLogicalIndex = getTopRowLogicalIndex();
  4046. int oldVisualRangeLength = visualRowOrder.size();
  4047. final int maxVisibleRowCount = getMaxVisibleRowCount();
  4048. final int neededEscalatorRows = Math.min(maxVisibleRowCount,
  4049. body.getRowCount());
  4050. final int rowDiff = neededEscalatorRows - oldVisualRangeLength;
  4051. if (rowDiff > 0) {
  4052. // more rows are needed
  4053. // calculate the indexes for adding rows below the last row of
  4054. // the visual range
  4055. final int visualTargetIndex = oldVisualRangeLength;
  4056. final int logicalTargetIndex;
  4057. if (!visualRowOrder.isEmpty()) {
  4058. logicalTargetIndex = oldTopRowLogicalIndex
  4059. + visualTargetIndex;
  4060. } else {
  4061. logicalTargetIndex = 0;
  4062. }
  4063. // prioritise adding to the bottom so that there's less chance
  4064. // for a gap if a details row is later closed (e.g. by user)
  4065. final int addToBottom = Math.min(rowDiff,
  4066. getRowCount() - logicalTargetIndex);
  4067. final int addToTop = Math.max(rowDiff - addToBottom, 0);
  4068. if (addToTop > 0) {
  4069. fillAndPopulateEscalatorRowsIfNeeded(0,
  4070. oldTopRowLogicalIndex - addToTop, addToTop);
  4071. updateTopRowLogicalIndex(-addToTop);
  4072. }
  4073. if (addToBottom > 0) {
  4074. // take into account that rows may have got added to top as
  4075. // well, affects visual but not logical indexing
  4076. fillAndPopulateEscalatorRowsIfNeeded(
  4077. visualTargetIndex + addToTop, logicalTargetIndex,
  4078. addToBottom);
  4079. // adding new rows due to resizing may have created a gap in
  4080. // the middle, check whether the existing rows need moving
  4081. double rowTop = getRowTop(oldTopRowLogicalIndex);
  4082. if (rowTop > getRowTop(visualRowOrder.get(addToTop))) {
  4083. for (int i = addToTop; i < visualTargetIndex; i++) {
  4084. final TableRowElement tr = visualRowOrder.get(i);
  4085. setRowPosition(tr, 0, rowTop);
  4086. rowTop += getDefaultRowHeight();
  4087. SpacerContainer.SpacerImpl spacer = spacerContainer
  4088. .getSpacer(oldTopRowLogicalIndex + i);
  4089. if (spacer != null) {
  4090. spacer.setPosition(0, rowTop);
  4091. rowTop += spacer.getHeight();
  4092. }
  4093. }
  4094. }
  4095. }
  4096. } else if (rowDiff < 0) {
  4097. // rows need to be removed
  4098. // prioritise removing rows from above the viewport as they are
  4099. // less likely to be needed in a hurry -- the rows below are
  4100. // more likely to slide into view when spacer contents are
  4101. // updated
  4102. // top of visible area before any rows are actually added
  4103. double scrollTop = getScrollTop();
  4104. // visual index of the first actually visible row, including
  4105. // spacer
  4106. int oldFirstVisibleVisualIndex = -1;
  4107. ListIterator<TableRowElement> iter = visualRowOrder
  4108. .listIterator(0);
  4109. for (int i = 0; i < visualRowOrder.size(); ++i) {
  4110. if (positions.getTop(iter.next()) > scrollTop) {
  4111. break;
  4112. }
  4113. oldFirstVisibleVisualIndex = i;
  4114. }
  4115. int rowsToRemoveFromAbove = Math.max(0, Math
  4116. .min(Math.abs(rowDiff), oldFirstVisibleVisualIndex));
  4117. boolean spacersRemovedFromAbove = false;
  4118. if (rowsToRemoveFromAbove > 0) {
  4119. double initialSpacerHeightSum = spacerContainer
  4120. .getSpacerHeightsSum();
  4121. iter = visualRowOrder.listIterator(0);
  4122. for (int i = 0; i < rowsToRemoveFromAbove; ++i) {
  4123. final Element first = iter.next();
  4124. first.removeFromParent();
  4125. iter.remove();
  4126. spacerContainer.removeSpacer(oldTopRowLogicalIndex + i);
  4127. }
  4128. spacersRemovedFromAbove = initialSpacerHeightSum != spacerContainer
  4129. .getSpacerHeightsSum();
  4130. }
  4131. // if there weren't enough rows above, remove the rest from
  4132. // below
  4133. int rowsToRemoveFromBelow = Math.abs(rowDiff)
  4134. - rowsToRemoveFromAbove;
  4135. if (rowsToRemoveFromBelow > 0) {
  4136. iter = visualRowOrder.listIterator(visualRowOrder.size());
  4137. for (int i = 1; i <= rowsToRemoveFromBelow; ++i) {
  4138. final Element last = iter.previous();
  4139. last.removeFromParent();
  4140. iter.remove();
  4141. spacerContainer.removeSpacer(oldTopRowLogicalIndex
  4142. + oldVisualRangeLength - i);
  4143. }
  4144. }
  4145. updateTopRowLogicalIndex(rowsToRemoveFromAbove);
  4146. if (spacersRemovedFromAbove) {
  4147. updateRowPositions(oldTopRowLogicalIndex, 0,
  4148. visualRowOrder.size());
  4149. }
  4150. // removing rows might cause a gap at the bottom
  4151. adjustScrollPositionIfNeeded();
  4152. }
  4153. if (rowDiff != 0) {
  4154. scroller.recalculateScrollbarsForVirtualViewport();
  4155. fireRowVisibilityChangeEvent();
  4156. }
  4157. Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount");
  4158. }
  4159. @Override
  4160. protected void reapplyDefaultRowHeights() {
  4161. if (visualRowOrder.isEmpty()) {
  4162. return;
  4163. }
  4164. Profiler.enter(
  4165. "Escalator.BodyRowContainer.reapplyDefaultRowHeights");
  4166. double spacerHeightsAboveViewport = spacerContainer
  4167. .getSpacerHeightsSumUntilPx(
  4168. verticalScrollbar.getScrollPos());
  4169. double allSpacerHeights = spacerContainer.getSpacerHeightsSum();
  4170. /* step 1: resize and reposition rows */
  4171. // there should be no spacers above the visual range
  4172. double spacerHeights = 0;
  4173. for (int i = 0; i < visualRowOrder.size(); i++) {
  4174. TableRowElement tr = visualRowOrder.get(i);
  4175. reapplyRowHeight(tr, getDefaultRowHeight());
  4176. final int logicalIndex = getTopRowLogicalIndex() + i;
  4177. double y = logicalIndex * getDefaultRowHeight() + spacerHeights;
  4178. setRowPosition(tr, 0, y);
  4179. SpacerContainer.SpacerImpl spacer = spacerContainer
  4180. .getSpacer(logicalIndex);
  4181. if (spacer != null) {
  4182. spacer.setPosition(0, y + getDefaultRowHeight());
  4183. spacerHeights += spacer.getHeight();
  4184. }
  4185. }
  4186. /*
  4187. * step 2: move scrollbar so that it corresponds to its previous
  4188. * place
  4189. */
  4190. // scrollRatio has to be calculated without spacers for it to be
  4191. // comparable between different row heights
  4192. double scrollRatio = (verticalScrollbar.getScrollPos()
  4193. - spacerHeightsAboveViewport)
  4194. / (verticalScrollbar.getScrollSize() - allSpacerHeights);
  4195. scroller.recalculateScrollbarsForVirtualViewport();
  4196. // spacer heights have to be added back for setting new scrollPos
  4197. verticalScrollbar.setScrollPos(
  4198. (int) ((getDefaultRowHeight() * getRowCount() * scrollRatio)
  4199. + spacerHeightsAboveViewport));
  4200. setBodyScrollPosition(horizontalScrollbar.getScrollPos(),
  4201. verticalScrollbar.getScrollPos());
  4202. scroller.onScroll();
  4203. /*
  4204. * step 3: make sure we have the correct amount of escalator rows.
  4205. */
  4206. verifyEscalatorCount();
  4207. Profiler.leave(
  4208. "Escalator.BodyRowContainer.reapplyDefaultRowHeights");
  4209. }
  4210. /**
  4211. * Sorts the rows in the DOM to correspond to the visual order.
  4212. *
  4213. * @see #visualRowOrder
  4214. */
  4215. private void sortDomElements() {
  4216. final String profilingName = "Escalator.BodyRowContainer.sortDomElements";
  4217. Profiler.enter(profilingName);
  4218. /*
  4219. * Focus is lost from an element if that DOM element is (or any of
  4220. * its parents are) removed from the document. Therefore, we sort
  4221. * everything around that row instead.
  4222. */
  4223. final TableRowElement focusedRow = getRowWithFocus();
  4224. if (focusedRow != null) {
  4225. assert focusedRow
  4226. .getParentElement() == root : "Trying to sort around a row that doesn't exist in body";
  4227. assert visualRowOrder.contains(focusedRow)
  4228. || body.spacerContainer.isSpacer(
  4229. focusedRow) : "Trying to sort around a row that doesn't exist in visualRowOrder or is not a spacer.";
  4230. }
  4231. /*
  4232. * Two cases handled simultaneously:
  4233. *
  4234. * 1) No focus on rows. We iterate visualRowOrder backwards, and
  4235. * take the respective element in the DOM, and place it as the first
  4236. * child in the body element. Then we take the next-to-last from
  4237. * visualRowOrder, and put that first, pushing the previous row as
  4238. * the second child. And so on...
  4239. *
  4240. * 2) Focus on some row within Escalator body. Again, we iterate
  4241. * visualRowOrder backwards. This time, we use the focused row as a
  4242. * pivot: Instead of placing rows from the bottom of visualRowOrder
  4243. * and placing it first, we place it underneath the focused row.
  4244. * Once we hit the focused row, we don't move it (to not reset
  4245. * focus) but change sorting mode. After that, we place all rows as
  4246. * the first child.
  4247. */
  4248. List<TableRowElement> orderedBodyRows = new ArrayList<>(
  4249. visualRowOrder);
  4250. Map<Integer, SpacerContainer.SpacerImpl> spacers = body.spacerContainer
  4251. .getSpacers();
  4252. /*
  4253. * Start at -1 to include a spacer that is rendered above the
  4254. * viewport, but its parent row is still not shown
  4255. */
  4256. int addedSpacers = 0;
  4257. for (int i = -1; i < visualRowOrder.size(); i++) {
  4258. SpacerContainer.SpacerImpl spacer = spacers
  4259. .remove(Integer.valueOf(getTopRowLogicalIndex() + i));
  4260. if (spacer != null) {
  4261. orderedBodyRows.add(i + 1 + addedSpacers,
  4262. spacer.getRootElement());
  4263. spacer.show();
  4264. ++addedSpacers;
  4265. }
  4266. }
  4267. /*
  4268. * At this point, invisible spacers aren't reordered, so their
  4269. * position in the DOM will remain undefined.
  4270. */
  4271. // If a spacer was not reordered, it means that it's out of visual
  4272. // range. This should never happen with default Grid implementations
  4273. // but it's possible on an extended Escalator.
  4274. for (SpacerContainer.SpacerImpl unmovedSpacer : spacers.values()) {
  4275. unmovedSpacer.hide();
  4276. }
  4277. /*
  4278. * If we have a focused row, start in the mode where we put
  4279. * everything underneath that row. Otherwise, all rows are placed as
  4280. * first child.
  4281. */
  4282. boolean insertFirst = (focusedRow == null);
  4283. final ListIterator<TableRowElement> i = orderedBodyRows
  4284. .listIterator(orderedBodyRows.size());
  4285. while (i.hasPrevious()) {
  4286. TableRowElement tr = i.previous();
  4287. if (tr == focusedRow) {
  4288. insertFirst = true;
  4289. } else if (insertFirst) {
  4290. // remove row explicitly to work around an IE11 bug (#9850)
  4291. if (BrowserInfo.get().isIE11()
  4292. && tr.equals(root.getFirstChildElement())) {
  4293. root.removeChild(tr);
  4294. }
  4295. root.insertFirst(tr);
  4296. } else {
  4297. root.insertAfter(tr, focusedRow);
  4298. }
  4299. }
  4300. Profiler.leave(profilingName);
  4301. }
  4302. /**
  4303. * Get the {@literal <tbody>} row that contains (or has) focus.
  4304. *
  4305. * @return The {@literal <tbody>} row that contains a focused DOM
  4306. * element, or <code>null</code> if focus is outside of a body
  4307. * row.
  4308. */
  4309. private TableRowElement getRowWithFocus() {
  4310. TableRowElement rowContainingFocus = null;
  4311. final Element focusedElement = WidgetUtil.getFocusedElement();
  4312. if (focusedElement != null && root.isOrHasChild(focusedElement)) {
  4313. Element e = focusedElement;
  4314. while (e != null && e != root) {
  4315. /*
  4316. * You never know if there's several tables embedded in a
  4317. * cell... We'll take the deepest one.
  4318. */
  4319. if (TableRowElement.is(e)) {
  4320. rowContainingFocus = TableRowElement.as(e);
  4321. }
  4322. e = e.getParentElement();
  4323. }
  4324. }
  4325. return rowContainingFocus;
  4326. }
  4327. /**
  4328. * Returns the cell object which contains information about the cell or
  4329. * spacer the element is in. As an implementation detail each spacer is
  4330. * a row with one cell, but they are stored in their own container and
  4331. * share the indexing with the regular rows.
  4332. *
  4333. * @param element
  4334. * The element to get the cell for. If element is not present
  4335. * in row or spacer container then <code>null</code> is
  4336. * returned.
  4337. *
  4338. * @return the cell reference of the element, or <code>null</code> if
  4339. * element is not present in the {@link RowContainer} or the
  4340. * {@link SpacerContainer}.
  4341. */
  4342. @Override
  4343. public Cell getCell(Element element) {
  4344. Cell cell = super.getCell(element);
  4345. if (cell == null) {
  4346. return null;
  4347. }
  4348. // Convert DOM coordinates to logical coordinates for rows
  4349. TableRowElement rowElement = (TableRowElement) cell.getElement()
  4350. .getParentElement();
  4351. if (!visualRowOrder.contains(rowElement)) {
  4352. for (Entry<Integer, SpacerContainer.SpacerImpl> entry : spacerContainer
  4353. .getSpacers().entrySet()) {
  4354. if (rowElement.equals(entry.getValue().getRootElement())) {
  4355. return new Cell(entry.getKey(), cell.getColumn(),
  4356. cell.getElement());
  4357. }
  4358. }
  4359. return null;
  4360. }
  4361. return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(),
  4362. cell.getElement());
  4363. }
  4364. @Override
  4365. public void setSpacer(int rowIndex, double height)
  4366. throws IllegalArgumentException {
  4367. spacerContainer.setSpacer(rowIndex, height);
  4368. }
  4369. @Override
  4370. public boolean spacerExists(int rowIndex) {
  4371. return spacerContainer.spacerExists(rowIndex);
  4372. }
  4373. @Override
  4374. public void resetSpacer(int rowIndex) {
  4375. spacerContainer.resetSpacer(rowIndex);
  4376. }
  4377. @Override
  4378. public void setSpacerUpdater(SpacerUpdater spacerUpdater)
  4379. throws IllegalArgumentException {
  4380. spacerContainer.setSpacerUpdater(spacerUpdater);
  4381. }
  4382. @Override
  4383. public SpacerUpdater getSpacerUpdater() {
  4384. return spacerContainer.getSpacerUpdater();
  4385. }
  4386. /**
  4387. * <em>Calculates</em> the expected top position of a row at a logical
  4388. * index, regardless if there is one there currently or not.
  4389. * <p>
  4390. * This method relies on fixed row height (by
  4391. * {@link #getDefaultRowHeight()}) and can only take into account
  4392. * spacers that are within visual range. Any scrolling might invalidate
  4393. * these results, so this method shouldn't be used to estimate scroll
  4394. * positions.
  4395. *
  4396. * @param logicalIndex
  4397. * the logical index of the row for which to calculate the
  4398. * top position
  4399. * @return the position where the row should currently be, were it to
  4400. * exist
  4401. * @see #getRowTop(TableRowElement)
  4402. */
  4403. private double getRowTop(int logicalIndex) {
  4404. double top = spacerContainer
  4405. .getSpacerHeightsSumUntilIndex(logicalIndex);
  4406. return top + (logicalIndex * getDefaultRowHeight());
  4407. }
  4408. public void shiftRowPositions(int row, double diff) {
  4409. for (TableRowElement tr : getVisibleRowsAfter(row)) {
  4410. setRowPosition(tr, 0, getRowTop(tr) + diff);
  4411. }
  4412. }
  4413. private List<TableRowElement> getVisibleRowsAfter(int logicalRow) {
  4414. Range visibleRowLogicalRange = getVisibleRowRange();
  4415. boolean allRowsAreInView = logicalRow < visibleRowLogicalRange
  4416. .getStart();
  4417. boolean noRowsAreInView = logicalRow >= visibleRowLogicalRange
  4418. .getEnd() - 1;
  4419. if (allRowsAreInView) {
  4420. return Collections.unmodifiableList(visualRowOrder);
  4421. } else if (noRowsAreInView) {
  4422. return Collections.emptyList();
  4423. } else {
  4424. int fromIndex = (logicalRow - visibleRowLogicalRange.getStart())
  4425. + 1;
  4426. int toIndex = visibleRowLogicalRange.length();
  4427. List<TableRowElement> sublist = visualRowOrder
  4428. .subList(fromIndex, toIndex);
  4429. return Collections.unmodifiableList(sublist);
  4430. }
  4431. }
  4432. @Override
  4433. public int getDomRowCount() {
  4434. return root.getChildCount()
  4435. - spacerContainer.getSpacersInDom().size();
  4436. }
  4437. @Override
  4438. protected boolean rowCanBeFrozen(TableRowElement tr) {
  4439. return visualRowOrder.contains(tr);
  4440. }
  4441. void reapplySpacerWidths() {
  4442. spacerContainer.reapplySpacerWidths();
  4443. }
  4444. void scrollToRowSpacerOrBoth(int targetRowIndex,
  4445. ScrollDestination destination, double padding,
  4446. ScrollType scrollType) {
  4447. if (!ensureScrollingAllowed()) {
  4448. return;
  4449. }
  4450. validateScrollDestination(destination, (int) padding);
  4451. // ignore the special case of -1 index spacer from the row index
  4452. // validation
  4453. if (!(targetRowIndex == -1 && !ScrollType.ROW.equals(scrollType))) {
  4454. // throws an IndexOutOfBoundsException if not valid
  4455. verifyValidRowIndex(targetRowIndex);
  4456. }
  4457. int oldTopRowLogicalIndex = getTopRowLogicalIndex();
  4458. int visualRangeLength = visualRowOrder.size();
  4459. int paddingInRows = 0;
  4460. if (!WidgetUtil.pixelValuesEqual(padding, 0d)) {
  4461. paddingInRows = (int) Math
  4462. .ceil(Double.valueOf(padding) / getDefaultRowHeight());
  4463. }
  4464. // calculate the largest index necessary to include at least
  4465. // partially below the top of the viewport and the smallest index
  4466. // necessary to include at least partially above the bottom of the
  4467. // viewport (target row itself might not be if padding is negative)
  4468. int firstVisibleIndexIfScrollingUp = targetRowIndex - paddingInRows;
  4469. int lastVisibleIndexIfScrollingDown = targetRowIndex
  4470. + paddingInRows;
  4471. int oldFirstBelowIndex = oldTopRowLogicalIndex + visualRangeLength;
  4472. int newTopRowLogicalIndex;
  4473. int logicalTargetIndex;
  4474. switch (destination) {
  4475. case ANY:
  4476. // scroll as little as possible, take into account that there
  4477. // needs to be a buffer row at both ends if there is room for
  4478. // one
  4479. boolean newRowsNeededAbove = (firstVisibleIndexIfScrollingUp < oldTopRowLogicalIndex)
  4480. || (firstVisibleIndexIfScrollingUp == oldTopRowLogicalIndex
  4481. && targetRowIndex > 0);
  4482. boolean rowsNeededBelow = (lastVisibleIndexIfScrollingDown >= oldFirstBelowIndex)
  4483. || ((lastVisibleIndexIfScrollingDown == oldFirstBelowIndex
  4484. - 1) && (oldFirstBelowIndex < getRowCount()));
  4485. if (newRowsNeededAbove) {
  4486. // scroll up, add buffer row if it fits
  4487. logicalTargetIndex = Math
  4488. .max(firstVisibleIndexIfScrollingUp - 1, 0);
  4489. newTopRowLogicalIndex = logicalTargetIndex;
  4490. } else if (rowsNeededBelow) {
  4491. // scroll down, add buffer row if it fits
  4492. newTopRowLogicalIndex = Math.min(
  4493. lastVisibleIndexIfScrollingDown + 1,
  4494. getRowCount() - 1) - visualRangeLength + 1;
  4495. if (newTopRowLogicalIndex
  4496. - oldTopRowLogicalIndex < visualRangeLength) {
  4497. // partial recycling, target index at the end of
  4498. // current range
  4499. logicalTargetIndex = oldFirstBelowIndex;
  4500. } else {
  4501. // full recycling, target index the same as the new
  4502. // top row index
  4503. logicalTargetIndex = newTopRowLogicalIndex;
  4504. }
  4505. } else {
  4506. // no need to recycle rows but viewport might need
  4507. // adjusting regardless
  4508. logicalTargetIndex = -1;
  4509. newTopRowLogicalIndex = oldTopRowLogicalIndex;
  4510. }
  4511. break;
  4512. case END:
  4513. // target row at the bottom of the viewport
  4514. newTopRowLogicalIndex = lastVisibleIndexIfScrollingDown + 1
  4515. - visualRangeLength + 1;
  4516. newTopRowLogicalIndex = ensureTopRowLogicalIndexSanity(
  4517. newTopRowLogicalIndex);
  4518. if ((newTopRowLogicalIndex > oldTopRowLogicalIndex)
  4519. && (newTopRowLogicalIndex
  4520. - oldTopRowLogicalIndex < visualRangeLength)) {
  4521. // partial recycling, target index at the end of
  4522. // current range
  4523. logicalTargetIndex = oldFirstBelowIndex;
  4524. } else {
  4525. // full recycling, target index the same as the new
  4526. // top row index
  4527. logicalTargetIndex = newTopRowLogicalIndex;
  4528. }
  4529. break;
  4530. case MIDDLE:
  4531. // target row at the middle of the viewport, padding has to be
  4532. // zero or we never would have reached this far
  4533. newTopRowLogicalIndex = targetRowIndex - visualRangeLength / 2;
  4534. newTopRowLogicalIndex = ensureTopRowLogicalIndexSanity(
  4535. newTopRowLogicalIndex);
  4536. if (newTopRowLogicalIndex < oldTopRowLogicalIndex) {
  4537. logicalTargetIndex = newTopRowLogicalIndex;
  4538. } else if (newTopRowLogicalIndex > oldTopRowLogicalIndex) {
  4539. if (newTopRowLogicalIndex
  4540. - oldTopRowLogicalIndex < visualRangeLength) {
  4541. // partial recycling, target index at the end of
  4542. // current range
  4543. logicalTargetIndex = oldFirstBelowIndex;
  4544. } else {
  4545. // full recycling, target index the same as the new
  4546. // top row index
  4547. logicalTargetIndex = newTopRowLogicalIndex;
  4548. }
  4549. } else {
  4550. logicalTargetIndex = -1;
  4551. }
  4552. break;
  4553. case START:
  4554. // target row at the top of the viewport, include buffer
  4555. // row if there is room for one
  4556. newTopRowLogicalIndex = firstVisibleIndexIfScrollingUp - 1;
  4557. newTopRowLogicalIndex = ensureTopRowLogicalIndexSanity(
  4558. newTopRowLogicalIndex);
  4559. if (getVisibleRowRange().contains(newTopRowLogicalIndex)) {
  4560. logicalTargetIndex = oldTopRowLogicalIndex
  4561. + visualRangeLength;
  4562. } else {
  4563. logicalTargetIndex = newTopRowLogicalIndex;
  4564. }
  4565. break;
  4566. default:
  4567. String msg = "Internal: Unsupported ScrollDestination: ";
  4568. throw new IllegalArgumentException(msg + destination.name());
  4569. }
  4570. // adjust visual range if necessary
  4571. if (newTopRowLogicalIndex < oldTopRowLogicalIndex) {
  4572. adjustVisualRangeUpForScrollToRowSpacerOrBoth(
  4573. oldTopRowLogicalIndex, visualRangeLength,
  4574. logicalTargetIndex);
  4575. } else if (newTopRowLogicalIndex > oldTopRowLogicalIndex) {
  4576. adjustVisualRangeDownForScrollToRowSpacerOrBoth(
  4577. oldTopRowLogicalIndex, visualRangeLength,
  4578. newTopRowLogicalIndex, logicalTargetIndex);
  4579. }
  4580. boolean rowsWereMoved = newTopRowLogicalIndex != oldTopRowLogicalIndex;
  4581. // update scroll position if necessary
  4582. adjustScrollPositionForScrollToRowSpacerOrBoth(targetRowIndex,
  4583. destination, padding, scrollType);
  4584. if (rowsWereMoved) {
  4585. fireRowVisibilityChangeEvent();
  4586. // schedule updating of the physical indexes
  4587. domSorter.reschedule();
  4588. }
  4589. }
  4590. /**
  4591. * Modifies the proposed top row logical index to fit within the logical
  4592. * range and to not leave gaps if it is avoidable.
  4593. *
  4594. * @param proposedTopRowLogicalIndex
  4595. * @return an adjusted index, or the original if no changes were
  4596. * necessary
  4597. */
  4598. private int ensureTopRowLogicalIndexSanity(
  4599. int proposedTopRowLogicalIndex) {
  4600. int newTopRowLogicalIndex = Math.max(proposedTopRowLogicalIndex, 0);
  4601. int visualRangeLength = visualRowOrder.size();
  4602. if (newTopRowLogicalIndex + visualRangeLength > getRowCount()) {
  4603. newTopRowLogicalIndex = getRowCount() - visualRangeLength;
  4604. }
  4605. return newTopRowLogicalIndex;
  4606. }
  4607. /**
  4608. * Checks that scrolling is allowed and resets the scroll position if
  4609. * it's not.
  4610. *
  4611. * @return {@code true} if scrolling is allowed, {@code false} otherwise
  4612. */
  4613. private boolean ensureScrollingAllowed() {
  4614. if (isScrollLocked(Direction.VERTICAL)) {
  4615. // no scrolling can happen
  4616. if (getScrollTop() != tBodyScrollTop) {
  4617. setBodyScrollPosition(tBodyScrollLeft, getScrollTop());
  4618. }
  4619. return false;
  4620. }
  4621. return true;
  4622. }
  4623. /**
  4624. * Adjusts visual range up for
  4625. * {@link #scrollToRowSpacerOrBoth(int, ScrollDestination, double, boolean, boolean)},
  4626. * reuse at your own peril.
  4627. *
  4628. * @param oldTopRowLogicalIndex
  4629. * @param visualRangeLength
  4630. * @param logicalTargetIndex
  4631. */
  4632. private void adjustVisualRangeUpForScrollToRowSpacerOrBoth(
  4633. int oldTopRowLogicalIndex, int visualRangeLength,
  4634. int logicalTargetIndex) {
  4635. // recycle at most the visual range's worth of rows to fill
  4636. // the gap between the new visualTargetIndex and the existing
  4637. // rows
  4638. int rowsToRecycle = Math.min(
  4639. oldTopRowLogicalIndex - logicalTargetIndex,
  4640. visualRangeLength);
  4641. // recycle from the end to the beginning
  4642. moveAndUpdateEscalatorRows(
  4643. Range.withLength(visualRangeLength - rowsToRecycle,
  4644. rowsToRecycle),
  4645. 0, logicalTargetIndex);
  4646. // update the index
  4647. setTopRowLogicalIndex(logicalTargetIndex);
  4648. }
  4649. /**
  4650. * Adjusts visual range down for
  4651. * {@link #scrollToRowSpacerOrBoth(int, ScrollDestination, double, boolean, boolean)},
  4652. * reuse at your own peril.
  4653. *
  4654. * @param oldTopRowLogicalIndex
  4655. * @param visualRangeLength
  4656. * @param newTopRowLogicalIndex
  4657. * @param logicalTargetIndex
  4658. */
  4659. private void adjustVisualRangeDownForScrollToRowSpacerOrBoth(
  4660. int oldTopRowLogicalIndex, int visualRangeLength,
  4661. int newTopRowLogicalIndex, int logicalTargetIndex) {
  4662. // recycle at most the visual range's worth of rows to fill
  4663. // the gap between the new visualTargetIndex and the existing
  4664. // rows
  4665. int rowsToRecycle;
  4666. if (newTopRowLogicalIndex
  4667. - oldTopRowLogicalIndex >= visualRangeLength) {
  4668. // full recycling
  4669. rowsToRecycle = visualRangeLength;
  4670. } else {
  4671. // partial recycling
  4672. rowsToRecycle = newTopRowLogicalIndex - oldTopRowLogicalIndex;
  4673. }
  4674. // recycle from the beginning to the end
  4675. moveAndUpdateEscalatorRows(Range.withLength(0, rowsToRecycle),
  4676. visualRangeLength, logicalTargetIndex);
  4677. // update the index
  4678. setTopRowLogicalIndex(newTopRowLogicalIndex);
  4679. }
  4680. /**
  4681. * Adjusts scroll position for
  4682. * {@link #scrollToRowSpacerOrBoth(int, ScrollDestination, double, boolean, boolean)},
  4683. * reuse at your own peril.
  4684. *
  4685. * @param targetRowIndex
  4686. * @param destination
  4687. * @param padding
  4688. * @param scrollType
  4689. */
  4690. private void adjustScrollPositionForScrollToRowSpacerOrBoth(
  4691. int targetRowIndex, ScrollDestination destination,
  4692. double padding, ScrollType scrollType) {
  4693. /*
  4694. * attempting to scroll above first row or below last row would get
  4695. * automatically corrected later but that causes unnecessary
  4696. * calculations, so try not to overshoot
  4697. */
  4698. double sectionHeight = getHeightOfSection();
  4699. double rowTop = getRowTop(targetRowIndex);
  4700. double spacerHeight = spacerContainer
  4701. .getSpacerHeight(targetRowIndex);
  4702. double scrollTop;
  4703. switch (destination) {
  4704. case ANY:
  4705. if (!ScrollType.SPACER.equals(scrollType)
  4706. && Math.max(rowTop - padding, 0) < getScrollTop()) {
  4707. // within visual range but row top above the viewport or not
  4708. // enough padding, shift a little
  4709. scrollTop = Math.max(rowTop - padding, 0);
  4710. } else if (ScrollType.SPACER.equals(scrollType)
  4711. && Math.max(rowTop + getDefaultRowHeight() - padding,
  4712. 0) < getScrollTop()) {
  4713. // within visual range but spacer top above the viewport or
  4714. // not enough padding, shift a little
  4715. scrollTop = Math
  4716. .max(rowTop + getDefaultRowHeight() - padding, 0);
  4717. } else if (ScrollType.ROW.equals(scrollType)
  4718. && rowTop + getDefaultRowHeight()
  4719. + padding > getScrollTop() + sectionHeight) {
  4720. // within visual range but end of row below the viewport
  4721. // or not enough padding, shift a little
  4722. scrollTop = rowTop + getDefaultRowHeight() - sectionHeight
  4723. + padding;
  4724. // ensure that we don't overshoot beyond bottom
  4725. scrollTop = Math.min(scrollTop,
  4726. getRowTop(getRowCount() - 1) + getDefaultRowHeight()
  4727. + spacerContainer
  4728. .getSpacerHeight(getRowCount() - 1)
  4729. - sectionHeight);
  4730. // if padding is set we want to overshoot or undershoot,
  4731. // otherwise make sure the top of the row is in view
  4732. if (padding == 0) {
  4733. scrollTop = Math.min(scrollTop, rowTop);
  4734. }
  4735. } else if (rowTop + getDefaultRowHeight() + spacerHeight
  4736. + padding > getScrollTop() + sectionHeight) {
  4737. // within visual range but end of spacer below the viewport
  4738. // or not enough padding, shift a little
  4739. scrollTop = rowTop + getDefaultRowHeight() + spacerHeight
  4740. - sectionHeight + padding;
  4741. // ensure that we don't overshoot beyond bottom
  4742. scrollTop = Math.min(scrollTop,
  4743. getRowTop(getRowCount()) - sectionHeight);
  4744. // if padding is set we want to overshoot or undershoot,
  4745. // otherwise make sure the top of the row or spacer is
  4746. // in view
  4747. if (padding == 0) {
  4748. if (ScrollType.SPACER.equals(scrollType)) {
  4749. scrollTop = Math.min(scrollTop,
  4750. rowTop + getDefaultRowHeight());
  4751. } else {
  4752. scrollTop = Math.min(scrollTop, rowTop);
  4753. }
  4754. }
  4755. } else {
  4756. // we are fine where we are
  4757. scrollTop = getScrollTop();
  4758. }
  4759. break;
  4760. case END:
  4761. if (ScrollType.ROW.equals(scrollType)
  4762. && rowTop + getDefaultRowHeight()
  4763. + padding != getScrollTop() + sectionHeight) {
  4764. // row should be at the bottom of the viewport
  4765. scrollTop = rowTop + getDefaultRowHeight() - sectionHeight
  4766. + padding;
  4767. } else if (rowTop + getDefaultRowHeight() + spacerHeight
  4768. + padding != getScrollTop() + sectionHeight) {
  4769. // spacer should be at the bottom of the viewport
  4770. scrollTop = rowTop + getDefaultRowHeight() + spacerHeight
  4771. - sectionHeight + padding;
  4772. } else {
  4773. // we are fine where we are
  4774. scrollTop = getScrollTop();
  4775. }
  4776. break;
  4777. case MIDDLE:
  4778. double center;
  4779. if (ScrollType.ROW.equals(scrollType)) {
  4780. // center the row itself
  4781. center = rowTop + (getDefaultRowHeight() / 2.0);
  4782. } else if (ScrollType.ROW_AND_SPACER.equals(scrollType)) {
  4783. // center both
  4784. center = rowTop
  4785. + ((getDefaultRowHeight() + spacerHeight) / 2.0);
  4786. } else {
  4787. // center the spacer
  4788. center = rowTop + getDefaultRowHeight()
  4789. + (spacerHeight / 2.0);
  4790. }
  4791. scrollTop = center - Math.ceil(sectionHeight / 2.0);
  4792. break;
  4793. case START:
  4794. if (!ScrollType.SPACER.equals(scrollType)
  4795. && Math.max(rowTop - padding, 0) != getScrollTop()) {
  4796. // row should be at the top of the viewport
  4797. scrollTop = Math.max(rowTop - padding, 0);
  4798. } else if (ScrollType.SPACER.equals(scrollType)
  4799. && Math.max(rowTop + getDefaultRowHeight() - padding,
  4800. 0) != getScrollTop()) {
  4801. // spacer should be at the top of the viewport
  4802. scrollTop = Math
  4803. .max(rowTop + getDefaultRowHeight() - padding, 0);
  4804. } else {
  4805. scrollTop = getScrollTop();
  4806. }
  4807. break;
  4808. default:
  4809. scrollTop = getScrollTop();
  4810. }
  4811. // ensure that we don't overshoot beyond bottom
  4812. scrollTop = Math.min(scrollTop,
  4813. getRowTop(getRowCount()) - sectionHeight);
  4814. // ensure that we don't overshoot beyond top
  4815. scrollTop = Math.max(0, scrollTop);
  4816. if (scrollTop != getScrollTop()) {
  4817. setScrollTop(scrollTop);
  4818. setBodyScrollPosition(tBodyScrollLeft, scrollTop);
  4819. }
  4820. }
  4821. @Override
  4822. public void setNewRowCallback(
  4823. Consumer<List<TableRowElement>> callback) {
  4824. newEscalatorRowCallback = callback;
  4825. }
  4826. }
  4827. private class ColumnConfigurationImpl implements ColumnConfiguration {
  4828. public class Column {
  4829. public static final double DEFAULT_COLUMN_WIDTH_PX = 100;
  4830. private double definedWidth = -1;
  4831. private double calculatedWidth = DEFAULT_COLUMN_WIDTH_PX;
  4832. private boolean measuringRequested = false;
  4833. public void setWidth(double px) {
  4834. Profiler.enter(
  4835. "Escalator.ColumnConfigurationImpl.Column.setWidth");
  4836. definedWidth = px;
  4837. if (px < 0) {
  4838. if (isAttached()) {
  4839. calculateWidth();
  4840. } else {
  4841. /*
  4842. * the column's width is calculated at Escalator.onLoad
  4843. * via measureAndSetWidthIfNeeded!
  4844. */
  4845. measuringRequested = true;
  4846. }
  4847. } else {
  4848. calculatedWidth = px;
  4849. }
  4850. Profiler.leave(
  4851. "Escalator.ColumnConfigurationImpl.Column.setWidth");
  4852. }
  4853. public double getDefinedWidth() {
  4854. return definedWidth;
  4855. }
  4856. /**
  4857. * Returns the actual width in the DOM.
  4858. *
  4859. * @return the width in pixels in the DOM. Returns -1 if the column
  4860. * needs measuring, but has not been yet measured
  4861. */
  4862. public double getCalculatedWidth() {
  4863. /*
  4864. * This might return an untrue value (e.g. during init/onload),
  4865. * since we haven't had a proper chance to actually calculate
  4866. * widths yet.
  4867. *
  4868. * This is fixed during Escalator.onLoad, by the call to
  4869. * "measureAndSetWidthIfNeeded", which fixes "everything".
  4870. */
  4871. if (!measuringRequested) {
  4872. return calculatedWidth;
  4873. } else {
  4874. return -1;
  4875. }
  4876. }
  4877. /**
  4878. * Checks if the column needs measuring, and then measures it.
  4879. * <p>
  4880. * Called by {@link Escalator#onLoad()}.
  4881. */
  4882. public boolean measureAndSetWidthIfNeeded() {
  4883. assert isAttached() : "Column.measureAndSetWidthIfNeeded() was called even though Escalator was not attached!";
  4884. if (measuringRequested) {
  4885. measuringRequested = false;
  4886. setWidth(definedWidth);
  4887. return true;
  4888. }
  4889. return false;
  4890. }
  4891. private void calculateWidth() {
  4892. calculatedWidth = getMaxCellWidth(columns.indexOf(this));
  4893. }
  4894. }
  4895. private final List<Column> columns = new ArrayList<>();
  4896. private int frozenColumns = 0;
  4897. /*
  4898. * TODO: this is a bit of a duplicate functionality with the
  4899. * Column.calculatedWidth caching. Probably should use one or the other,
  4900. * not both
  4901. */
  4902. /**
  4903. * A cached array of all the calculated column widths.
  4904. *
  4905. * @see #getCalculatedColumnWidths()
  4906. */
  4907. private double[] widthsArray = null;
  4908. /**
  4909. * {@inheritDoc}
  4910. * <p>
  4911. * <em>Implementation detail:</em> This method does no DOM modifications
  4912. * (i.e. is very cheap to call) if there are no rows in the DOM when
  4913. * this method is called.
  4914. *
  4915. * @see #hasSomethingInDom()
  4916. */
  4917. @Override
  4918. public void removeColumns(final int index, final int numberOfColumns) {
  4919. if (numberOfColumns == 0) {
  4920. return;
  4921. }
  4922. // Validate
  4923. assertArgumentsAreValidAndWithinRange(index, numberOfColumns);
  4924. // Move the horizontal scrollbar to the left, if removed columns are
  4925. // to the left of the viewport
  4926. removeColumnsAdjustScrollbar(index, numberOfColumns);
  4927. // Remove from DOM
  4928. header.paintRemoveColumns(index, numberOfColumns);
  4929. body.paintRemoveColumns(index, numberOfColumns);
  4930. footer.paintRemoveColumns(index, numberOfColumns);
  4931. // Remove from bookkeeping
  4932. flyweightRow.removeCells(index, numberOfColumns);
  4933. columns.subList(index, index + numberOfColumns).clear();
  4934. // Adjust frozen columns
  4935. if (index < getFrozenColumnCount()) {
  4936. if (index + numberOfColumns < frozenColumns) {
  4937. /*
  4938. * Last removed column was frozen, meaning that all removed
  4939. * columns were frozen. Just decrement the number of frozen
  4940. * columns accordingly.
  4941. */
  4942. frozenColumns -= numberOfColumns;
  4943. } else {
  4944. /*
  4945. * If last removed column was not frozen, we have removed
  4946. * columns beyond the frozen range, so all remaining frozen
  4947. * columns are to the left of the removed columns.
  4948. */
  4949. frozenColumns = index;
  4950. }
  4951. }
  4952. scroller.recalculateScrollbarsForVirtualViewport();
  4953. body.verifyEscalatorCount();
  4954. if (getColumnConfiguration().getColumnCount() > 0) {
  4955. reapplyRowWidths(header);
  4956. reapplyRowWidths(body);
  4957. reapplyRowWidths(footer);
  4958. }
  4959. /*
  4960. * Colspans make any kind of automatic clever content re-rendering
  4961. * impossible: As soon as anything has colspans, removing one might
  4962. * reveal further colspans, modifying the DOM structure once again,
  4963. * ending in a cascade of updates. Because we don't know how the
  4964. * data is updated.
  4965. *
  4966. * So, instead, we don't do anything. The client code is responsible
  4967. * for re-rendering the content (if so desired). Everything Just
  4968. * Works (TM) if colspans aren't used.
  4969. */
  4970. }
  4971. private void reapplyRowWidths(AbstractRowContainer container) {
  4972. if (container.getRowCount() > 0) {
  4973. container.reapplyRowWidths();
  4974. }
  4975. }
  4976. private void removeColumnsAdjustScrollbar(int index,
  4977. int numberOfColumns) {
  4978. if (horizontalScrollbar.getOffsetSize() >= horizontalScrollbar
  4979. .getScrollSize()) {
  4980. return;
  4981. }
  4982. double leftPosOfFirstColumnToRemove = getCalculatedColumnsWidth(
  4983. Range.between(0, index));
  4984. double widthOfColumnsToRemove = getCalculatedColumnsWidth(
  4985. Range.withLength(index, numberOfColumns));
  4986. double scrollLeft = horizontalScrollbar.getScrollPos();
  4987. if (scrollLeft <= leftPosOfFirstColumnToRemove) {
  4988. /*
  4989. * viewport is scrolled to the left of the first removed column,
  4990. * so there's no need to adjust anything
  4991. */
  4992. return;
  4993. }
  4994. double adjustedScrollLeft = Math.max(leftPosOfFirstColumnToRemove,
  4995. scrollLeft - widthOfColumnsToRemove);
  4996. horizontalScrollbar.setScrollPos(adjustedScrollLeft);
  4997. }
  4998. /**
  4999. * Calculate the width of a row, as the sum of columns' widths.
  5000. *
  5001. * @return the width of a row, in pixels
  5002. */
  5003. public double calculateRowWidth() {
  5004. return getCalculatedColumnsWidth(
  5005. Range.between(0, getColumnCount()));
  5006. }
  5007. private void assertArgumentsAreValidAndWithinRange(final int index,
  5008. final int numberOfColumns) {
  5009. if (numberOfColumns < 1) {
  5010. throw new IllegalArgumentException(
  5011. "Number of columns can't be less than 1 (was "
  5012. + numberOfColumns + ")");
  5013. }
  5014. if (index < 0 || index + numberOfColumns > getColumnCount()) {
  5015. throw new IndexOutOfBoundsException("The given "
  5016. + "column range (" + index + ".."
  5017. + (index + numberOfColumns)
  5018. + ") was outside of the current "
  5019. + "number of columns (" + getColumnCount() + ")");
  5020. }
  5021. }
  5022. /**
  5023. * {@inheritDoc}
  5024. * <p>
  5025. * <em>Implementation detail:</em> This method does no DOM modifications
  5026. * (i.e. is very cheap to call) if there is no data for rows when this
  5027. * method is called.
  5028. *
  5029. * @see #hasColumnAndRowData()
  5030. */
  5031. @Override
  5032. public void insertColumns(final int index, final int numberOfColumns) {
  5033. if (numberOfColumns == 0) {
  5034. return;
  5035. }
  5036. // Validate
  5037. if (index < 0 || index > getColumnCount()) {
  5038. throw new IndexOutOfBoundsException("The given index(" + index
  5039. + ") was outside of the current number of columns (0.."
  5040. + getColumnCount() + ")");
  5041. }
  5042. if (numberOfColumns < 1) {
  5043. throw new IllegalArgumentException(
  5044. "Number of columns must be 1 or greater (was "
  5045. + numberOfColumns);
  5046. }
  5047. // Add to bookkeeping
  5048. flyweightRow.addCells(index, numberOfColumns);
  5049. for (int i = 0; i < numberOfColumns; i++) {
  5050. columns.add(index, new Column());
  5051. }
  5052. // Adjust frozen columns
  5053. boolean frozen = index < frozenColumns;
  5054. if (frozen) {
  5055. frozenColumns += numberOfColumns;
  5056. }
  5057. // Add to DOM
  5058. header.paintInsertColumns(index, numberOfColumns, frozen);
  5059. body.paintInsertColumns(index, numberOfColumns, frozen);
  5060. footer.paintInsertColumns(index, numberOfColumns, frozen);
  5061. // this needs to be before the scrollbar adjustment.
  5062. boolean scrollbarWasNeeded = horizontalScrollbar
  5063. .getOffsetSize() < horizontalScrollbar.getScrollSize();
  5064. scroller.recalculateScrollbarsForVirtualViewport();
  5065. boolean scrollbarIsNowNeeded = horizontalScrollbar
  5066. .getOffsetSize() < horizontalScrollbar.getScrollSize();
  5067. if (!scrollbarWasNeeded && scrollbarIsNowNeeded) {
  5068. // This might as a side effect move rows around (when scrolled
  5069. // all the way down) and require the DOM to be up to date, i.e.
  5070. // the column to be added
  5071. body.verifyEscalatorCount();
  5072. }
  5073. // fix initial width
  5074. if (header.getRowCount() > 0 || body.getRowCount() > 0
  5075. || footer.getRowCount() > 0) {
  5076. Map<Integer, Double> colWidths = new HashMap<>();
  5077. Double width = Double.valueOf(Column.DEFAULT_COLUMN_WIDTH_PX);
  5078. for (int i = index; i < index + numberOfColumns; i++) {
  5079. Integer col = Integer.valueOf(i);
  5080. colWidths.put(col, width);
  5081. }
  5082. getColumnConfiguration().setColumnWidths(colWidths, true);
  5083. }
  5084. // Adjust scrollbar
  5085. double pixelsToInsertedColumn = columnConfiguration
  5086. .getCalculatedColumnsWidth(Range.withLength(0, index));
  5087. final boolean columnsWereAddedToTheLeftOfViewport = scroller.lastScrollLeft > pixelsToInsertedColumn;
  5088. if (columnsWereAddedToTheLeftOfViewport) {
  5089. double insertedColumnsWidth = columnConfiguration
  5090. .getCalculatedColumnsWidth(
  5091. Range.withLength(index, numberOfColumns));
  5092. horizontalScrollbar.setScrollPos(
  5093. scroller.lastScrollLeft + insertedColumnsWidth);
  5094. }
  5095. /*
  5096. * Colspans make any kind of automatic clever content re-rendering
  5097. * impossible: As soon as anything has colspans, adding one might
  5098. * affect surrounding colspans, modifying the DOM structure once
  5099. * again, ending in a cascade of updates. Because we don't know how
  5100. * the data is updated.
  5101. *
  5102. * So, instead, we don't do anything. The client code is responsible
  5103. * for re-rendering the content (if so desired). Everything Just
  5104. * Works (TM) if colspans aren't used.
  5105. */
  5106. }
  5107. @Override
  5108. public int getColumnCount() {
  5109. return columns.size();
  5110. }
  5111. @Override
  5112. public void setFrozenColumnCount(int count)
  5113. throws IllegalArgumentException {
  5114. if (count < 0 || count > getColumnCount()) {
  5115. throw new IllegalArgumentException(
  5116. "count must be between 0 and the current number of columns ("
  5117. + getColumnCount() + ")");
  5118. }
  5119. int oldCount = frozenColumns;
  5120. if (count == oldCount) {
  5121. return;
  5122. }
  5123. frozenColumns = count;
  5124. if (hasSomethingInDom()) {
  5125. // Are we freezing or unfreezing?
  5126. boolean frozen = count > oldCount;
  5127. int firstAffectedCol;
  5128. int firstUnaffectedCol;
  5129. if (frozen) {
  5130. firstAffectedCol = oldCount;
  5131. firstUnaffectedCol = count;
  5132. } else {
  5133. firstAffectedCol = count;
  5134. firstUnaffectedCol = oldCount;
  5135. }
  5136. if (oldCount > 0) {
  5137. header.setColumnLastFrozen(oldCount - 1, false);
  5138. body.setColumnLastFrozen(oldCount - 1, false);
  5139. footer.setColumnLastFrozen(oldCount - 1, false);
  5140. }
  5141. if (count > 0) {
  5142. header.setColumnLastFrozen(count - 1, true);
  5143. body.setColumnLastFrozen(count - 1, true);
  5144. footer.setColumnLastFrozen(count - 1, true);
  5145. }
  5146. for (int col = firstAffectedCol; col < firstUnaffectedCol; col++) {
  5147. header.setColumnFrozen(col, frozen);
  5148. body.setColumnFrozen(col, frozen);
  5149. footer.setColumnFrozen(col, frozen);
  5150. }
  5151. }
  5152. scroller.recalculateScrollbarsForVirtualViewport();
  5153. }
  5154. @Override
  5155. public int getFrozenColumnCount() {
  5156. return frozenColumns;
  5157. }
  5158. @Override
  5159. public void setColumnWidth(int index, double px)
  5160. throws IllegalArgumentException {
  5161. setColumnWidths(Collections.singletonMap(Integer.valueOf(index),
  5162. Double.valueOf(px)), true);
  5163. }
  5164. @Override
  5165. public void setColumnWidths(Map<Integer, Double> indexWidthMap)
  5166. throws IllegalArgumentException {
  5167. setColumnWidths(indexWidthMap, true);
  5168. }
  5169. @Override
  5170. public void setColumnWidths(Map<Integer, Double> indexWidthMap,
  5171. boolean recalculateElementSizes)
  5172. throws IllegalArgumentException {
  5173. if (indexWidthMap == null) {
  5174. throw new IllegalArgumentException("indexWidthMap was null");
  5175. }
  5176. if (indexWidthMap.isEmpty()) {
  5177. return;
  5178. }
  5179. Profiler.enter("Escalator.ColumnConfigurationImpl.setColumnWidths");
  5180. try {
  5181. for (Entry<Integer, Double> entry : indexWidthMap.entrySet()) {
  5182. int index = entry.getKey().intValue();
  5183. double width = entry.getValue().doubleValue();
  5184. checkValidColumnIndex(index);
  5185. // Not all browsers will accept any fractional size..
  5186. width = WidgetUtil.roundSizeDown(width);
  5187. columns.get(index).setWidth(width);
  5188. }
  5189. widthsArray = null;
  5190. header.reapplyColumnWidths();
  5191. body.reapplyColumnWidths();
  5192. footer.reapplyColumnWidths();
  5193. if (recalculateElementSizes) {
  5194. recalculateElementSizes();
  5195. }
  5196. } finally {
  5197. Profiler.leave(
  5198. "Escalator.ColumnConfigurationImpl.setColumnWidths");
  5199. }
  5200. }
  5201. private void checkValidColumnIndex(int index)
  5202. throws IllegalArgumentException {
  5203. if (!Range.withLength(0, getColumnCount()).contains(index)) {
  5204. throw new IllegalArgumentException("The given column index ("
  5205. + index + ") does not exist");
  5206. }
  5207. }
  5208. @Override
  5209. public double getColumnWidth(int index)
  5210. throws IllegalArgumentException {
  5211. checkValidColumnIndex(index);
  5212. return columns.get(index).getDefinedWidth();
  5213. }
  5214. @Override
  5215. public double getColumnWidthActual(int index) {
  5216. return columns.get(index).getCalculatedWidth();
  5217. }
  5218. private double getMaxCellWidth(int colIndex)
  5219. throws IllegalArgumentException {
  5220. double headerWidth = header.measureMinCellWidth(colIndex, true);
  5221. double bodyWidth = body.measureMinCellWidth(colIndex, true);
  5222. double footerWidth = footer.measureMinCellWidth(colIndex, true);
  5223. double maxWidth = Math.max(headerWidth,
  5224. Math.max(bodyWidth, footerWidth));
  5225. if (maxWidth < 0 && header.getRowCount() == 0
  5226. && body.getRowCount() == 0 && footer.getRowCount() == 0) {
  5227. maxWidth = 0;
  5228. }
  5229. assert maxWidth >= 0 : "Got a negative max width for a column, which should be impossible.";
  5230. return maxWidth;
  5231. }
  5232. private double getMinCellWidth(int colIndex)
  5233. throws IllegalArgumentException {
  5234. double headerWidth = header.measureMinCellWidth(colIndex, false);
  5235. double bodyWidth = body.measureMinCellWidth(colIndex, false);
  5236. double footerWidth = footer.measureMinCellWidth(colIndex, false);
  5237. double minWidth = Math.max(headerWidth,
  5238. Math.max(bodyWidth, footerWidth));
  5239. if (minWidth < 0 && header.getRowCount() == 0
  5240. && body.getRowCount() == 0 && footer.getRowCount() == 0) {
  5241. minWidth = 0;
  5242. }
  5243. assert minWidth >= 0 : "Got a negative min width for a column, which should be impossible.";
  5244. return minWidth;
  5245. }
  5246. /**
  5247. * Calculates the width of the columns in a given range.
  5248. *
  5249. * @param columns
  5250. * the columns to calculate
  5251. * @return the total width of the columns in the given
  5252. * <code>columns</code>
  5253. */
  5254. double getCalculatedColumnsWidth(final Range columns) {
  5255. /*
  5256. * This is an assert instead of an exception, since this is an
  5257. * internal method.
  5258. */
  5259. assert columns
  5260. .isSubsetOf(Range.between(0, getColumnCount())) : "Range "
  5261. + "was outside of current column range (i.e.: "
  5262. + Range.between(0, getColumnCount())
  5263. + ", but was given :" + columns;
  5264. double sum = 0;
  5265. for (int i = columns.getStart(); i < columns.getEnd(); i++) {
  5266. double columnWidthActual = getColumnWidthActual(i);
  5267. sum += columnWidthActual;
  5268. }
  5269. return sum;
  5270. }
  5271. double[] getCalculatedColumnWidths() {
  5272. if (widthsArray == null || widthsArray.length != getColumnCount()) {
  5273. widthsArray = new double[getColumnCount()];
  5274. for (int i = 0; i < columns.size(); i++) {
  5275. widthsArray[i] = columns.get(i).getCalculatedWidth();
  5276. }
  5277. }
  5278. return widthsArray;
  5279. }
  5280. @Override
  5281. public void refreshColumns(int index, int numberOfColumns)
  5282. throws IndexOutOfBoundsException, IllegalArgumentException {
  5283. if (numberOfColumns < 1) {
  5284. throw new IllegalArgumentException(
  5285. "Number of columns must be 1 or greater (was "
  5286. + numberOfColumns + ")");
  5287. }
  5288. if (index < 0 || index + numberOfColumns > getColumnCount()) {
  5289. throw new IndexOutOfBoundsException("The given "
  5290. + "column range (" + index + ".."
  5291. + (index + numberOfColumns)
  5292. + ") was outside of the current number of columns ("
  5293. + getColumnCount() + ")");
  5294. }
  5295. header.refreshColumns(index, numberOfColumns);
  5296. body.refreshColumns(index, numberOfColumns);
  5297. footer.refreshColumns(index, numberOfColumns);
  5298. }
  5299. }
  5300. /**
  5301. * A decision on how to measure a spacer when it is partially within a
  5302. * designated range.
  5303. * <p>
  5304. * The meaning of each value may differ depending on the context it is being
  5305. * used in. Check that particular method's JavaDoc.
  5306. */
  5307. private enum SpacerInclusionStrategy {
  5308. /** A representation of "the entire spacer". */
  5309. COMPLETE,
  5310. /** A representation of "a partial spacer". */
  5311. PARTIAL,
  5312. /** A representation of "no spacer at all". */
  5313. NONE
  5314. }
  5315. private class SpacerContainer {
  5316. /** This is used mainly for testing purposes */
  5317. private static final String SPACER_LOGICAL_ROW_PROPERTY = "vLogicalRow";
  5318. private final class SpacerImpl implements Spacer {
  5319. private TableCellElement spacerElement;
  5320. private TableRowElement root;
  5321. private DivElement deco;
  5322. private int rowIndex;
  5323. private double height = -1;
  5324. private boolean domHasBeenSetup = false;
  5325. private double decoHeight;
  5326. private double defaultCellBorderBottomSize = -1;
  5327. public SpacerImpl(int rowIndex) {
  5328. this.rowIndex = rowIndex;
  5329. root = TableRowElement.as(DOM.createTR());
  5330. spacerElement = TableCellElement.as(DOM.createTD());
  5331. root.appendChild(spacerElement);
  5332. root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex);
  5333. deco = DivElement.as(DOM.createDiv());
  5334. }
  5335. public void setPositionDiff(double x, double y) {
  5336. setPosition(getLeft() + x, getTop() + y);
  5337. }
  5338. public void setupDom(double height) {
  5339. assert !domHasBeenSetup : "DOM can't be set up twice.";
  5340. assert RootPanel.get().getElement().isOrHasChild(
  5341. root) : "Root element should've been attached to the DOM by now.";
  5342. domHasBeenSetup = true;
  5343. getRootElement().getStyle().setWidth(getInnerWidth(), Unit.PX);
  5344. setHeight(height);
  5345. spacerElement
  5346. .setColSpan(getColumnConfiguration().getColumnCount());
  5347. setStylePrimaryName(getStylePrimaryName());
  5348. }
  5349. public TableRowElement getRootElement() {
  5350. return root;
  5351. }
  5352. @Override
  5353. public Element getDecoElement() {
  5354. return deco;
  5355. }
  5356. public void setPosition(double x, double y) {
  5357. positions.set(getRootElement(), x, y);
  5358. positions.set(getDecoElement(), 0,
  5359. y - getSpacerDecoTopOffset());
  5360. }
  5361. private double getSpacerDecoTopOffset() {
  5362. return getBody().getDefaultRowHeight();
  5363. }
  5364. public void setStylePrimaryName(String style) {
  5365. UIObject.setStylePrimaryName(root, style + "-spacer");
  5366. UIObject.setStylePrimaryName(deco, style + "-spacer-deco");
  5367. }
  5368. /**
  5369. * Clear spacer height without moving other contents.
  5370. *
  5371. * @see #setHeight(double)
  5372. */
  5373. private void clearHeight() {
  5374. height = 0;
  5375. root.getStyle().setHeight(0, Unit.PX);
  5376. updateDecoratorGeometry(0);
  5377. }
  5378. public void setHeight(double height) {
  5379. assert height >= 0 : "Height must be more >= 0 (was " + height
  5380. + ")";
  5381. final double heightDiff = height - Math.max(0, this.height);
  5382. final double oldHeight = this.height;
  5383. this.height = height;
  5384. // since the spacer might be rendered on top of the previous
  5385. // rows border (done with css), need to increase height the
  5386. // amount of the border thickness
  5387. if (defaultCellBorderBottomSize < 0) {
  5388. defaultCellBorderBottomSize = WidgetUtil
  5389. .getBorderBottomThickness(body
  5390. .getRowElement(
  5391. getVisibleRowRange().getStart())
  5392. .getFirstChildElement());
  5393. }
  5394. root.getStyle().setHeight(height + defaultCellBorderBottomSize,
  5395. Unit.PX);
  5396. // move the visible spacers getRow row onwards.
  5397. shiftSpacerPositionsAfterRow(getRow(), heightDiff);
  5398. /*
  5399. * If we're growing, we'll adjust the scroll size first, then
  5400. * adjust scrolling. If we're shrinking, we do it after the
  5401. * second if-clause.
  5402. */
  5403. boolean spacerIsGrowing = heightDiff > 0;
  5404. if (spacerIsGrowing) {
  5405. verticalScrollbar.setScrollSize(
  5406. verticalScrollbar.getScrollSize() + heightDiff);
  5407. }
  5408. /*
  5409. * Don't modify the scrollbars if we're expanding the -1 spacer
  5410. * while we're scrolled to the top.
  5411. */
  5412. boolean minusOneSpacerException = spacerIsGrowing
  5413. && getRow() == -1 && body.getTopRowLogicalIndex() == 0;
  5414. boolean viewportNeedsScrolling = getRow() < body
  5415. .getTopRowLogicalIndex() && !minusOneSpacerException;
  5416. if (viewportNeedsScrolling) {
  5417. /*
  5418. * We can't use adjustScrollPos here, probably because of a
  5419. * bookkeeping-related race condition.
  5420. *
  5421. * This particular situation is easier, however, since we
  5422. * know exactly how many pixels we need to move (heightDiff)
  5423. * and all elements below the spacer always need to move
  5424. * that pixel amount.
  5425. */
  5426. for (TableRowElement row : body.visualRowOrder) {
  5427. body.setRowPosition(row, 0,
  5428. body.getRowTop(row) + heightDiff);
  5429. }
  5430. double top = getTop();
  5431. double bottom = top + oldHeight;
  5432. double scrollTop = verticalScrollbar.getScrollPos();
  5433. boolean viewportTopIsAtMidSpacer = top < scrollTop
  5434. && scrollTop < bottom;
  5435. final double moveDiff;
  5436. if (viewportTopIsAtMidSpacer && !spacerIsGrowing) {
  5437. /*
  5438. * If the scroll top is in the middle of the modified
  5439. * spacer, we want to scroll the viewport up as usual,
  5440. * but we don't want to scroll past the top of it.
  5441. *
  5442. * Math.max ensures this (remember: the result is going
  5443. * to be negative).
  5444. */
  5445. moveDiff = Math.max(heightDiff, top - scrollTop);
  5446. } else {
  5447. moveDiff = heightDiff;
  5448. }
  5449. body.setBodyScrollPosition(tBodyScrollLeft,
  5450. tBodyScrollTop + moveDiff);
  5451. verticalScrollbar.setScrollPosByDelta(moveDiff);
  5452. } else {
  5453. body.shiftRowPositions(getRow(), heightDiff);
  5454. }
  5455. if (!spacerIsGrowing) {
  5456. verticalScrollbar.setScrollSize(
  5457. verticalScrollbar.getScrollSize() + heightDiff);
  5458. }
  5459. updateDecoratorGeometry(height);
  5460. }
  5461. /** Resizes and places the decorator. */
  5462. private void updateDecoratorGeometry(double detailsHeight) {
  5463. Style style = deco.getStyle();
  5464. decoHeight = detailsHeight + getBody().getDefaultRowHeight();
  5465. style.setHeight(decoHeight, Unit.PX);
  5466. }
  5467. @Override
  5468. public Element getElement() {
  5469. return spacerElement;
  5470. }
  5471. @Override
  5472. public int getRow() {
  5473. return rowIndex;
  5474. }
  5475. public double getHeight() {
  5476. assert height >= 0 : "Height was not previously set by setHeight.";
  5477. return height;
  5478. }
  5479. public double getTop() {
  5480. return positions.getTop(getRootElement());
  5481. }
  5482. public double getLeft() {
  5483. return positions.getLeft(getRootElement());
  5484. }
  5485. /**
  5486. * Sets a new row index for this spacer. Also updates the
  5487. * bookkeeping at {@link SpacerContainer#rowIndexToSpacer}.
  5488. */
  5489. @SuppressWarnings("boxing")
  5490. public void setRowIndex(int rowIndex) {
  5491. SpacerImpl spacer = rowIndexToSpacer.remove(this.rowIndex);
  5492. assert this == spacer : "trying to move an unexpected spacer.";
  5493. int oldIndex = this.rowIndex;
  5494. this.rowIndex = rowIndex;
  5495. root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex);
  5496. rowIndexToSpacer.put(this.rowIndex, this);
  5497. fireEvent(new SpacerIndexChangedEvent(oldIndex, this.rowIndex));
  5498. }
  5499. /**
  5500. * Updates the spacer's visibility parameters, based on whether it
  5501. * is being currently visible or not.
  5502. *
  5503. * @deprecated Escalator no longer uses this logic at initialisation
  5504. * as there can only be a limited number of spacers and
  5505. * hidden spacers within visual range interfere with
  5506. * position calculations.
  5507. */
  5508. @Deprecated
  5509. public void updateVisibility() {
  5510. if (isInViewport()) {
  5511. show();
  5512. } else {
  5513. hide();
  5514. }
  5515. }
  5516. private boolean isInViewport() {
  5517. int top = (int) Math.ceil(getTop());
  5518. int height = (int) Math.floor(getHeight());
  5519. Range location = Range.withLength(top, height);
  5520. return getViewportPixels().intersects(location);
  5521. }
  5522. public void show() {
  5523. getRootElement().getStyle().clearDisplay();
  5524. getDecoElement().getStyle().clearDisplay();
  5525. fireEvent(new SpacerVisibilityChangedEvent(getRow(), true));
  5526. }
  5527. public void hide() {
  5528. getRootElement().getStyle().setDisplay(Display.NONE);
  5529. getDecoElement().getStyle().setDisplay(Display.NONE);
  5530. fireEvent(new SpacerVisibilityChangedEvent(getRow(), false));
  5531. }
  5532. /**
  5533. * Crop the decorator element so that it doesn't overlap the header
  5534. * and footer sections.
  5535. *
  5536. * @param bodyTop
  5537. * the top cordinate of the escalator body
  5538. * @param bodyBottom
  5539. * the bottom cordinate of the escalator body
  5540. * @param decoWidth
  5541. * width of the deco
  5542. */
  5543. private void updateDecoClip(final double bodyTop,
  5544. final double bodyBottom, final double decoWidth) {
  5545. final int top = deco.getAbsoluteTop();
  5546. final int bottom = deco.getAbsoluteBottom();
  5547. /*
  5548. * FIXME
  5549. *
  5550. * Height and its use is a workaround for the issue where
  5551. * coordinates of the deco are not calculated yet. This will
  5552. * prevent a deco from being displayed when it's added to DOM
  5553. */
  5554. final int height = bottom - top;
  5555. if (top < bodyTop || bottom > bodyBottom) {
  5556. final double topClip = Math.max(0.0D, bodyTop - top);
  5557. final double bottomClip = height
  5558. - Math.max(0.0D, bottom - bodyBottom);
  5559. // TODO [optimize] not sure how GWT compiles this
  5560. final String clip = new StringBuilder("rect(")
  5561. .append(topClip).append("px,").append(decoWidth)
  5562. .append("px,").append(bottomClip).append("px,0)")
  5563. .toString();
  5564. deco.getStyle().setProperty("clip", clip);
  5565. } else {
  5566. deco.getStyle().setProperty("clip", "auto");
  5567. }
  5568. }
  5569. }
  5570. private final TreeMap<Integer, SpacerImpl> rowIndexToSpacer = new TreeMap<>();
  5571. private SpacerUpdater spacerUpdater = SpacerUpdater.NULL;
  5572. private final ScrollHandler spacerScroller = new ScrollHandler() {
  5573. private double prevScrollX = 0;
  5574. @Override
  5575. public void onScroll(ScrollEvent event) {
  5576. if (WidgetUtil.pixelValuesEqual(getScrollLeft(), prevScrollX)) {
  5577. return;
  5578. }
  5579. prevScrollX = getScrollLeft();
  5580. for (SpacerImpl spacer : rowIndexToSpacer.values()) {
  5581. spacer.setPosition(prevScrollX, spacer.getTop());
  5582. }
  5583. }
  5584. };
  5585. private HandlerRegistration spacerScrollerRegistration;
  5586. /** Width of the spacers' decos. Calculated once then cached. */
  5587. private double spacerDecoWidth = 0.0D;
  5588. public void setSpacer(int rowIndex, double height)
  5589. throws IllegalArgumentException {
  5590. if (rowIndex < -1 || rowIndex >= getBody().getRowCount()) {
  5591. throw new IllegalArgumentException("invalid row index: "
  5592. + rowIndex + ", while the body only has "
  5593. + getBody().getRowCount() + " rows.");
  5594. }
  5595. if (height >= 0) {
  5596. if (!spacerExists(rowIndex)) {
  5597. insertNewSpacer(rowIndex, height);
  5598. } else {
  5599. updateExistingSpacer(rowIndex, height);
  5600. }
  5601. } else if (spacerExists(rowIndex)) {
  5602. removeSpacer(rowIndex);
  5603. }
  5604. updateSpacerDecosVisibility();
  5605. }
  5606. /** Checks if a given element is a spacer element */
  5607. public boolean isSpacer(Element row) {
  5608. /*
  5609. * If this needs optimization, we could do a more heuristic check
  5610. * based on stylenames and stuff, instead of iterating through the
  5611. * map.
  5612. */
  5613. for (SpacerImpl spacer : rowIndexToSpacer.values()) {
  5614. if (spacer.getRootElement().equals(row)) {
  5615. return true;
  5616. }
  5617. }
  5618. return false;
  5619. }
  5620. void resetSpacer(int rowIndex) {
  5621. if (spacerExists(rowIndex)) {
  5622. SpacerImpl spacer = getSpacer(rowIndex);
  5623. destroySpacerContent(spacer);
  5624. initSpacerContent(spacer);
  5625. }
  5626. }
  5627. @SuppressWarnings("boxing")
  5628. void scrollToSpacer(int spacerIndex, ScrollDestination destination,
  5629. int padding) {
  5630. assert !destination.equals(ScrollDestination.MIDDLE)
  5631. || padding != 0 : "destination/padding check should be done before this method";
  5632. body.scrollToRowSpacerOrBoth(spacerIndex, destination, padding,
  5633. ScrollType.SPACER);
  5634. }
  5635. public void reapplySpacerWidths() {
  5636. // FIXME #16266 , spacers get couple pixels too much because borders
  5637. final double width = getInnerWidth() - spacerDecoWidth;
  5638. for (SpacerImpl spacer : rowIndexToSpacer.values()) {
  5639. spacer.getRootElement().getStyle().setWidth(width, Unit.PX);
  5640. }
  5641. }
  5642. /**
  5643. * @deprecated This method is no longer used by Escalator and is likely
  5644. * to be removed soon.
  5645. *
  5646. * @param removedRowsRange
  5647. */
  5648. @Deprecated
  5649. public void paintRemoveSpacers(Range removedRowsRange) {
  5650. removeSpacers(removedRowsRange);
  5651. shiftSpacersByRows(removedRowsRange.getStart(),
  5652. -removedRowsRange.length());
  5653. }
  5654. /**
  5655. * Removes spacers of the given range without moving other contents.
  5656. * <p>
  5657. * NOTE: Changed functionality since 8.9. Previous incarnation of this
  5658. * method updated the positions of all the contents below the first
  5659. * removed spacer.
  5660. *
  5661. * @param removedRange
  5662. * logical range of spacers to remove
  5663. */
  5664. @SuppressWarnings("boxing")
  5665. public void removeSpacers(Range removedRange) {
  5666. Map<Integer, SpacerImpl> removedSpacers = rowIndexToSpacer.subMap(
  5667. removedRange.getStart(), true, removedRange.getEnd(),
  5668. false);
  5669. if (removedSpacers.isEmpty()) {
  5670. return;
  5671. }
  5672. double specialSpacerHeight = removedRange.contains(-1)
  5673. ? getSpacerHeight(-1)
  5674. : 0;
  5675. for (Entry<Integer, SpacerImpl> entry : removedSpacers.entrySet()) {
  5676. SpacerImpl spacer = entry.getValue();
  5677. rowIndexToSpacer.remove(entry.getKey());
  5678. destroySpacerContent(spacer);
  5679. spacer.clearHeight();
  5680. spacer.getRootElement().removeFromParent();
  5681. spacer.getDecoElement().removeFromParent();
  5682. }
  5683. removedSpacers.clear();
  5684. if (rowIndexToSpacer.isEmpty()) {
  5685. assert spacerScrollerRegistration != null : "Spacer scroller registration was null";
  5686. spacerScrollerRegistration.removeHandler();
  5687. spacerScrollerRegistration = null;
  5688. }
  5689. // if a rowless spacer at the top got removed, all rows and spacers
  5690. // need to be moved up accordingly
  5691. if (!WidgetUtil.pixelValuesEqual(specialSpacerHeight, 0)) {
  5692. double scrollDiff = Math.min(specialSpacerHeight,
  5693. getScrollTop());
  5694. body.moveViewportAndContent(null, -specialSpacerHeight,
  5695. -specialSpacerHeight, -scrollDiff);
  5696. }
  5697. }
  5698. public Map<Integer, SpacerImpl> getSpacers() {
  5699. return new HashMap<>(rowIndexToSpacer);
  5700. }
  5701. /**
  5702. * Calculates the sum of all spacers.
  5703. *
  5704. * @return sum of all spacers, or 0 if no spacers present
  5705. */
  5706. public double getSpacerHeightsSum() {
  5707. return getHeights(rowIndexToSpacer.values());
  5708. }
  5709. /**
  5710. * Calculates the sum of all spacers from one row index onwards.
  5711. *
  5712. * @param logicalRowIndex
  5713. * the spacer to include as the first calculated spacer
  5714. * @return the sum of all spacers from {@code logicalRowIndex} and
  5715. * onwards, or 0 if no suitable spacers were found
  5716. */
  5717. @SuppressWarnings("boxing")
  5718. public Collection<SpacerImpl> getSpacersForRowAndAfter(
  5719. int logicalRowIndex) {
  5720. return new ArrayList<>(
  5721. rowIndexToSpacer.tailMap(logicalRowIndex, true).values());
  5722. }
  5723. /**
  5724. * Get all spacers from one pixel point onwards.
  5725. * <p>
  5726. *
  5727. * In this method, the {@link SpacerInclusionStrategy} has the following
  5728. * meaning when a spacer lies in the middle of either pixel argument:
  5729. * <dl>
  5730. * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE}
  5731. * <dd>include the spacer
  5732. * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL}
  5733. * <dd>include the spacer
  5734. * <dt>{@link SpacerInclusionStrategy#NONE NONE}
  5735. * <dd>ignore the spacer
  5736. * </dl>
  5737. *
  5738. * @param px
  5739. * the pixel point after which to return all spacers
  5740. * @param strategy
  5741. * the inclusion strategy regarding the {@code px}
  5742. * @return a collection of the spacers that exist after {@code px}
  5743. */
  5744. public Collection<SpacerImpl> getSpacersAfterPx(final double px,
  5745. final SpacerInclusionStrategy strategy) {
  5746. List<SpacerImpl> spacers = new ArrayList<>(
  5747. rowIndexToSpacer.values());
  5748. for (int i = 0; i < spacers.size(); i++) {
  5749. SpacerImpl spacer = spacers.get(i);
  5750. double top = spacer.getTop();
  5751. double bottom = top + spacer.getHeight();
  5752. if (top > px) {
  5753. return spacers.subList(i, spacers.size());
  5754. } else if (bottom > px) {
  5755. if (strategy == SpacerInclusionStrategy.NONE) {
  5756. return spacers.subList(i + 1, spacers.size());
  5757. } else {
  5758. return spacers.subList(i, spacers.size());
  5759. }
  5760. }
  5761. }
  5762. return Collections.emptySet();
  5763. }
  5764. /**
  5765. * Gets the spacers currently rendered in the DOM.
  5766. *
  5767. * @return an unmodifiable (but live) collection of the spacers
  5768. * currently in the DOM
  5769. */
  5770. public Collection<SpacerImpl> getSpacersInDom() {
  5771. return Collections
  5772. .unmodifiableCollection(rowIndexToSpacer.values());
  5773. }
  5774. /**
  5775. * Gets the amount of pixels occupied by spacers between two pixel
  5776. * points.
  5777. * <p>
  5778. * In this method, the {@link SpacerInclusionStrategy} has the following
  5779. * meaning when a spacer lies in the middle of either pixel argument:
  5780. * <dl>
  5781. * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE}
  5782. * <dd>take the entire spacer into account
  5783. * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL}
  5784. * <dd>take only the visible area into account
  5785. * <dt>{@link SpacerInclusionStrategy#NONE NONE}
  5786. * <dd>ignore that spacer
  5787. * </dl>
  5788. *
  5789. * @param rangeTop
  5790. * the top pixel point
  5791. * @param topInclusion
  5792. * the inclusion strategy regarding {@code rangeTop}.
  5793. * @param rangeBottom
  5794. * the bottom pixel point
  5795. * @param bottomInclusion
  5796. * the inclusion strategy regarding {@code rangeBottom}.
  5797. * @return the pixels occupied by spacers between {@code rangeTop} and
  5798. * {@code rangeBottom}
  5799. */
  5800. public double getSpacerHeightsSumBetweenPx(double rangeTop,
  5801. SpacerInclusionStrategy topInclusion, double rangeBottom,
  5802. SpacerInclusionStrategy bottomInclusion) {
  5803. assert rangeTop <= rangeBottom : "rangeTop must be less than rangeBottom";
  5804. double heights = 0;
  5805. /*
  5806. * TODO [[optimize]]: this might be somewhat inefficient (due to
  5807. * iterator-based scanning, instead of using the treemap's search
  5808. * functionalities). But it should be easy to write, read, verify
  5809. * and maintain.
  5810. */
  5811. for (SpacerImpl spacer : rowIndexToSpacer.values()) {
  5812. double top = spacer.getTop();
  5813. double height = spacer.getHeight();
  5814. double bottom = top + height;
  5815. /*
  5816. * If we happen to implement a DoubleRange (in addition to the
  5817. * int-based Range) at some point, the following logic should
  5818. * probably be converted into using the
  5819. * Range.partitionWith-equivalent.
  5820. */
  5821. boolean topIsAboveRange = top < rangeTop;
  5822. boolean topIsInRange = rangeTop <= top && top <= rangeBottom;
  5823. boolean topIsBelowRange = rangeBottom < top;
  5824. boolean bottomIsAboveRange = bottom < rangeTop;
  5825. boolean bottomIsInRange = rangeTop <= bottom
  5826. && bottom <= rangeBottom;
  5827. boolean bottomIsBelowRange = rangeBottom < bottom;
  5828. assert topIsAboveRange ^ topIsBelowRange
  5829. ^ topIsInRange : "Bad top logic";
  5830. assert bottomIsAboveRange ^ bottomIsBelowRange
  5831. ^ bottomIsInRange : "Bad bottom logic";
  5832. if (bottomIsAboveRange) {
  5833. continue;
  5834. } else if (topIsBelowRange) {
  5835. return heights;
  5836. } else if (topIsAboveRange && bottomIsInRange) {
  5837. switch (topInclusion) {
  5838. case PARTIAL:
  5839. heights += bottom - rangeTop;
  5840. break;
  5841. case COMPLETE:
  5842. heights += height;
  5843. break;
  5844. default:
  5845. break;
  5846. }
  5847. } else if (topIsAboveRange && bottomIsBelowRange) {
  5848. /*
  5849. * Here we arbitrarily decide that the top inclusion will
  5850. * have the honor of overriding the bottom inclusion if
  5851. * happens to be a conflict of interests.
  5852. */
  5853. switch (topInclusion) {
  5854. case NONE:
  5855. return 0;
  5856. case COMPLETE:
  5857. return height;
  5858. case PARTIAL:
  5859. return rangeBottom - rangeTop;
  5860. default:
  5861. throw new IllegalArgumentException(
  5862. "Unexpected inclusion state :" + topInclusion);
  5863. }
  5864. } else if (topIsInRange && bottomIsInRange) {
  5865. heights += height;
  5866. } else if (topIsInRange && bottomIsBelowRange) {
  5867. switch (bottomInclusion) {
  5868. case PARTIAL:
  5869. heights += rangeBottom - top;
  5870. break;
  5871. case COMPLETE:
  5872. heights += height;
  5873. break;
  5874. default:
  5875. break;
  5876. }
  5877. return heights;
  5878. } else {
  5879. assert false : "Unnaccounted-for situation";
  5880. }
  5881. }
  5882. return heights;
  5883. }
  5884. /**
  5885. * Gets the amount of pixels occupied by spacers from the top until a
  5886. * certain spot from the top of the body.
  5887. *
  5888. * @param px
  5889. * pixels counted from the top
  5890. * @return the pixels occupied by spacers up until {@code px}
  5891. */
  5892. public double getSpacerHeightsSumUntilPx(double px) {
  5893. return getSpacerHeightsSumBetweenPx(0,
  5894. SpacerInclusionStrategy.PARTIAL, px,
  5895. SpacerInclusionStrategy.PARTIAL);
  5896. }
  5897. /**
  5898. * Gets the amount of pixels occupied by spacers until a logical row
  5899. * index. The spacer of the row corresponding with the given index isn't
  5900. * included.
  5901. *
  5902. * @param logicalIndex
  5903. * a logical row index
  5904. * @return the pixels occupied by spacers up until {@code logicalIndex}
  5905. */
  5906. @SuppressWarnings("boxing")
  5907. public double getSpacerHeightsSumUntilIndex(int logicalIndex) {
  5908. return getHeights(
  5909. rowIndexToSpacer.headMap(logicalIndex, false).values());
  5910. }
  5911. private double getHeights(Collection<SpacerImpl> spacers) {
  5912. double heights = 0;
  5913. for (SpacerImpl spacer : spacers) {
  5914. heights += spacer.getHeight();
  5915. }
  5916. return heights;
  5917. }
  5918. /**
  5919. * Gets the height of the spacer for a row index.
  5920. *
  5921. * @param rowIndex
  5922. * the index of the row where the spacer should be
  5923. * @return the height of the spacer at index {@code rowIndex}, or 0 if
  5924. * there is no spacer there
  5925. */
  5926. public double getSpacerHeight(int rowIndex) {
  5927. SpacerImpl spacer = getSpacer(rowIndex);
  5928. if (spacer != null) {
  5929. return spacer.getHeight();
  5930. } else {
  5931. return 0;
  5932. }
  5933. }
  5934. private boolean spacerExists(int rowIndex) {
  5935. return rowIndexToSpacer.containsKey(Integer.valueOf(rowIndex));
  5936. }
  5937. @SuppressWarnings("boxing")
  5938. private void insertNewSpacer(int rowIndex, double height) {
  5939. if (spacerScrollerRegistration == null) {
  5940. spacerScrollerRegistration = addScrollHandler(spacerScroller);
  5941. }
  5942. final SpacerImpl spacer = new SpacerImpl(rowIndex);
  5943. rowIndexToSpacer.put(rowIndex, spacer);
  5944. // set the position before adding it to DOM
  5945. positions.set(spacer.getRootElement(), getScrollLeft(),
  5946. calculateSpacerTop(rowIndex));
  5947. TableRowElement spacerRoot = spacer.getRootElement();
  5948. spacerRoot.getStyle()
  5949. .setWidth(columnConfiguration.calculateRowWidth(), Unit.PX);
  5950. body.getElement().appendChild(spacerRoot);
  5951. spacer.setupDom(height);
  5952. // set the deco position, requires that spacer is in the DOM
  5953. positions.set(spacer.getDecoElement(), 0,
  5954. spacer.getTop() - spacer.getSpacerDecoTopOffset());
  5955. spacerDecoContainer.appendChild(spacer.getDecoElement());
  5956. if (spacerDecoContainer.getParentElement() == null) {
  5957. getElement().appendChild(spacerDecoContainer);
  5958. // calculate the spacer deco width, it won't change
  5959. spacerDecoWidth = getBoundingWidth(spacer.getDecoElement());
  5960. }
  5961. initSpacerContent(spacer);
  5962. // schedule updating of the physical indexes
  5963. body.domSorter.reschedule();
  5964. }
  5965. private void updateExistingSpacer(int rowIndex, double newHeight) {
  5966. getSpacer(rowIndex).setHeight(newHeight);
  5967. }
  5968. public SpacerImpl getSpacer(int rowIndex) {
  5969. return rowIndexToSpacer.get(Integer.valueOf(rowIndex));
  5970. }
  5971. private void removeSpacer(int rowIndex) {
  5972. removeSpacers(Range.withOnly(rowIndex));
  5973. }
  5974. public void setStylePrimaryName(String style) {
  5975. for (SpacerImpl spacer : rowIndexToSpacer.values()) {
  5976. spacer.setStylePrimaryName(style);
  5977. }
  5978. }
  5979. public void setSpacerUpdater(SpacerUpdater spacerUpdater)
  5980. throws IllegalArgumentException {
  5981. if (spacerUpdater == null) {
  5982. throw new IllegalArgumentException(
  5983. "spacer updater cannot be null");
  5984. }
  5985. destroySpacerContent(rowIndexToSpacer.values());
  5986. this.spacerUpdater = spacerUpdater;
  5987. initSpacerContent(rowIndexToSpacer.values());
  5988. }
  5989. public SpacerUpdater getSpacerUpdater() {
  5990. return spacerUpdater;
  5991. }
  5992. private void destroySpacerContent(Iterable<SpacerImpl> spacers) {
  5993. for (SpacerImpl spacer : spacers) {
  5994. destroySpacerContent(spacer);
  5995. }
  5996. }
  5997. private void destroySpacerContent(SpacerImpl spacer) {
  5998. assert getElement().isOrHasChild(spacer
  5999. .getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching";
  6000. assert getElement().isOrHasChild(spacer
  6001. .getElement()) : "Spacer element somehow got detached from Escalator before detaching";
  6002. spacerUpdater.destroy(spacer);
  6003. assert getElement().isOrHasChild(spacer
  6004. .getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching";
  6005. assert getElement().isOrHasChild(spacer
  6006. .getElement()) : "Spacer element somehow got detached from Escalator before detaching";
  6007. }
  6008. private void initSpacerContent(Iterable<SpacerImpl> spacers) {
  6009. for (SpacerImpl spacer : spacers) {
  6010. initSpacerContent(spacer);
  6011. }
  6012. }
  6013. private void initSpacerContent(SpacerImpl spacer) {
  6014. assert getElement().isOrHasChild(spacer
  6015. .getRootElement()) : "Spacer's root element somehow got detached from Escalator before attaching";
  6016. assert getElement().isOrHasChild(spacer
  6017. .getElement()) : "Spacer element somehow got detached from Escalator before attaching";
  6018. spacerUpdater.init(spacer);
  6019. assert getElement().isOrHasChild(spacer
  6020. .getRootElement()) : "Spacer's root element somehow got detached from Escalator during attaching";
  6021. assert getElement().isOrHasChild(spacer
  6022. .getElement()) : "Spacer element somehow got detached from Escalator during attaching";
  6023. spacer.show();
  6024. }
  6025. public String getSubPartName(Element subElement) {
  6026. for (SpacerImpl spacer : rowIndexToSpacer.values()) {
  6027. if (spacer.getRootElement().isOrHasChild(subElement)) {
  6028. return "spacer[" + spacer.getRow() + "]";
  6029. }
  6030. }
  6031. return null;
  6032. }
  6033. public Element getSubPartElement(int index) {
  6034. SpacerImpl spacer = rowIndexToSpacer.get(Integer.valueOf(index));
  6035. if (spacer != null) {
  6036. return spacer.getElement();
  6037. } else {
  6038. return null;
  6039. }
  6040. }
  6041. private double calculateSpacerTop(int logicalIndex) {
  6042. return body.getRowTop(logicalIndex) + body.getDefaultRowHeight();
  6043. }
  6044. @SuppressWarnings("boxing")
  6045. private void shiftSpacerPositionsAfterRow(int changedRowIndex,
  6046. double diffPx) {
  6047. for (SpacerImpl spacer : rowIndexToSpacer
  6048. .tailMap(changedRowIndex, false).values()) {
  6049. spacer.setPositionDiff(0, diffPx);
  6050. }
  6051. }
  6052. /**
  6053. * Shifts spacers at and after a specific row by an amount of rows that
  6054. * don't contain spacers of their own.
  6055. * <p>
  6056. * This moves both their associated logical row index and also their
  6057. * visual placement.
  6058. * <p>
  6059. * <em>Note:</em> This method does not check for the validity of any
  6060. * arguments.
  6061. *
  6062. * @param index
  6063. * the index of first row to move
  6064. * @param numberOfRows
  6065. * the number of rows to shift the spacers with. A positive
  6066. * value is downwards, a negative value is upwards.
  6067. */
  6068. public void shiftSpacersByRows(int index, int numberOfRows) {
  6069. final double pxDiff = numberOfRows * body.getDefaultRowHeight();
  6070. List<SpacerContainer.SpacerImpl> spacers = new ArrayList<>(
  6071. getSpacersForRowAndAfter(index));
  6072. if (numberOfRows < 0) {
  6073. for (SpacerContainer.SpacerImpl spacer : spacers) {
  6074. spacer.setPositionDiff(0, pxDiff);
  6075. spacer.setRowIndex(spacer.getRow() + numberOfRows);
  6076. }
  6077. } else {
  6078. for (int i = spacers.size() - 1; i >= 0; --i) {
  6079. SpacerContainer.SpacerImpl spacer = spacers.get(i);
  6080. spacer.setPositionDiff(0, pxDiff);
  6081. spacer.setRowIndex(spacer.getRow() + numberOfRows);
  6082. }
  6083. }
  6084. }
  6085. /**
  6086. * Update the associated logical row indexes for spacers without moving
  6087. * their actual positions.
  6088. * <p>
  6089. * <em>Note:</em> This method does not check for the validity of any
  6090. * arguments.
  6091. *
  6092. * @param startIndex
  6093. * the previous logical index of first row to update
  6094. * @param endIndex
  6095. * the previous logical index of first row that doesn't need
  6096. * updating anymore
  6097. * @param numberOfRows
  6098. * the number of rows to shift the associated logical index
  6099. * with. A positive value is downwards, a negative value is
  6100. * upwards.
  6101. */
  6102. private void updateSpacerIndexesForRowAndAfter(int startIndex,
  6103. int endIndex, int numberOfRows) {
  6104. List<SpacerContainer.SpacerImpl> spacers = new ArrayList<>(
  6105. getSpacersForRowAndAfter(startIndex));
  6106. spacers.removeAll(getSpacersForRowAndAfter(endIndex));
  6107. if (numberOfRows < 0) {
  6108. for (SpacerContainer.SpacerImpl spacer : spacers) {
  6109. spacer.setRowIndex(spacer.getRow() + numberOfRows);
  6110. }
  6111. } else {
  6112. for (int i = spacers.size() - 1; i >= 0; --i) {
  6113. SpacerContainer.SpacerImpl spacer = spacers.get(i);
  6114. spacer.setRowIndex(spacer.getRow() + numberOfRows);
  6115. }
  6116. }
  6117. }
  6118. private void updateSpacerDecosVisibility() {
  6119. final Range visibleRowRange = getVisibleRowRange();
  6120. Collection<SpacerImpl> visibleSpacers = rowIndexToSpacer
  6121. .subMap(visibleRowRange.getStart() - 1,
  6122. visibleRowRange.getEnd() + 1)
  6123. .values();
  6124. if (!visibleSpacers.isEmpty()) {
  6125. final double top = tableWrapper.getAbsoluteTop()
  6126. + header.getHeightOfSection();
  6127. final double bottom = tableWrapper.getAbsoluteBottom()
  6128. - footer.getHeightOfSection();
  6129. for (SpacerImpl spacer : visibleSpacers) {
  6130. spacer.updateDecoClip(top, bottom, spacerDecoWidth);
  6131. }
  6132. }
  6133. }
  6134. }
  6135. private class ElementPositionBookkeeper {
  6136. /**
  6137. * A map containing cached values of an element's current top position.
  6138. */
  6139. private final Map<Element, Double> elementTopPositionMap = new HashMap<>();
  6140. private final Map<Element, Double> elementLeftPositionMap = new HashMap<>();
  6141. public void set(final Element e, final double x, final double y) {
  6142. assert e != null : "Element was null";
  6143. position.set(e, x, y);
  6144. elementTopPositionMap.put(e, Double.valueOf(y));
  6145. elementLeftPositionMap.put(e, Double.valueOf(x));
  6146. }
  6147. public double getTop(final Element e) {
  6148. Double top = elementTopPositionMap.get(e);
  6149. if (top == null) {
  6150. throw new IllegalArgumentException("Element " + e
  6151. + " was not found in the position bookkeeping");
  6152. }
  6153. return top.doubleValue();
  6154. }
  6155. public double getLeft(final Element e) {
  6156. Double left = elementLeftPositionMap.get(e);
  6157. if (left == null) {
  6158. throw new IllegalArgumentException("Element " + e
  6159. + " was not found in the position bookkeeping");
  6160. }
  6161. return left.doubleValue();
  6162. }
  6163. public void remove(Element e) {
  6164. elementTopPositionMap.remove(e);
  6165. elementLeftPositionMap.remove(e);
  6166. }
  6167. }
  6168. /**
  6169. * Utility class for parsing and storing SubPart request string attributes
  6170. * for Grid and Escalator.
  6171. *
  6172. * @since 7.5.0
  6173. */
  6174. public static class SubPartArguments {
  6175. private String type;
  6176. private int[] indices;
  6177. private SubPartArguments(String type, int[] indices) {
  6178. /*
  6179. * The constructor is private so that no third party would by
  6180. * mistake start using this parsing scheme, since it's not official
  6181. * by TestBench (yet?).
  6182. */
  6183. this.type = type;
  6184. this.indices = indices;
  6185. }
  6186. public String getType() {
  6187. return type;
  6188. }
  6189. public int getIndicesLength() {
  6190. return indices.length;
  6191. }
  6192. public int getIndex(int i) {
  6193. return indices[i];
  6194. }
  6195. public int[] getIndices() {
  6196. return Arrays.copyOf(indices, indices.length);
  6197. }
  6198. static SubPartArguments create(String subPart) {
  6199. String[] splitArgs = subPart.split("\\[");
  6200. String type = splitArgs[0];
  6201. int[] indices = new int[splitArgs.length - 1];
  6202. for (int i = 0; i < indices.length; ++i) {
  6203. String tmp = splitArgs[i + 1];
  6204. indices[i] = Integer
  6205. .parseInt(tmp.substring(0, tmp.indexOf("]", 1)));
  6206. }
  6207. return new SubPartArguments(type, indices);
  6208. }
  6209. }
  6210. enum ScrollType {
  6211. ROW, SPACER, ROW_AND_SPACER
  6212. }
  6213. // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y
  6214. /**
  6215. * The solution to
  6216. * <code>|tan<sup>-1</sup>(<i>x</i>)|&times;(180/&pi;)&nbsp;=&nbsp;30</code>
  6217. * .
  6218. * <p>
  6219. * This constant is placed in the Escalator class, instead of an inner
  6220. * class, since even mathematical expressions aren't allowed in non-static
  6221. * inner classes for constants.
  6222. */
  6223. private static final double RATIO_OF_30_DEGREES = 1 / Math.sqrt(3);
  6224. /**
  6225. * The solution to
  6226. * <code>|tan<sup>-1</sup>(<i>x</i>)|&times;(180/&pi;)&nbsp;=&nbsp;40</code>
  6227. * .
  6228. * <p>
  6229. * This constant is placed in the Escalator class, instead of an inner
  6230. * class, since even mathematical expressions aren't allowed in non-static
  6231. * inner classes for constants.
  6232. */
  6233. private static final double RATIO_OF_40_DEGREES = Math.tan(2 * Math.PI / 9);
  6234. private static final String DEFAULT_WIDTH = "500.0px";
  6235. private static final String DEFAULT_HEIGHT = "400.0px";
  6236. private FlyweightRow flyweightRow = new FlyweightRow();
  6237. /** The {@code <thead/>} tag. */
  6238. private final TableSectionElement headElem = TableSectionElement
  6239. .as(DOM.createTHead());
  6240. /** The {@code <tbody/>} tag. */
  6241. private final TableSectionElement bodyElem = TableSectionElement
  6242. .as(DOM.createTBody());
  6243. /** The {@code <tfoot/>} tag. */
  6244. private final TableSectionElement footElem = TableSectionElement
  6245. .as(DOM.createTFoot());
  6246. /**
  6247. * TODO: investigate whether this field is now unnecessary, as
  6248. * {@link ScrollbarBundle} now caches its values.
  6249. *
  6250. * @deprecated maybe...
  6251. */
  6252. @Deprecated
  6253. private double tBodyScrollTop = 0;
  6254. /**
  6255. * TODO: investigate whether this field is now unnecessary, as
  6256. * {@link ScrollbarBundle} now caches its values.
  6257. *
  6258. * @deprecated maybe...
  6259. */
  6260. @Deprecated
  6261. private double tBodyScrollLeft = 0;
  6262. private final VerticalScrollbarBundle verticalScrollbar = new VerticalScrollbarBundle();
  6263. private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle();
  6264. private final AriaGridHelper ariaGridHelper = new AriaGridHelper();
  6265. private final HeaderRowContainer header = new HeaderRowContainer(headElem);
  6266. private final BodyRowContainerImpl body = new BodyRowContainerImpl(
  6267. bodyElem);
  6268. private final FooterRowContainer footer = new FooterRowContainer(footElem);
  6269. /**
  6270. * Flag for keeping track of {@link RowHeightChangedEvent}s
  6271. */
  6272. private boolean rowHeightChangedEventFired = false;
  6273. private final Scroller scroller = new Scroller();
  6274. private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl();
  6275. private final DivElement tableWrapper;
  6276. private final Element table;
  6277. private final DivElement horizontalScrollbarDeco = DivElement
  6278. .as(DOM.createDiv());
  6279. private final DivElement headerDeco = DivElement.as(DOM.createDiv());
  6280. private final DivElement footerDeco = DivElement.as(DOM.createDiv());
  6281. private final DivElement spacerDecoContainer = DivElement
  6282. .as(DOM.createDiv());
  6283. private PositionFunction position;
  6284. /** The cached width of the escalator, in pixels. */
  6285. private double widthOfEscalator = 0;
  6286. /** The cached height of the escalator, in pixels. */
  6287. private double heightOfEscalator = 0;
  6288. /** The height of Escalator in terms of body rows. */
  6289. private double heightByRows = 10.0d;
  6290. /** The height of Escalator, as defined by {@link #setHeight(String)} */
  6291. private String heightByCss = "";
  6292. private HeightMode heightMode = HeightMode.CSS;
  6293. private double delayToCancelTouchScroll = -1;
  6294. private boolean layoutIsScheduled = false;
  6295. private ScheduledCommand layoutCommand = () -> {
  6296. // ensure that row heights have been set or auto-detected if
  6297. // auto-detection is already possible, because visibility changes might
  6298. // not trigger the default check that happens in onLoad()
  6299. header.autodetectRowHeightLater();
  6300. body.autodetectRowHeightLater();
  6301. footer.autodetectRowHeightLater();
  6302. recalculateElementSizes();
  6303. layoutIsScheduled = false;
  6304. };
  6305. private final ElementPositionBookkeeper positions = new ElementPositionBookkeeper();
  6306. private Map<Element, ComputedStyle> computedStyleMap = new HashMap<>();
  6307. /**
  6308. * Creates a new Escalator widget instance.
  6309. */
  6310. public Escalator() {
  6311. detectAndApplyPositionFunction();
  6312. getLogger().info("Using " + position.getClass().getSimpleName()
  6313. + " for position");
  6314. final Element root = DOM.createDiv();
  6315. setElement(root);
  6316. setupScrollbars(root);
  6317. tableWrapper = DivElement.as(DOM.createDiv());
  6318. Event.sinkEvents(tableWrapper, Event.ONSCROLL | Event.KEYEVENTS);
  6319. Event.setEventListener(tableWrapper, event -> {
  6320. if (event.getKeyCode() != KeyCodes.KEY_TAB) {
  6321. return;
  6322. }
  6323. boolean browserScroll = tableWrapper.getScrollLeft() != 0
  6324. || tableWrapper.getScrollTop() != 0;
  6325. boolean keyEvent = event.getType().startsWith("key");
  6326. if (browserScroll || keyEvent) {
  6327. // Browser is scrolling our div automatically, reset
  6328. tableWrapper.setScrollLeft(0);
  6329. tableWrapper.setScrollTop(0);
  6330. Element focused = WidgetUtil.getFocusedElement();
  6331. Stream.of(header, body, footer).forEach(container -> {
  6332. Cell cell = container.getCell(focused);
  6333. if (cell == null) {
  6334. return;
  6335. }
  6336. scrollToColumn(cell.getColumn(), ScrollDestination.ANY, 0);
  6337. if (container == body) {
  6338. scrollToRow(cell.getRow(), ScrollDestination.ANY, 0);
  6339. }
  6340. });
  6341. }
  6342. });
  6343. root.appendChild(tableWrapper);
  6344. table = DOM.createTable();
  6345. tableWrapper.appendChild(table);
  6346. table.appendChild(headElem);
  6347. table.appendChild(bodyElem);
  6348. table.appendChild(footElem);
  6349. Style hCornerStyle = headerDeco.getStyle();
  6350. hCornerStyle.setWidth(verticalScrollbar.getScrollbarThickness(),
  6351. Unit.PX);
  6352. hCornerStyle.setDisplay(Display.NONE);
  6353. root.appendChild(headerDeco);
  6354. Style fCornerStyle = footerDeco.getStyle();
  6355. fCornerStyle.setWidth(verticalScrollbar.getScrollbarThickness(),
  6356. Unit.PX);
  6357. fCornerStyle.setDisplay(Display.NONE);
  6358. root.appendChild(footerDeco);
  6359. Style hWrapperStyle = horizontalScrollbarDeco.getStyle();
  6360. hWrapperStyle.setDisplay(Display.NONE);
  6361. hWrapperStyle.setHeight(horizontalScrollbar.getScrollbarThickness(),
  6362. Unit.PX);
  6363. root.appendChild(horizontalScrollbarDeco);
  6364. setStylePrimaryName("v-escalator");
  6365. spacerDecoContainer.setAttribute("aria-hidden", "true");
  6366. // init default dimensions
  6367. setHeight(null);
  6368. setWidth(null);
  6369. publishJSHelpers(root);
  6370. }
  6371. private double getBoundingWidth(Element element) {
  6372. // Gets the current width, including border and padding, for the element
  6373. // while ignoring any transforms applied to the element (e.g. scale)
  6374. if (!computedStyleMap.containsKey(element)) {
  6375. if (computedStyleMap.isEmpty()) {
  6376. // ensure the next event loop calculates the sizes anew
  6377. Scheduler.get().scheduleDeferred(() -> clearComputedStyles());
  6378. }
  6379. computedStyleMap.put(element, new ComputedStyle(element));
  6380. }
  6381. return computedStyleMap.get(element).getWidthIncludingBorderPadding();
  6382. }
  6383. private double getBoundingHeight(Element element) {
  6384. // Gets the current height, including border and padding, for the
  6385. // element while ignoring any transforms applied to the element (e.g.
  6386. // scale)
  6387. if (!computedStyleMap.containsKey(element)) {
  6388. if (computedStyleMap.isEmpty()) {
  6389. // ensure the next event loop calculates the sizes anew
  6390. Scheduler.get().scheduleDeferred(() -> clearComputedStyles());
  6391. }
  6392. computedStyleMap.put(element, new ComputedStyle(element));
  6393. }
  6394. return computedStyleMap.get(element).getHeightIncludingBorderPadding();
  6395. }
  6396. private int getBodyRowCount() {
  6397. return getBody().getRowCount();
  6398. }
  6399. private native void publishJSHelpers(Element root)
  6400. /*-{
  6401. var self = this;
  6402. root.getBodyRowCount = $entry(function () {
  6403. return self.@Escalator::getBodyRowCount()();
  6404. });
  6405. }-*/;
  6406. private void setupScrollbars(final Element root) {
  6407. ScrollHandler scrollHandler = event -> {
  6408. scroller.onScroll();
  6409. fireEvent(new ScrollEvent());
  6410. };
  6411. int scrollbarThickness = WidgetUtil.getNativeScrollbarSize();
  6412. if (BrowserInfo.get().isIE()) {
  6413. /*
  6414. * IE refuses to scroll properly if the DIV isn't at least one pixel
  6415. * larger than the scrollbar controls themselves.
  6416. */
  6417. scrollbarThickness += 1;
  6418. }
  6419. root.appendChild(verticalScrollbar.getElement());
  6420. verticalScrollbar.addScrollHandler(scrollHandler);
  6421. verticalScrollbar.setScrollbarThickness(scrollbarThickness);
  6422. verticalScrollbar
  6423. .addVisibilityHandler(new ScrollbarBundle.VisibilityHandler() {
  6424. private boolean queued = false;
  6425. @Override
  6426. public void visibilityChanged(
  6427. ScrollbarBundle.VisibilityChangeEvent event) {
  6428. if (queued) {
  6429. return;
  6430. }
  6431. queued = true;
  6432. /*
  6433. * We either lost or gained a scrollbar. In either case,
  6434. * we may need to update the column widths.
  6435. */
  6436. Scheduler.get().scheduleFinally(() -> {
  6437. fireVerticalScrollbarVisibilityChangeEvent();
  6438. queued = false;
  6439. });
  6440. }
  6441. });
  6442. root.appendChild(horizontalScrollbar.getElement());
  6443. horizontalScrollbar.addScrollHandler(scrollHandler);
  6444. horizontalScrollbar.setScrollbarThickness(scrollbarThickness);
  6445. horizontalScrollbar
  6446. .addVisibilityHandler(new ScrollbarBundle.VisibilityHandler() {
  6447. private boolean queued = false;
  6448. @Override
  6449. public void visibilityChanged(
  6450. ScrollbarBundle.VisibilityChangeEvent event) {
  6451. if (queued) {
  6452. return;
  6453. }
  6454. queued = true;
  6455. /*
  6456. * We either lost or gained a scrollbar. In any case, we
  6457. * need to change the height, if it's defined by rows.
  6458. */
  6459. Scheduler.get().scheduleFinally(() -> {
  6460. applyHeightByRows();
  6461. queued = false;
  6462. });
  6463. }
  6464. });
  6465. /*
  6466. * Because of all the IE hacks we've done above, we now have scrollbars
  6467. * hiding underneath a lot of DOM elements.
  6468. *
  6469. * This leads to problems with OSX (and many touch-only devices) when
  6470. * scrollbars are only shown when scrolling, as the scrollbar elements
  6471. * are hidden underneath everything. We trust that the scrollbars behave
  6472. * properly in these situations and simply pop them out with a bit of
  6473. * z-indexing.
  6474. */
  6475. if (WidgetUtil.getNativeScrollbarSize() == 0) {
  6476. verticalScrollbar.getElement().getStyle().setZIndex(90);
  6477. horizontalScrollbar.getElement().getStyle().setZIndex(90);
  6478. }
  6479. }
  6480. @Override
  6481. protected void onLoad() {
  6482. super.onLoad();
  6483. // ensure that row heights have been set or auto-detected if
  6484. // auto-detection is already possible, if not the check will be
  6485. // performed again in layoutCommand
  6486. header.autodetectRowHeightLater();
  6487. body.autodetectRowHeightLater();
  6488. footer.autodetectRowHeightLater();
  6489. header.paintInsertRows(0, header.getRowCount());
  6490. footer.paintInsertRows(0, footer.getRowCount());
  6491. boolean columnsChanged = false;
  6492. for (ColumnConfigurationImpl.Column column : columnConfiguration.columns) {
  6493. boolean columnChanged = column.measureAndSetWidthIfNeeded();
  6494. if (columnChanged) {
  6495. columnsChanged = true;
  6496. }
  6497. }
  6498. if (columnsChanged) {
  6499. header.reapplyColumnWidths();
  6500. body.reapplyColumnWidths();
  6501. footer.reapplyColumnWidths();
  6502. }
  6503. verticalScrollbar.onLoad();
  6504. horizontalScrollbar.onLoad();
  6505. scroller.attachScrollListener(verticalScrollbar.getElement());
  6506. scroller.attachScrollListener(horizontalScrollbar.getElement());
  6507. scroller.attachMousewheelListener(getElement());
  6508. if (isCurrentBrowserIE11OrEdge()) {
  6509. // Touch listeners doesn't work for IE11 and Edge (#18737)
  6510. scroller.attachPointerEventListeners(getElement());
  6511. } else {
  6512. scroller.attachTouchListeners(getElement());
  6513. }
  6514. /*
  6515. * Note: There's no need to explicitly insert rows into the body.
  6516. *
  6517. * recalculateElementSizes will recalculate the height of the body. This
  6518. * has the side-effect that as the body's size grows bigger (i.e. from 0
  6519. * to its actual height), more escalator rows are populated. Those
  6520. * escalator rows are then immediately rendered. This, in effect, is the
  6521. * same thing as inserting those rows.
  6522. *
  6523. * In fact, having an extra paintInsertRows here would lead to duplicate
  6524. * rows.
  6525. */
  6526. recalculateElementSizes();
  6527. }
  6528. @Override
  6529. protected void onUnload() {
  6530. scroller.detachScrollListener(verticalScrollbar.getElement());
  6531. scroller.detachScrollListener(horizontalScrollbar.getElement());
  6532. scroller.detachMousewheelListener(getElement());
  6533. if (isCurrentBrowserIE11OrEdge()) {
  6534. // Touch listeners doesn't work for IE11 and Edge (#18737)
  6535. scroller.detachPointerEventListeners(getElement());
  6536. } else {
  6537. scroller.detachTouchListeners(getElement());
  6538. }
  6539. /*
  6540. * We can call paintRemoveRows here, because static ranges are simple to
  6541. * remove.
  6542. */
  6543. header.paintRemoveRows(0, header.getRowCount());
  6544. footer.paintRemoveRows(0, footer.getRowCount());
  6545. /*
  6546. * We can't call body.paintRemoveRows since it relies on rowCount to be
  6547. * updated correctly. Since it isn't, we'll simply and brutally rip out
  6548. * the DOM elements (in an elegant way, of course).
  6549. */
  6550. int rowsToRemove = body.getDomRowCount();
  6551. for (int i = 0; i < rowsToRemove; i++) {
  6552. int index = rowsToRemove - i - 1;
  6553. TableRowElement tr = bodyElem.getRows().getItem(index);
  6554. body.paintRemoveRow(tr, index);
  6555. positions.remove(tr);
  6556. }
  6557. body.visualRowOrder.clear();
  6558. body.setTopRowLogicalIndex(0);
  6559. super.onUnload();
  6560. }
  6561. private void detectAndApplyPositionFunction() {
  6562. final Style docStyle = Document.get().getBody().getStyle();
  6563. if (hasProperty(docStyle, "transform")) {
  6564. if (hasProperty(docStyle, "transformStyle")) {
  6565. position = new Translate3DPosition();
  6566. } else {
  6567. position = new TranslatePosition();
  6568. }
  6569. } else if (hasProperty(docStyle, "webkitTransform")) {
  6570. position = new WebkitTranslate3DPosition();
  6571. }
  6572. }
  6573. private Logger getLogger() {
  6574. return Logger.getLogger(getClass().getName());
  6575. }
  6576. private static native boolean hasProperty(Style style, String name)
  6577. /*-{
  6578. return style[name] !== undefined;
  6579. }-*/;
  6580. /**
  6581. * Check whether there are both columns and any row data (for either
  6582. * headers, body or footer).
  6583. *
  6584. * @return <code>true</code> if header, body or footer has rows and there
  6585. * are columns
  6586. */
  6587. private boolean hasColumnAndRowData() {
  6588. return (header.getRowCount() > 0 || body.getRowCount() > 0
  6589. || footer.getRowCount() > 0)
  6590. && columnConfiguration.getColumnCount() > 0;
  6591. }
  6592. /**
  6593. * Check whether there are any cells in the DOM.
  6594. *
  6595. * @return <code>true</code> if header, body or footer has any child
  6596. * elements
  6597. */
  6598. private boolean hasSomethingInDom() {
  6599. return headElem.hasChildNodes() || bodyElem.hasChildNodes()
  6600. || footElem.hasChildNodes();
  6601. }
  6602. /**
  6603. * Returns the row container for the header in this Escalator.
  6604. *
  6605. * @return the header. Never <code>null</code>
  6606. */
  6607. public RowContainer getHeader() {
  6608. return header;
  6609. }
  6610. /**
  6611. * Returns the row container for the body in this Escalator.
  6612. *
  6613. * @return the body. Never <code>null</code>
  6614. */
  6615. public BodyRowContainer getBody() {
  6616. return body;
  6617. }
  6618. /**
  6619. * Returns the row container for the footer in this Escalator.
  6620. *
  6621. * @return the footer. Never <code>null</code>
  6622. */
  6623. public RowContainer getFooter() {
  6624. return footer;
  6625. }
  6626. /**
  6627. * Returns the configuration object for the columns in this Escalator.
  6628. *
  6629. * @return the configuration object for the columns in this Escalator. Never
  6630. * <code>null</code>
  6631. */
  6632. public ColumnConfiguration getColumnConfiguration() {
  6633. return columnConfiguration;
  6634. }
  6635. @Override
  6636. public void setWidth(final String width) {
  6637. String oldWidth = getElement().getStyle().getProperty("width");
  6638. if (width != null && !width.isEmpty()) {
  6639. super.setWidth(width);
  6640. if (!width.equals(oldWidth)) {
  6641. fireEscalatorSizeChangeEvent();
  6642. }
  6643. } else {
  6644. super.setWidth(DEFAULT_WIDTH);
  6645. if (!DEFAULT_WIDTH.equals(oldWidth)) {
  6646. fireEscalatorSizeChangeEvent();
  6647. }
  6648. }
  6649. recalculateElementSizes();
  6650. }
  6651. /**
  6652. * {@inheritDoc}
  6653. * <p>
  6654. * If Escalator is currently not in {@link HeightMode#CSS}, the given value
  6655. * is remembered, and applied once the mode is applied.
  6656. *
  6657. * @see #setHeightMode(HeightMode)
  6658. */
  6659. @Override
  6660. public void setHeight(String height) {
  6661. /*
  6662. * TODO remove method once RequiresResize and the Vaadin layoutmanager
  6663. * listening mechanisms are implemented
  6664. */
  6665. if (height != null && !height.isEmpty()) {
  6666. heightByCss = height;
  6667. } else {
  6668. if (getHeightMode() == HeightMode.UNDEFINED) {
  6669. heightByRows = body.getRowCount();
  6670. applyHeightByRows();
  6671. return;
  6672. } else {
  6673. heightByCss = DEFAULT_HEIGHT;
  6674. }
  6675. }
  6676. if (getHeightMode() == HeightMode.CSS) {
  6677. setHeightInternal(height);
  6678. }
  6679. }
  6680. private void setHeightInternal(final String height) {
  6681. final int escalatorRowsBefore = body.visualRowOrder.size();
  6682. if (height != null && !height.isEmpty()) {
  6683. String oldHeight = getElement().getStyle().getProperty("height");
  6684. super.setHeight(height);
  6685. if (!height.equals(oldHeight)) {
  6686. fireEscalatorSizeChangeEvent();
  6687. }
  6688. } else {
  6689. if (getHeightMode() == HeightMode.UNDEFINED) {
  6690. int newHeightByRows = body.getRowCount();
  6691. if (heightByRows != newHeightByRows) {
  6692. heightByRows = newHeightByRows;
  6693. applyHeightByRows();
  6694. }
  6695. return;
  6696. } else {
  6697. String oldHeight = getElement().getStyle()
  6698. .getProperty("height");
  6699. super.setHeight(DEFAULT_HEIGHT);
  6700. if (!DEFAULT_HEIGHT.equals(oldHeight)) {
  6701. fireEscalatorSizeChangeEvent();
  6702. }
  6703. }
  6704. }
  6705. recalculateElementSizes();
  6706. if (escalatorRowsBefore != body.visualRowOrder.size()) {
  6707. fireRowVisibilityChangeEvent();
  6708. }
  6709. }
  6710. /**
  6711. * Returns the vertical scroll offset. Note that this is not necessarily the
  6712. * same as the {@code scrollTop} attribute in the DOM.
  6713. *
  6714. * @return the logical vertical scroll offset
  6715. */
  6716. public double getScrollTop() {
  6717. return verticalScrollbar.getScrollPos();
  6718. }
  6719. /**
  6720. * Sets the vertical scroll offset. Note that this will not necessarily
  6721. * become the same as the {@code scrollTop} attribute in the DOM.
  6722. *
  6723. * @param scrollTop
  6724. * the number of pixels to scroll vertically
  6725. */
  6726. public void setScrollTop(final double scrollTop) {
  6727. verticalScrollbar.setScrollPos(scrollTop);
  6728. }
  6729. /**
  6730. * Returns the logical horizontal scroll offset. Note that this is not
  6731. * necessarily the same as the {@code scrollLeft} attribute in the DOM.
  6732. *
  6733. * @return the logical horizontal scroll offset
  6734. */
  6735. public double getScrollLeft() {
  6736. return horizontalScrollbar.getScrollPos();
  6737. }
  6738. /**
  6739. * Sets the logical horizontal scroll offset. Note that will not necessarily
  6740. * become the same as the {@code scrollLeft} attribute in the DOM.
  6741. *
  6742. * @param scrollLeft
  6743. * the number of pixels to scroll horizontally
  6744. */
  6745. public void setScrollLeft(final double scrollLeft) {
  6746. horizontalScrollbar.setScrollPos(scrollLeft);
  6747. }
  6748. /**
  6749. * Returns the scroll width for the escalator. Note that this is not
  6750. * necessary the same as {@code Element.scrollWidth} in the DOM.
  6751. *
  6752. * @since 7.5.0
  6753. * @return the scroll width in pixels
  6754. */
  6755. public double getScrollWidth() {
  6756. return horizontalScrollbar.getScrollSize();
  6757. }
  6758. /**
  6759. * Returns the scroll height for the escalator. Note that this is not
  6760. * necessary the same as {@code Element.scrollHeight} in the DOM.
  6761. *
  6762. * @since 7.5.0
  6763. * @return the scroll height in pixels
  6764. */
  6765. public double getScrollHeight() {
  6766. return verticalScrollbar.getScrollSize();
  6767. }
  6768. /**
  6769. * Scrolls the body horizontally so that the column at the given index is
  6770. * visible and there is at least {@code padding} pixels in the direction of
  6771. * the given scroll destination.
  6772. *
  6773. * @param columnIndex
  6774. * the index of the column to scroll to
  6775. * @param destination
  6776. * where the column should be aligned visually after scrolling
  6777. * @param padding
  6778. * the number pixels to place between the scrolled-to column and
  6779. * the viewport edge.
  6780. * @throws IndexOutOfBoundsException
  6781. * if {@code columnIndex} is not a valid index for an existing
  6782. * column
  6783. * @throws IllegalArgumentException
  6784. * if {@code destination} is {@link ScrollDestination#MIDDLE}
  6785. * and padding is nonzero; or if the indicated column is frozen;
  6786. * or if {@code destination == null}
  6787. */
  6788. public void scrollToColumn(final int columnIndex,
  6789. final ScrollDestination destination, final int padding)
  6790. throws IndexOutOfBoundsException, IllegalArgumentException {
  6791. validateScrollDestination(destination, padding);
  6792. verifyValidColumnIndex(columnIndex);
  6793. if (columnIndex < columnConfiguration.frozenColumns) {
  6794. throw new IllegalArgumentException(
  6795. "The given column index " + columnIndex + " is frozen.");
  6796. }
  6797. scroller.scrollToColumn(columnIndex, destination, padding);
  6798. }
  6799. private void verifyValidColumnIndex(final int columnIndex)
  6800. throws IndexOutOfBoundsException {
  6801. if (columnIndex < 0
  6802. || columnIndex >= columnConfiguration.getColumnCount()) {
  6803. throw new IndexOutOfBoundsException("The given column index "
  6804. + columnIndex + " does not exist.");
  6805. }
  6806. }
  6807. /**
  6808. * Scrolls the body vertically so that the row at the given index is visible
  6809. * and there is at least {@literal padding} pixels to the given scroll
  6810. * destination.
  6811. *
  6812. * @param rowIndex
  6813. * the index of the logical row to scroll to
  6814. * @param destination
  6815. * where the row should be aligned visually after scrolling
  6816. * @param padding
  6817. * the number pixels to place between the scrolled-to row and the
  6818. * viewport edge.
  6819. * @throws IndexOutOfBoundsException
  6820. * if {@code rowIndex} is not a valid index for an existing row
  6821. * @throws IllegalArgumentException
  6822. * if {@code destination} is {@link ScrollDestination#MIDDLE}
  6823. * and padding is nonzero; or if {@code destination == null}
  6824. * @see #scrollToRowAndSpacer(int, ScrollDestination, int)
  6825. * @see #scrollToSpacer(int, ScrollDestination, int)
  6826. */
  6827. public void scrollToRow(final int rowIndex,
  6828. final ScrollDestination destination, final int padding)
  6829. throws IndexOutOfBoundsException, IllegalArgumentException {
  6830. verifyValidRowIndex(rowIndex);
  6831. body.scrollToRowSpacerOrBoth(rowIndex, destination, padding,
  6832. ScrollType.ROW);
  6833. }
  6834. private void verifyValidRowIndex(final int rowIndex) {
  6835. if (rowIndex < 0 || rowIndex >= body.getRowCount()) {
  6836. throw new IndexOutOfBoundsException(
  6837. "The given row index " + rowIndex + " does not exist.");
  6838. }
  6839. }
  6840. /**
  6841. * Scrolls the body vertically so that the spacer at the given row index is
  6842. * visible and there is at least {@literal padding} pixels to the given
  6843. * scroll destination.
  6844. *
  6845. * @since 7.5.0
  6846. * @param spacerIndex
  6847. * the row index of the spacer to scroll to
  6848. * @param destination
  6849. * where the spacer should be aligned visually after scrolling
  6850. * @param padding
  6851. * the number of pixels to place between the scrolled-to spacer
  6852. * and the viewport edge
  6853. * @throws IllegalArgumentException
  6854. * if {@code spacerIndex} is not an opened spacer; or if
  6855. * {@code destination} is {@link ScrollDestination#MIDDLE} and
  6856. * padding is nonzero; or if {@code destination == null}
  6857. * @see #scrollToRow(int, ScrollDestination, int)
  6858. * @see #scrollToRowAndSpacer(int, ScrollDestination, int)
  6859. */
  6860. public void scrollToSpacer(final int spacerIndex,
  6861. ScrollDestination destination, final int padding)
  6862. throws IllegalArgumentException {
  6863. body.scrollToRowSpacerOrBoth(spacerIndex, destination, padding,
  6864. ScrollType.SPACER);
  6865. }
  6866. /**
  6867. * Scrolls vertically to a row and the spacer below it.
  6868. * <p>
  6869. * If a spacer is not open at that index, this method behaves like
  6870. * {@link #scrollToRow(int, ScrollDestination, int)}
  6871. *
  6872. * @since 7.5.0
  6873. * @param rowIndex
  6874. * the index of the logical row to scroll to. -1 takes the
  6875. * topmost spacer into account as well.
  6876. * @param destination
  6877. * where the row should be aligned visually after scrolling
  6878. * @param padding
  6879. * the number pixels to place between the scrolled-to row and the
  6880. * viewport edge.
  6881. * @see #scrollToRow(int, ScrollDestination, int)
  6882. * @see #scrollToSpacer(int, ScrollDestination, int)
  6883. * @throws IllegalArgumentException
  6884. * if {@code destination} is {@link ScrollDestination#MIDDLE}
  6885. * and {@code padding} is not zero; or if {@code rowIndex} is
  6886. * not a valid row index, or -1; or if
  6887. * {@code destination == null}; or if {@code rowIndex == -1} and
  6888. * there is no spacer open at that index.
  6889. */
  6890. public void scrollToRowAndSpacer(final int rowIndex,
  6891. final ScrollDestination destination, final int padding)
  6892. throws IllegalArgumentException {
  6893. if (rowIndex != -1) {
  6894. verifyValidRowIndex(rowIndex);
  6895. }
  6896. body.scrollToRowSpacerOrBoth(rowIndex, destination, padding,
  6897. ScrollType.ROW_AND_SPACER);
  6898. }
  6899. private static void validateScrollDestination(
  6900. final ScrollDestination destination, final int padding) {
  6901. if (destination == null) {
  6902. throw new IllegalArgumentException("Destination cannot be null");
  6903. }
  6904. if (destination == ScrollDestination.MIDDLE && padding != 0) {
  6905. throw new IllegalArgumentException(
  6906. "You cannot have a padding with a MIDDLE destination");
  6907. }
  6908. }
  6909. /**
  6910. * Recalculates the dimensions for all elements that require manual
  6911. * calculations. Also updates the dimension caches.
  6912. * <p>
  6913. * <em>Note:</em> This method has the <strong>side-effect</strong>
  6914. * automatically makes sure that an appropriate amount of escalator rows are
  6915. * present. So, if the body area grows, more <strong>escalator rows might be
  6916. * inserted</strong>. Conversely, if the body area shrinks,
  6917. * <strong>escalator rows might be removed</strong>.
  6918. */
  6919. private void recalculateElementSizes() {
  6920. if (!isAttached()) {
  6921. return;
  6922. }
  6923. Profiler.enter("Escalator.recalculateElementSizes");
  6924. widthOfEscalator = Math.max(0, getBoundingWidth(getElement()));
  6925. heightOfEscalator = Math.max(0, getBoundingHeight(getElement()));
  6926. header.recalculateSectionHeight();
  6927. body.recalculateSectionHeight();
  6928. footer.recalculateSectionHeight();
  6929. scroller.recalculateScrollbarsForVirtualViewport();
  6930. body.verifyEscalatorCount();
  6931. body.reapplySpacerWidths();
  6932. Profiler.leave("Escalator.recalculateElementSizes");
  6933. }
  6934. /**
  6935. * Snap deltas of x and y to the major four axes (up, down, left, right)
  6936. * with a threshold of a number of degrees from those axes.
  6937. *
  6938. * @param deltaX
  6939. * the delta in the x axis
  6940. * @param deltaY
  6941. * the delta in the y axis
  6942. * @param thresholdRatio
  6943. * the threshold in ratio (0..1) between x and y for when to snap
  6944. * @return a two-element array: <code>[snappedX, snappedY]</code>
  6945. */
  6946. private static double[] snapDeltas(final double deltaX, final double deltaY,
  6947. final double thresholdRatio) {
  6948. final double[] array = new double[2];
  6949. if (deltaX != 0 && deltaY != 0) {
  6950. final double aDeltaX = Math.abs(deltaX);
  6951. final double aDeltaY = Math.abs(deltaY);
  6952. final double yRatio = aDeltaY / aDeltaX;
  6953. final double xRatio = aDeltaX / aDeltaY;
  6954. array[0] = (xRatio < thresholdRatio) ? 0 : deltaX;
  6955. array[1] = (yRatio < thresholdRatio) ? 0 : deltaY;
  6956. } else {
  6957. array[0] = deltaX;
  6958. array[1] = deltaY;
  6959. }
  6960. return array;
  6961. }
  6962. /**
  6963. * FOR INTERNAL USE ONLY, MAY GET REMOVED OR MODIFIED AT ANY TIME!
  6964. * <p>
  6965. * Adds an event handler that gets notified when the visibility of the
  6966. * vertical scrollbar changes.
  6967. *
  6968. * @param verticalScrollbarVisibilityChangeHandler
  6969. * the event handler
  6970. * @return a handler registration for the added handler
  6971. */
  6972. public HandlerRegistration addVerticalScrollbarVisibilityChangeHandler(
  6973. VerticalScrollbarVisibilityChangeHandler verticalScrollbarVisibilityChangeHandler) {
  6974. return addHandler(verticalScrollbarVisibilityChangeHandler,
  6975. VerticalScrollbarVisibilityChangeEvent.TYPE);
  6976. }
  6977. private void fireVerticalScrollbarVisibilityChangeEvent() {
  6978. fireEvent(new VerticalScrollbarVisibilityChangeEvent());
  6979. }
  6980. /**
  6981. * FOR INTERNAL USE ONLY, MAY GET REMOVED OR MODIFIED AT ANY TIME!
  6982. * <p>
  6983. * Adds an event handler that gets notified when the Escalator size changes.
  6984. *
  6985. * @param escalatorSizeChangeHandler
  6986. * the event handler
  6987. * @return a handler registration for the added handler
  6988. */
  6989. public HandlerRegistration addEscalatorSizeChangeHandler(
  6990. EscalatorSizeChangeHandler escalatorSizeChangeHandler) {
  6991. return addHandler(escalatorSizeChangeHandler,
  6992. EscalatorSizeChangeEvent.TYPE);
  6993. }
  6994. private void fireEscalatorSizeChangeEvent() {
  6995. fireEvent(new EscalatorSizeChangeEvent());
  6996. }
  6997. /**
  6998. * Adds an event handler that gets notified when the range of visible rows
  6999. * changes e.g. because of scrolling, row resizing or spacers
  7000. * appearing/disappearing.
  7001. *
  7002. * @param rowVisibilityChangeHandler
  7003. * the event handler
  7004. * @return a handler registration for the added handler
  7005. */
  7006. public HandlerRegistration addRowVisibilityChangeHandler(
  7007. RowVisibilityChangeHandler rowVisibilityChangeHandler) {
  7008. return addHandler(rowVisibilityChangeHandler,
  7009. RowVisibilityChangeEvent.TYPE);
  7010. }
  7011. private void fireRowVisibilityChangeEvent() {
  7012. if (!body.visualRowOrder.isEmpty()) {
  7013. int visibleRangeStart = body.getTopRowLogicalIndex();
  7014. int visibleRowCount = body.visualRowOrder.size();
  7015. fireEvent(new RowVisibilityChangeEvent(visibleRangeStart,
  7016. visibleRowCount));
  7017. } else {
  7018. fireEvent(new RowVisibilityChangeEvent(0, 0));
  7019. }
  7020. }
  7021. /**
  7022. * Gets the logical index range of currently visible rows.
  7023. *
  7024. * @return logical index range of visible rows
  7025. */
  7026. public Range getVisibleRowRange() {
  7027. if (!body.visualRowOrder.isEmpty()) {
  7028. return Range.withLength(body.getTopRowLogicalIndex(),
  7029. body.visualRowOrder.size());
  7030. } else {
  7031. return Range.withLength(0, 0);
  7032. }
  7033. }
  7034. /**
  7035. * Returns the widget from a cell node or <code>null</code> if there is no
  7036. * widget in the cell
  7037. *
  7038. * @param cellNode
  7039. * The cell node
  7040. */
  7041. static Widget getWidgetFromCell(Node cellNode) {
  7042. Node possibleWidgetNode = cellNode.getFirstChild();
  7043. if (possibleWidgetNode != null
  7044. && possibleWidgetNode.getNodeType() == Node.ELEMENT_NODE) {
  7045. @SuppressWarnings("deprecation")
  7046. com.google.gwt.user.client.Element castElement = (com.google.gwt.user.client.Element) possibleWidgetNode
  7047. .cast();
  7048. Widget w = WidgetUtil.findWidget(castElement);
  7049. // Ensure findWidget did not traverse past the cell element in the
  7050. // DOM hierarchy
  7051. if (cellNode.isOrHasChild(w.getElement())) {
  7052. return w;
  7053. }
  7054. }
  7055. return null;
  7056. }
  7057. @Override
  7058. public void setStylePrimaryName(String style) {
  7059. super.setStylePrimaryName(style);
  7060. verticalScrollbar.setStylePrimaryName(style);
  7061. horizontalScrollbar.setStylePrimaryName(style);
  7062. UIObject.setStylePrimaryName(tableWrapper, style + "-tablewrapper");
  7063. UIObject.setStylePrimaryName(headerDeco, style + "-header-deco");
  7064. UIObject.setStylePrimaryName(footerDeco, style + "-footer-deco");
  7065. UIObject.setStylePrimaryName(horizontalScrollbarDeco,
  7066. style + "-horizontal-scrollbar-deco");
  7067. UIObject.setStylePrimaryName(spacerDecoContainer,
  7068. style + "-spacer-deco-container");
  7069. header.setStylePrimaryName(style);
  7070. body.setStylePrimaryName(style);
  7071. footer.setStylePrimaryName(style);
  7072. }
  7073. /**
  7074. * Sets the number of rows that should be visible in Escalator's body, while
  7075. * {@link #getHeightMode()} is {@link HeightMode#ROW}.
  7076. * <p>
  7077. * If Escalator is currently not in {@link HeightMode#ROW}, the given value
  7078. * is remembered, and applied once the mode is applied.
  7079. *
  7080. * @param rows
  7081. * the number of rows that should be visible in Escalator's body
  7082. * @throws IllegalArgumentException
  7083. * if {@code rows} is &leq; 0, {@link Double#isInfinite(double)
  7084. * infinite} or {@link Double#isNaN(double) NaN}.
  7085. * @see #setHeightMode(HeightMode)
  7086. */
  7087. public void setHeightByRows(double rows) throws IllegalArgumentException {
  7088. if (heightMode == HeightMode.UNDEFINED && body.insertingOrRemoving) {
  7089. // this will be called again once the operation is finished, ignore
  7090. // for now
  7091. return;
  7092. }
  7093. if (rows < 0) {
  7094. throw new IllegalArgumentException(
  7095. "The number of rows must be a positive number.");
  7096. } else if (Double.isInfinite(rows)) {
  7097. throw new IllegalArgumentException(
  7098. "The number of rows must be finite.");
  7099. } else if (Double.isNaN(rows)) {
  7100. throw new IllegalArgumentException("The number must not be NaN.");
  7101. }
  7102. heightByRows = rows;
  7103. applyHeightByRows();
  7104. }
  7105. /**
  7106. * Gets the amount of rows in Escalator's body that are shown, while
  7107. * {@link #getHeightMode()} is {@link HeightMode#ROW}.
  7108. * <p>
  7109. * By default, it is 10.
  7110. *
  7111. * @return the amount of rows that are being shown in Escalator's body
  7112. * @see #setHeightByRows(double)
  7113. */
  7114. public double getHeightByRows() {
  7115. return heightByRows;
  7116. }
  7117. /**
  7118. * Reapplies the row-based height of the Grid, if Grid currently should
  7119. * define its height that way.
  7120. */
  7121. private void applyHeightByRows() {
  7122. if (heightMode != HeightMode.ROW
  7123. && heightMode != HeightMode.UNDEFINED) {
  7124. return;
  7125. }
  7126. double headerHeight = header.getHeightOfSection();
  7127. double footerHeight = footer.getHeightOfSection();
  7128. double bodyHeight = body.getDefaultRowHeight() * heightByRows;
  7129. double scrollbar = horizontalScrollbar.showsScrollHandle()
  7130. ? horizontalScrollbar.getScrollbarThickness()
  7131. : 0;
  7132. double spacerHeight = 0; // ignored if HeightMode.ROW
  7133. if (heightMode == HeightMode.UNDEFINED) {
  7134. spacerHeight = body.spacerContainer.getSpacerHeightsSum();
  7135. }
  7136. double totalHeight = headerHeight + bodyHeight + spacerHeight
  7137. + scrollbar + footerHeight;
  7138. setHeightInternal(totalHeight + "px");
  7139. }
  7140. /**
  7141. * Defines the mode in which the Escalator widget's height is calculated.
  7142. * <p>
  7143. * If {@link HeightMode#CSS} is given, Escalator will respect the values
  7144. * given via {@link #setHeight(String)}, and behave as a traditional Widget.
  7145. * <p>
  7146. * If {@link HeightMode#ROW} is given, Escalator will make sure that the
  7147. * {@link #getBody() body} will display as many rows as
  7148. * {@link #getHeightByRows()} defines. <em>Note:</em> If headers/footers are
  7149. * inserted or removed, the widget will resize itself to still display the
  7150. * required amount of rows in its body. It also takes the horizontal
  7151. * scrollbar into account.
  7152. *
  7153. * @param heightMode
  7154. * the mode in to which Escalator should be set
  7155. */
  7156. public void setHeightMode(HeightMode heightMode) {
  7157. /*
  7158. * This method is a workaround for the fact that Vaadin re-applies
  7159. * widget dimensions (height/width) on each state change event. The
  7160. * original design was to have setHeight an setHeightByRow be equals,
  7161. * and whichever was called the latest was considered in effect.
  7162. *
  7163. * But, because of Vaadin always calling setHeight on the widget, this
  7164. * approach doesn't work.
  7165. */
  7166. if (heightMode != this.heightMode) {
  7167. this.heightMode = heightMode;
  7168. switch (this.heightMode) {
  7169. case CSS:
  7170. setHeight(heightByCss);
  7171. break;
  7172. case ROW:
  7173. setHeightByRows(heightByRows);
  7174. break;
  7175. case UNDEFINED:
  7176. setHeightByRows(body.getRowCount());
  7177. break;
  7178. default:
  7179. throw new IllegalStateException("Unimplemented feature "
  7180. + "- unknown HeightMode: " + this.heightMode);
  7181. }
  7182. }
  7183. }
  7184. /**
  7185. * Returns the current {@link HeightMode} the Escalator is in.
  7186. * <p>
  7187. * Defaults to {@link HeightMode#CSS}.
  7188. *
  7189. * @return the current HeightMode
  7190. */
  7191. public HeightMode getHeightMode() {
  7192. return heightMode;
  7193. }
  7194. /**
  7195. * Returns the {@link RowContainer} which contains the element.
  7196. *
  7197. * @param element
  7198. * the element to check for
  7199. * @return the container the element is in or <code>null</code> if element
  7200. * is not present in any container.
  7201. */
  7202. public RowContainer findRowContainer(Element element) {
  7203. if (getHeader().getElement() != element
  7204. && getHeader().getElement().isOrHasChild(element)) {
  7205. return getHeader();
  7206. } else if (getBody().getElement() != element
  7207. && getBody().getElement().isOrHasChild(element)) {
  7208. return getBody();
  7209. } else if (getFooter().getElement() != element
  7210. && getFooter().getElement().isOrHasChild(element)) {
  7211. return getFooter();
  7212. }
  7213. return null;
  7214. }
  7215. /**
  7216. * Sets whether a scroll direction is locked or not.
  7217. * <p>
  7218. * If a direction is locked, the escalator will refuse to scroll in that
  7219. * direction.
  7220. *
  7221. * @param direction
  7222. * the orientation of the scroll to set the lock status
  7223. * @param locked
  7224. * <code>true</code> to lock, <code>false</code> to unlock
  7225. */
  7226. public void setScrollLocked(ScrollbarBundle.Direction direction,
  7227. boolean locked) {
  7228. switch (direction) {
  7229. case HORIZONTAL:
  7230. horizontalScrollbar.setLocked(locked);
  7231. break;
  7232. case VERTICAL:
  7233. verticalScrollbar.setLocked(locked);
  7234. break;
  7235. default:
  7236. throw new UnsupportedOperationException(
  7237. "Unexpected value: " + direction);
  7238. }
  7239. }
  7240. /**
  7241. * Checks whether or not an direction is locked for scrolling.
  7242. *
  7243. * @param direction
  7244. * the direction of the scroll of which to check the lock status
  7245. * @return <code>true</code> if the direction is locked
  7246. */
  7247. public boolean isScrollLocked(ScrollbarBundle.Direction direction) {
  7248. switch (direction) {
  7249. case HORIZONTAL:
  7250. return horizontalScrollbar.isLocked();
  7251. case VERTICAL:
  7252. return verticalScrollbar.isLocked();
  7253. default:
  7254. throw new UnsupportedOperationException(
  7255. "Unexpected value: " + direction);
  7256. }
  7257. }
  7258. /**
  7259. * Adds a scroll handler to this escalator.
  7260. *
  7261. * @param handler
  7262. * the scroll handler to add
  7263. * @return a handler registration for the registered scroll handler
  7264. */
  7265. public HandlerRegistration addScrollHandler(ScrollHandler handler) {
  7266. return addHandler(handler, ScrollEvent.TYPE);
  7267. }
  7268. /**
  7269. * Returns true if the Escalator is currently scrolling by touch, or has not
  7270. * made the decision yet whether to accept touch actions as scrolling or
  7271. * not.
  7272. *
  7273. * @see #setDelayToCancelTouchScroll(double)
  7274. *
  7275. * @return true when the component is touch scrolling at the moment
  7276. * @since 8.1
  7277. */
  7278. public boolean isTouchScrolling() {
  7279. return scroller.touchHandlerBundle.touching;
  7280. }
  7281. /**
  7282. * Returns the time after which to not consider a touch event a scroll event
  7283. * if the user has not moved the touch. This can be used to differentiate
  7284. * between quick touch move (scrolling) and long tap (e.g. context menu or
  7285. * drag and drop operation).
  7286. *
  7287. * @return delay in milliseconds after which to cancel touch scrolling if
  7288. * there is no movement, -1 means scrolling is always allowed
  7289. * @since 8.1
  7290. */
  7291. public double getDelayToCancelTouchScroll() {
  7292. return delayToCancelTouchScroll;
  7293. }
  7294. /**
  7295. * Sets the time after which to not consider a touch event a scroll event if
  7296. * the user has not moved the touch. This can be used to differentiate
  7297. * between quick touch move (scrolling) and long tap (e.g. context menu or
  7298. * drag and drop operation).
  7299. *
  7300. * @param delayToCancelTouchScroll
  7301. * delay in milliseconds after which to cancel touch scrolling if
  7302. * there is no movement, -1 to always allow scrolling
  7303. * @since 8.1
  7304. */
  7305. public void setDelayToCancelTouchScroll(double delayToCancelTouchScroll) {
  7306. this.delayToCancelTouchScroll = delayToCancelTouchScroll;
  7307. }
  7308. @Override
  7309. public boolean isWorkPending() {
  7310. return body.domSorter.waiting || verticalScrollbar.isWorkPending()
  7311. || horizontalScrollbar.isWorkPending() || layoutIsScheduled;
  7312. }
  7313. @Override
  7314. public void onResize() {
  7315. if (isAttached() && !layoutIsScheduled) {
  7316. layoutIsScheduled = true;
  7317. Scheduler.get().scheduleFinally(layoutCommand);
  7318. }
  7319. }
  7320. /**
  7321. * Gets the maximum number of body rows that can be visible on the screen at
  7322. * once.
  7323. *
  7324. * @return the maximum capacity
  7325. */
  7326. public int getMaxVisibleRowCount() {
  7327. return body.getMaxVisibleRowCount();
  7328. }
  7329. /**
  7330. * Gets the escalator's inner width. This is the entire width in pixels,
  7331. * without the vertical scrollbar.
  7332. *
  7333. * @return escalator's inner width
  7334. */
  7335. public double getInnerWidth() {
  7336. return getBoundingWidth(tableWrapper);
  7337. }
  7338. /**
  7339. * Gets the escalator's inner height. This is the entire height in pixels,
  7340. * without the horizontal scrollbar.
  7341. *
  7342. * @return escalator's inner height
  7343. */
  7344. public double getInnerHeight() {
  7345. return getBoundingHeight(tableWrapper);
  7346. }
  7347. /**
  7348. * FOR INTERNAL USE ONLY, MAY GET REMOVED OR MODIFIED AT ANY TIME!
  7349. * <p>
  7350. * Clears the computed styles.
  7351. */
  7352. void clearComputedStyles() {
  7353. computedStyleMap.clear();
  7354. }
  7355. /**
  7356. * Resets all cached pixel sizes and reads new values from the DOM. This
  7357. * methods should be used e.g. when styles affecting the dimensions of
  7358. * elements in this escalator have been changed.
  7359. */
  7360. public void resetSizesFromDom() {
  7361. header.autodetectRowHeightNow();
  7362. body.autodetectRowHeightNow();
  7363. footer.autodetectRowHeightNow();
  7364. for (int i = 0; i < columnConfiguration.getColumnCount(); i++) {
  7365. columnConfiguration.setColumnWidth(i,
  7366. columnConfiguration.getColumnWidth(i));
  7367. }
  7368. }
  7369. private Range getViewportPixels() {
  7370. int from = (int) Math.floor(verticalScrollbar.getScrollPos());
  7371. int to = (int) body.getHeightOfSection();
  7372. return Range.withLength(from, to);
  7373. }
  7374. @Override
  7375. @SuppressWarnings("deprecation")
  7376. public com.google.gwt.user.client.Element getSubPartElement(
  7377. String subPart) {
  7378. SubPartArguments args = SubPartArguments.create(subPart);
  7379. Element tableStructureElement = getSubPartElementTableStructure(args);
  7380. if (tableStructureElement != null) {
  7381. return DOM.asOld(tableStructureElement);
  7382. }
  7383. Element spacerElement = getSubPartElementSpacer(args);
  7384. if (spacerElement != null) {
  7385. return DOM.asOld(spacerElement);
  7386. }
  7387. return null;
  7388. }
  7389. /**
  7390. * Returns the {@code <div class="{primary-stylename}-tablewrapper" />}
  7391. * element which has the table inside it. {primary-stylename} is .e.g
  7392. * {@code v-grid}.
  7393. * <p>
  7394. * <em>NOTE: you should not do any modifications to the returned element.
  7395. * This API is only available for querying data from the element.</em>
  7396. *
  7397. * @return the table wrapper element
  7398. * @since 8.1
  7399. */
  7400. public Element getTableWrapper() {
  7401. return tableWrapper;
  7402. }
  7403. /**
  7404. * Returns the <code>&lt;table&gt;</code> element of the grid.
  7405. *
  7406. * @return the table element
  7407. * @since 8.2
  7408. */
  7409. public Element getTable() {
  7410. return table;
  7411. }
  7412. private Element getSubPartElementTableStructure(SubPartArguments args) {
  7413. String type = args.getType();
  7414. int[] indices = args.getIndices();
  7415. // Get correct RowContainer for type from Escalator
  7416. RowContainer container = null;
  7417. if (type.equalsIgnoreCase("header")) {
  7418. container = getHeader();
  7419. } else if (type.equalsIgnoreCase("cell")) {
  7420. if (indices.length > 0) {
  7421. // If wanted row is not visible, we need to scroll there.
  7422. // Scrolling might be a no-op if row is already in the viewport.
  7423. scrollToRow(indices[0], ScrollDestination.ANY, 0);
  7424. }
  7425. container = getBody();
  7426. } else if (type.equalsIgnoreCase("footer")) {
  7427. container = getFooter();
  7428. }
  7429. if (null != container) {
  7430. if (indices.length == 0) {
  7431. // No indexing. Just return the wanted container element
  7432. return container.getElement();
  7433. } else {
  7434. try {
  7435. return getSubPart(container, indices);
  7436. } catch (Exception e) {
  7437. getLogger().log(Level.SEVERE, e.getMessage());
  7438. }
  7439. }
  7440. }
  7441. return null;
  7442. }
  7443. private Element getSubPart(RowContainer container, int[] indices) {
  7444. Element targetElement = container.getRowElement(indices[0]);
  7445. // Scroll wanted column to view if able
  7446. if (indices.length > 1 && targetElement != null) {
  7447. if (getColumnConfiguration().getFrozenColumnCount() <= indices[1]) {
  7448. scrollToColumn(indices[1], ScrollDestination.ANY, 0);
  7449. }
  7450. targetElement = getCellFromRow(TableRowElement.as(targetElement),
  7451. indices[1]);
  7452. for (int i = 2; i < indices.length && targetElement != null; ++i) {
  7453. targetElement = (Element) targetElement.getChild(indices[i]);
  7454. }
  7455. }
  7456. return targetElement;
  7457. }
  7458. private static Element getCellFromRow(TableRowElement rowElement,
  7459. int index) {
  7460. int childCount = rowElement.getCells().getLength();
  7461. if (index < 0 || index >= childCount) {
  7462. return null;
  7463. }
  7464. TableCellElement currentCell = null;
  7465. boolean indexInColspan = false;
  7466. int i = 0;
  7467. while (!indexInColspan) {
  7468. currentCell = rowElement.getCells().getItem(i);
  7469. // Calculate if this is the cell we are looking for
  7470. int colSpan = currentCell.getColSpan();
  7471. indexInColspan = index < colSpan + i;
  7472. // Increment by colspan to skip over hidden cells
  7473. i += colSpan;
  7474. }
  7475. return currentCell;
  7476. }
  7477. private Element getSubPartElementSpacer(SubPartArguments args) {
  7478. if ("spacer".equals(args.getType()) && args.getIndicesLength() == 1) {
  7479. // If spacer's row is not visible, we need to scroll there.
  7480. // Scrolling might be a no-op if row is already in the viewport.
  7481. scrollToSpacer(args.getIndex(0), ScrollDestination.ANY, 0);
  7482. return body.spacerContainer.getSubPartElement(args.getIndex(0));
  7483. } else {
  7484. return null;
  7485. }
  7486. }
  7487. @Override
  7488. @SuppressWarnings("deprecation")
  7489. public String getSubPartName(
  7490. com.google.gwt.user.client.Element subElement) {
  7491. /*
  7492. * The spacer check needs to be before table structure check, because
  7493. * (for now) the table structure will take spacer elements into account
  7494. * as well, when it shouldn't.
  7495. */
  7496. String spacer = getSubPartNameSpacer(subElement);
  7497. if (spacer != null) {
  7498. return spacer;
  7499. }
  7500. String tableStructure = getSubPartNameTableStructure(subElement);
  7501. if (tableStructure != null) {
  7502. return tableStructure;
  7503. }
  7504. return null;
  7505. }
  7506. private String getSubPartNameTableStructure(Element subElement) {
  7507. List<RowContainer> containers = Arrays.asList(getHeader(), getBody(),
  7508. getFooter());
  7509. List<String> containerType = Arrays.asList("header", "cell", "footer");
  7510. for (int i = 0; i < containers.size(); ++i) {
  7511. RowContainer container = containers.get(i);
  7512. boolean containerRow = (subElement.getTagName()
  7513. .equalsIgnoreCase("tr")
  7514. && subElement.getParentElement() == container.getElement());
  7515. if (containerRow) {
  7516. /*
  7517. * Wanted SubPart is row that is a child of containers root to
  7518. * get indices, we use a cell that is a child of this row
  7519. */
  7520. subElement = subElement.getFirstChildElement();
  7521. }
  7522. Cell cell = container.getCell(subElement);
  7523. if (cell != null) {
  7524. // Skip the column index if subElement was a child of root
  7525. return containerType.get(i) + "[" + cell.getRow()
  7526. + (containerRow ? "]" : "][" + cell.getColumn() + "]");
  7527. }
  7528. }
  7529. return null;
  7530. }
  7531. private String getSubPartNameSpacer(Element subElement) {
  7532. return body.spacerContainer.getSubPartName(subElement);
  7533. }
  7534. private void logWarning(String message) {
  7535. getLogger().warning(message);
  7536. }
  7537. /**
  7538. * This is an internal method for calculating minimum width for Column
  7539. * resize.
  7540. *
  7541. * @return minimum width for column
  7542. */
  7543. double getMinCellWidth(int colIndex) {
  7544. return columnConfiguration.getMinCellWidth(colIndex);
  7545. }
  7546. /**
  7547. * Internal method for checking whether the browser is IE11 or Edge
  7548. *
  7549. * @return true only if the current browser is IE11, or Edge
  7550. */
  7551. private static boolean isCurrentBrowserIE11OrEdge() {
  7552. return BrowserInfo.get().isIE11() || BrowserInfo.get().isEdge();
  7553. }
  7554. }