123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993 |
- /*
- * Copyright 2000-2013 Vaadin Ltd.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
- package com.vaadin.client.ui.grid;
-
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.ListIterator;
- import java.util.Map;
- import java.util.logging.Level;
- import java.util.logging.Logger;
-
- import com.google.gwt.animation.client.AnimationScheduler;
- import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
- import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
- import com.google.gwt.core.client.Duration;
- import com.google.gwt.core.client.JavaScriptObject;
- import com.google.gwt.dom.client.Document;
- import com.google.gwt.dom.client.Element;
- import com.google.gwt.dom.client.NativeEvent;
- import com.google.gwt.dom.client.Node;
- import com.google.gwt.dom.client.NodeList;
- import com.google.gwt.dom.client.Style;
- import com.google.gwt.dom.client.Style.Display;
- import com.google.gwt.dom.client.Style.Unit;
- import com.google.gwt.event.shared.HandlerRegistration;
- import com.google.gwt.logging.client.LogConfiguration;
- import com.google.gwt.user.client.DOM;
- import com.google.gwt.user.client.Window;
- import com.google.gwt.user.client.ui.UIObject;
- import com.google.gwt.user.client.ui.Widget;
- import com.vaadin.client.Profiler;
- import com.vaadin.client.Util;
- import com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle;
- import com.vaadin.client.ui.grid.PositionFunction.AbsolutePosition;
- import com.vaadin.client.ui.grid.PositionFunction.Translate3DPosition;
- import com.vaadin.client.ui.grid.PositionFunction.TranslatePosition;
- import com.vaadin.client.ui.grid.PositionFunction.WebkitTranslate3DPosition;
- import com.vaadin.client.ui.grid.ScrollbarBundle.HorizontalScrollbarBundle;
- import com.vaadin.client.ui.grid.ScrollbarBundle.VerticalScrollbarBundle;
- import com.vaadin.shared.ui.grid.Range;
- import com.vaadin.shared.ui.grid.ScrollDestination;
- import com.vaadin.shared.util.SharedUtil;
-
- /*-
-
- Maintenance Notes! Reading these might save your day.
-
-
- == Row Container Structure
-
- AbstractRowContainer
- |-- AbstractStaticRowContainer
- | |-- HeaderRowContainer
- | `-- FooterContainer
- `-- BodyRowContainer
-
- AbstractRowContainer is intended to contain all common logic
- between RowContainers. It manages the bookkeeping of row
- count, makes sure that all individual cells are rendered
- the same way, and so on.
-
- AbstractStaticRowContainer has some special logic that is
- required by all RowContainers that don't scroll (hence the
- word "static"). HeaderRowContainer and FooterRowContainer
- are pretty thin special cases of a StaticRowContainer
- (mostly relating to positioning of the root element).
-
- BodyRowContainer could also be split into an additional
- "AbstractScrollingRowContainer", but I felt that no more
- inner classes were needed. So it contains both logic
- required for making things scroll about, and equivalent
- special cases for layouting, as are found in
- Header/FooterRowContainers.
-
-
- == The Three Indices
-
- Each RowContainer can be thought to have three levels of
- indices for any given displayed row (but the distinction
- matters primarily for the BodyRowContainer, because of the
- way it scrolls through data):
-
- - Logical index
- - Physical (or DOM) index
- - Visual index
-
- LOGICAL INDEX is the index that is linked to the data
- source. If you want your data source to represent a SQL
- database with 10 000 rows, the 7 000:th row in the SQL has a
- logical index of 6 999, since the index is 0-based (unless
- that data source does some funky logic).
-
- PHYSICAL INDEX is the index for a row that you see in a
- browser's DOM inspector. If your row is the second <tr>
- element within a <tbody> tag, it has a physical index of 1
- (because of 0-based indices). In Header and
- FooterRowContainers, you are safe to assume that the logical
- index is the same as the physical index. But because the
- BodyRowContainer never displays large data sources entirely
- in the DOM, a physical index usually has no apparent direct
- relationship with its logical index.
-
- VISUAL INDEX is the index relating to the order that you
- see a row in, in the browser, as it is rendered. The
- topmost row is 0, the second is 1, and so on. The visual
- index is similar to the physical index in the sense that
- Header and FooterRowContainers can assume a 1:1
- relationship between visual index and logical index. And
- again, BodyRowContainer has no such relationship. The
- body's visual index has additionally no apparent
- relationship with its physical index. Because the <tr> tags
- are reused in the body and visually repositioned with CSS
- as the user scrolls, the relationship between physical
- index and visual index is quickly broken. You can get an
- element's visual index via the field
- BodyRowContainer.visualRowOrder.
-
- */
-
- /**
- * A workaround-class for GWT and JSNI.
- * <p>
- * GWT is unable to handle some method calls to Java methods in inner-classes
- * from within JSNI blocks. Having that inner class implement a non-inner-class
- * (or interface), makes it possible for JSNI to indirectly refer to the inner
- * class, by invoking methods and fields in the non-inner-class.
- *
- * @see Escalator.Scroller
- */
- abstract class JsniWorkaround {
- /**
- * A JavaScript function that handles the scroll DOM event, and passes it on
- * to Java code.
- *
- * @see #createScrollListenerFunction(Escalator)
- * @see Escalator#onScroll(double,double)
- * @see Escalator.Scroller#onScroll(double, double)
- */
- protected final JavaScriptObject scrollListenerFunction;
-
- /**
- * A JavaScript function that handles the mousewheel DOM event, and passes
- * it on to Java code.
- *
- * @see #createMousewheelListenerFunction(Escalator)
- * @see Escalator#onScroll(double,double)
- * @see Escalator.Scroller#onScroll(double, double)
- */
- protected final JavaScriptObject mousewheelListenerFunction;
-
- /**
- * A JavaScript function that handles the touch start DOM event, and passes
- * it on to Java code.
- *
- * @see TouchHandlerBundle#touchStart(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
- */
- protected JavaScriptObject touchStartFunction;
-
- /**
- * A JavaScript function that handles the touch move DOM event, and passes
- * it on to Java code.
- *
- * @see TouchHandlerBundle#touchMove(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
- */
- protected JavaScriptObject touchMoveFunction;
-
- /**
- * A JavaScript function that handles the touch end and cancel DOM events,
- * and passes them on to Java code.
- *
- * @see TouchHandlerBundle#touchEnd(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
- */
- protected JavaScriptObject touchEndFunction;
-
- protected JsniWorkaround(final Escalator escalator) {
- scrollListenerFunction = createScrollListenerFunction(escalator);
- mousewheelListenerFunction = createMousewheelListenerFunction(escalator);
-
- final TouchHandlerBundle bundle = new TouchHandlerBundle(escalator);
- touchStartFunction = bundle.getTouchStartHandler();
- touchMoveFunction = bundle.getTouchMoveHandler();
- touchEndFunction = bundle.getTouchEndHandler();
- }
-
- /**
- * A method that constructs the JavaScript function that will be stored into
- * {@link #scrollListenerFunction}.
- *
- * @param esc
- * a reference to the current instance of {@link Escalator}
- * @see Escalator#onScroll(double,double)
- */
- protected abstract JavaScriptObject createScrollListenerFunction(
- Escalator esc);
-
- /**
- * A method that constructs the JavaScript function that will be stored into
- * {@link #mousewheelListenerFunction}.
- *
- * @param esc
- * a reference to the current instance of {@link Escalator}
- * @see Escalator#onScroll(double,double)
- */
- protected abstract JavaScriptObject createMousewheelListenerFunction(
- Escalator esc);
- }
-
- /**
- * A low-level table-like widget that features a scrolling virtual viewport and
- * lazily generated rows.
- *
- * @since 7.2
- * @author Vaadin Ltd
- */
- public class Escalator extends Widget {
-
- // todo comments legend
- /*
- * [[optimize]]: There's an opportunity to rewrite the code in such a way
- * that it _might_ perform better (rememeber to measure, implement,
- * re-measure)
- */
- /*
- * [[rowheight]]: This code will require alterations that are relevant for
- * being able to support variable row heights. NOTE: these bits can most
- * often also be identified by searching for code reading the ROW_HEIGHT_PX
- * constant.
- */
- /*
- * [[API]]: Implementing this suggestion would require a change in the
- * public API. These suggestions usually don't come lightly.
- */
- /*
- * [[mpixscroll]]: This code will require alterations that are relevant for
- * supporting the scrolling through more pixels than some browsers normally
- * would support. (i.e. when we support more than "a million" pixels in the
- * escalator DOM). NOTE: these bits can most often also be identified by
- * searching for code that call scrollElem.getScrollTop();.
- */
-
- /**
- * A utility class that contains utility methods that are usually called
- * from JSNI.
- * <p>
- * The methods are moved in this class to minimize the amount of JSNI code
- * as much as feasible.
- */
- static class JsniUtil {
- public static class TouchHandlerBundle {
-
- /**
- * A <a href=
- * "http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsOverlay.html"
- * >JavaScriptObject overlay</a> for the <a
- * href="http://www.w3.org/TR/touch-events/">JavaScript
- * TouchEvent</a> object.
- * <p>
- * This needs to be used in the touch event handlers, since GWT's
- * {@link com.google.gwt.event.dom.client.TouchEvent TouchEvent}
- * can't be cast from the JSNI call, and the
- * {@link com.google.gwt.dom.client.NativeEvent NativeEvent} isn't
- * properly populated with the correct values.
- */
- private final static class CustomTouchEvent extends
- JavaScriptObject {
- protected CustomTouchEvent() {
- }
-
- public native NativeEvent getNativeEvent()
- /*-{
- return this;
- }-*/;
-
- public native int getPageX()
- /*-{
- return this.targetTouches[0].pageX;
- }-*/;
-
- public native int getPageY()
- /*-{
- return this.targetTouches[0].pageY;
- }-*/;
- }
-
- private double touches = 0;
- private int lastX = 0;
- private int lastY = 0;
- private double lastTime = 0;
- private boolean snappedScrollEnabled = true;
- private double deltaX = 0;
- private double deltaY = 0;
-
- private final Escalator escalator;
- private CustomTouchEvent latestTouchMoveEvent;
- private AnimationCallback mover = new AnimationCallback() {
- @Override
- public void execute(double timestamp) {
- if (touches != 1) {
- return;
- }
-
- final int x = latestTouchMoveEvent.getPageX();
- final int y = latestTouchMoveEvent.getPageY();
- deltaX = x - lastX;
- deltaY = y - lastY;
- lastX = x;
- lastY = y;
- lastTime = timestamp;
-
- // snap the scroll to the major axes, at first.
- if (snappedScrollEnabled) {
- final double oldDeltaX = deltaX;
- final double oldDeltaY = deltaY;
-
- /*
- * Scrolling snaps to 40 degrees vs. flick scroll's 30
- * degrees, since slow movements have poor resolution -
- * it's easy to interpret a slight angle as a steep
- * angle, since the sample rate is "unnecessarily" high.
- * 40 simply felt better than 30.
- */
- final double[] snapped = Escalator.snapDeltas(deltaX,
- deltaY, RATIO_OF_40_DEGREES);
- deltaX = snapped[0];
- deltaY = snapped[1];
-
- /*
- * if the snap failed once, let's follow the pointer
- * from now on.
- */
- if (oldDeltaX != 0 && deltaX == oldDeltaX
- && oldDeltaY != 0 && deltaY == oldDeltaY) {
- snappedScrollEnabled = false;
- }
- }
-
- moveScrollFromEvent(escalator, -deltaX, -deltaY,
- latestTouchMoveEvent.getNativeEvent());
- }
- };
- private AnimationHandle animationHandle;
-
- public TouchHandlerBundle(final Escalator escalator) {
- this.escalator = escalator;
- }
-
- public native JavaScriptObject getTouchStartHandler()
- /*-{
- // we need to store "this", since it won't be preserved on call.
- var self = this;
- return $entry(function (e) {
- self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchStart(*)(e);
- });
- }-*/;
-
- public native JavaScriptObject getTouchMoveHandler()
- /*-{
- // we need to store "this", since it won't be preserved on call.
- var self = this;
- return $entry(function (e) {
- self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchMove(*)(e);
- });
- }-*/;
-
- public native JavaScriptObject getTouchEndHandler()
- /*-{
- // we need to store "this", since it won't be preserved on call.
- var self = this;
- return $entry(function (e) {
- self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchEnd(*)(e);
- });
- }-*/;
-
- public void touchStart(final CustomTouchEvent event) {
- touches++;
- if (touches != 1) {
- return;
- }
-
- escalator.scroller.cancelFlickScroll();
-
- lastX = event.getPageX();
- lastY = event.getPageY();
-
- snappedScrollEnabled = true;
- }
-
- public void touchMove(final CustomTouchEvent event) {
- /*
- * since we only use the getPageX/Y, and calculate the diff
- * within the handler, we don't need to calculate any
- * intermediate deltas.
- */
- latestTouchMoveEvent = event;
-
- if (animationHandle != null) {
- animationHandle.cancel();
- }
- animationHandle = AnimationScheduler.get()
- .requestAnimationFrame(mover, escalator.bodyElem);
- event.getNativeEvent().preventDefault();
- mover.execute(Duration.currentTimeMillis());
- }
-
- public void touchEnd(@SuppressWarnings("unused")
- final CustomTouchEvent event) {
- touches--;
-
- if (touches == 0) {
- escalator.scroller.handleFlickScroll(deltaX, deltaY,
- lastTime);
- }
- }
- }
-
- public static void moveScrollFromEvent(final Escalator escalator,
- final double deltaX, final double deltaY,
- final NativeEvent event) {
-
- if (!Double.isNaN(deltaX)) {
- escalator.horizontalScrollbar.setScrollPosByDelta((int) deltaX);
- }
-
- if (!Double.isNaN(deltaY)) {
- escalator.verticalScrollbar.setScrollPosByDelta((int) deltaY);
- }
-
- /*
- * TODO: only prevent if not scrolled to end/bottom. Or no? UX team
- * needs to decide.
- */
- final boolean warrantedYScroll = deltaY != 0
- && escalator.verticalScrollbar.showsScrollHandle();
- final boolean warrantedXScroll = deltaX != 0
- && escalator.horizontalScrollbar.showsScrollHandle();
- if (warrantedYScroll || warrantedXScroll) {
- event.preventDefault();
- }
- }
- }
-
- /**
- * The animation callback that handles the animation of a touch-scrolling
- * flick with inertia.
- */
- private class FlickScrollAnimator implements AnimationCallback {
- private static final double MIN_MAGNITUDE = 0.005;
- private static final double MAX_SPEED = 7;
-
- private double velX;
- private double velY;
- private double prevTime = 0;
- private int millisLeft;
- private double xFric;
- private double yFric;
-
- private boolean cancelled = false;
-
- /**
- * Creates a new animation callback to handle touch-scrolling flick with
- * inertia.
- *
- * @param deltaX
- * the last scrolling delta in the x-axis in a touchmove
- * @param deltaY
- * the last scrolling delta in the y-axis in a touchmove
- * @param lastTime
- * the timestamp of the last touchmove
- */
- public FlickScrollAnimator(final double deltaX, final double deltaY,
- final double lastTime) {
- final double currentTimeMillis = Duration.currentTimeMillis();
- velX = Math.max(Math.min(deltaX / (currentTimeMillis - lastTime),
- MAX_SPEED), -MAX_SPEED);
- velY = Math.max(Math.min(deltaY / (currentTimeMillis - lastTime),
- MAX_SPEED), -MAX_SPEED);
- prevTime = lastTime;
-
- /*
- * If we're scrolling mainly in one of the four major directions,
- * and only a teeny bit to any other side, snap the scroll to that
- * major direction instead.
- */
- final double[] snapDeltas = Escalator.snapDeltas(velX, velY,
- RATIO_OF_30_DEGREES);
- velX = snapDeltas[0];
- velY = snapDeltas[1];
-
- if (velX * velX + velY * velY > MIN_MAGNITUDE) {
- millisLeft = 1500;
- xFric = velX / millisLeft;
- yFric = velY / millisLeft;
- } else {
- millisLeft = 0;
- }
-
- }
-
- @Override
- public void execute(final double timestamp) {
- if (millisLeft <= 0 || cancelled) {
- scroller.currentFlickScroller = null;
- return;
- }
-
- final int lastLeft = tBodyScrollLeft;
- final int lastTop = tBodyScrollTop;
-
- final double timeDiff = timestamp - prevTime;
- setScrollLeft((int) (tBodyScrollLeft - velX * timeDiff));
- velX -= xFric * timeDiff;
-
- setScrollTop(tBodyScrollTop - velY * timeDiff);
- velY -= yFric * timeDiff;
-
- cancelBecauseOfEdgeOrCornerMaybe(lastLeft, lastTop);
-
- prevTime = timestamp;
- millisLeft -= timeDiff;
- AnimationScheduler.get().requestAnimationFrame(this);
- }
-
- private void cancelBecauseOfEdgeOrCornerMaybe(final int lastLeft,
- final int lastTop) {
- if (lastLeft == horizontalScrollbar.getScrollPos()
- && lastTop == verticalScrollbar.getScrollPos()) {
- cancel();
- }
- }
-
- public void cancel() {
- cancelled = true;
- }
- }
-
- /**
- * ScrollDestination case-specific handling logic.
- */
- private static double getScrollPos(final ScrollDestination destination,
- final double targetStartPx, final double targetEndPx,
- final double viewportStartPx, final double viewportEndPx,
- final int padding) {
-
- final double viewportLength = viewportEndPx - viewportStartPx;
-
- switch (destination) {
-
- /*
- * Scroll as little as possible to show the target element. If the
- * element fits into view, this works as START or END depending on the
- * current scroll position. If the element does not fit into view, this
- * works as START.
- */
- case ANY: {
- final double startScrollPos = targetStartPx - padding;
- final double endScrollPos = targetEndPx + padding - viewportLength;
-
- if (startScrollPos < viewportStartPx) {
- return startScrollPos;
- } else if (targetEndPx + padding > viewportEndPx) {
- return endScrollPos;
- } else {
- // NOOP, it's already visible
- return viewportStartPx;
- }
- }
-
- /*
- * Scrolls so that the element is shown at the end of the viewport. The
- * viewport will, however, not scroll before its first element.
- */
- case END: {
- return targetEndPx + padding - viewportLength;
- }
-
- /*
- * Scrolls so that the element is shown in the middle of the viewport.
- * The viewport will, however, not scroll beyond its contents, given
- * more elements than what the viewport is able to show at once. Under
- * no circumstances will the viewport scroll before its first element.
- */
- case MIDDLE: {
- final double targetMiddle = targetStartPx
- + (targetEndPx - targetStartPx) / 2;
- return targetMiddle - viewportLength / 2;
- }
-
- /*
- * Scrolls so that the element is shown at the start of the viewport.
- * The viewport will, however, not scroll beyond its contents.
- */
- case START: {
- return targetStartPx - padding;
- }
-
- /*
- * Throw an error if we're here. This can only mean that
- * ScrollDestination has been carelessly amended..
- */
- default: {
- throw new IllegalArgumentException(
- "Internal: ScrollDestination has been modified, "
- + "but Escalator.getScrollPos has not been updated "
- + "to match new values.");
- }
- }
-
- }
-
- /** An inner class that handles all logic related to scrolling. */
- private class Scroller extends JsniWorkaround {
- private double lastScrollTop = 0;
- private double lastScrollLeft = 0;
- /**
- * The current flick scroll animator. This is <code>null</code> if the
- * view isn't animating a flick scroll at the moment.
- */
- private FlickScrollAnimator currentFlickScroller;
-
- public Scroller() {
- super(Escalator.this);
- }
-
- @Override
- protected native JavaScriptObject createScrollListenerFunction(
- Escalator esc)
- /*-{
- var vScroll = esc.@com.vaadin.client.ui.grid.Escalator::verticalScrollbar;
- var vScrollElem = vScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::getElement()();
-
- var hScroll = esc.@com.vaadin.client.ui.grid.Escalator::horizontalScrollbar;
- var hScrollElem = hScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::getElement()();
-
- return $entry(function(e) {
- var target = e.target || e.srcElement; // IE8 uses e.scrElement
-
- // in case the scroll event was native (i.e. scrollbars were dragged, or
- // the scrollTop/Left was manually modified), the bundles have old cache
- // values. We need to make sure that the caches are kept up to date.
- if (target === vScrollElem) {
- vScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::updateScrollPosFromDom()();
- } else if (target === hScrollElem) {
- hScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::updateScrollPosFromDom()();
- } else {
- $wnd.console.error("unexpected scroll target: "+target);
- }
-
- esc.@com.vaadin.client.ui.grid.Escalator::onScroll()();
- });
- }-*/;
-
- @Override
- protected native JavaScriptObject createMousewheelListenerFunction(
- Escalator esc)
- /*-{
- return $entry(function(e) {
- var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
- var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
-
- // IE8 has only delta y
- if (isNaN(deltaY)) {
- deltaY = -0.5*e.wheelDelta;
- }
-
- @com.vaadin.client.ui.grid.Escalator.JsniUtil::moveScrollFromEvent(*)(esc, deltaX, deltaY, e);
- });
- }-*/;
-
- /**
- * Recalculates the virtual viewport represented by the scrollbars, so
- * that the sizes of the scroll handles appear correct in the browser
- */
- public void recalculateScrollbarsForVirtualViewport() {
- int scrollContentHeight = body.calculateEstimatedTotalRowHeight();
- int scrollContentWidth = columnConfiguration.calculateRowWidth();
-
- double tableWrapperHeight = heightOfEscalator;
- double tableWrapperWidth = widthOfEscalator;
-
- boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
- - header.heightOfSection - footer.heightOfSection;
- boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth;
-
- // One dimension got scrollbars, but not the other. Recheck time!
- if (verticalScrollNeeded != horizontalScrollNeeded) {
- if (!verticalScrollNeeded && horizontalScrollNeeded) {
- verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
- - header.heightOfSection
- - footer.heightOfSection
- - horizontalScrollbar.getScrollbarThickness();
- } else {
- horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth
- - verticalScrollbar.getScrollbarThickness();
- }
- }
-
- // let's fix the table wrapper size, since it's now stable.
- if (verticalScrollNeeded) {
- tableWrapperWidth -= verticalScrollbar.getScrollbarThickness();
- }
- if (horizontalScrollNeeded) {
- tableWrapperHeight -= horizontalScrollbar
- .getScrollbarThickness();
- }
- tableWrapper.getStyle().setHeight(tableWrapperHeight, Unit.PX);
- tableWrapper.getStyle().setWidth(tableWrapperWidth, Unit.PX);
-
- verticalScrollbar.setOffsetSize((int) (tableWrapperHeight
- - footer.heightOfSection - header.heightOfSection));
- verticalScrollbar.setScrollSize(scrollContentHeight);
-
- /*
- * If decreasing the amount of frozen columns, and scrolled to the
- * right, the scroll position might reset. So we need to remember
- * the scroll position, and re-apply it once the scrollbar size has
- * been adjusted.
- */
- int prevScrollPos = horizontalScrollbar.getScrollPos();
-
- int unfrozenPixels = columnConfiguration
- .getCalculatedColumnsWidth(Range.between(
- columnConfiguration.getFrozenColumnCount(),
- columnConfiguration.getColumnCount()));
- int frozenPixels = scrollContentWidth - unfrozenPixels;
- double hScrollOffsetWidth = tableWrapperWidth - frozenPixels;
- horizontalScrollbar.setOffsetSize((int) hScrollOffsetWidth);
- horizontalScrollbar.setScrollSize(unfrozenPixels);
- horizontalScrollbar.getElement().getStyle()
- .setLeft(frozenPixels, Unit.PX);
- horizontalScrollbar.setScrollPos(prevScrollPos);
- }
-
- /**
- * Logical scrolling event handler for the entire widget.
- *
- * @param scrollLeft
- * the current number of pixels that the user has scrolled
- * from left
- * @param scrollTop
- * the current number of pixels that the user has scrolled
- * from the top
- */
- public void onScroll() {
- if (internalScrollEventCalls > 0) {
- internalScrollEventCalls--;
- return;
- }
-
- final int scrollLeft = horizontalScrollbar.getScrollPos();
- final int scrollTop = verticalScrollbar.getScrollPos();
-
- if (lastScrollLeft != scrollLeft) {
- for (int i = 0; i < columnConfiguration.frozenColumns; i++) {
- header.updateFreezePosition(i, scrollLeft);
- body.updateFreezePosition(i, scrollLeft);
- footer.updateFreezePosition(i, scrollLeft);
- }
-
- position.set(headElem, -scrollLeft, 0);
-
- /*
- * TODO [[optimize]]: cache this value in case the instanceof
- * check has undesirable overhead. This could also be a
- * candidate for some deferred binding magic so that e.g.
- * AbsolutePosition is not even considered in permutations that
- * we know support something better. That would let the compiler
- * completely remove the entire condition since it knows that
- * the if will never be true.
- */
- if (position instanceof AbsolutePosition) {
- /*
- * we don't want to put "top: 0" on the footer, since it'll
- * render wrong, as we already have
- * "bottom: $footer-height".
- */
- footElem.getStyle().setLeft(-scrollLeft, Unit.PX);
- } else {
- position.set(footElem, -scrollLeft, 0);
- }
-
- lastScrollLeft = scrollLeft;
- }
-
- body.setBodyScrollPosition(scrollLeft, scrollTop);
-
- lastScrollTop = scrollTop;
- body.updateEscalatorRowsOnScroll();
- /*
- * TODO [[optimize]]: Might avoid a reflow by first calculating new
- * scrolltop and scrolleft, then doing the escalator magic based on
- * those numbers and only updating the positions after that.
- */
- }
-
- public native void attachScrollListener(Element element)
- /*
- * Attaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- if (element.addEventListener) {
- element.addEventListener("scroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
- } else {
- element.attachEvent("onscroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
- }
- }-*/;
-
- public native void detachScrollListener(Element element)
- /*
- * Attaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- if (element.addEventListener) {
- element.removeEventListener("scroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
- } else {
- element.detachEvent("onscroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
- }
- }-*/;
-
- public native void attachMousewheelListener(Element element)
- /*
- * Attaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- if (element.addEventListener) {
- // firefox likes "wheel", while others use "mousewheel"
- var eventName = element.onwheel===undefined?"mousewheel":"wheel";
- element.addEventListener(eventName, this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
- } else {
- // IE8
- element.attachEvent("onmousewheel", this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
- }
- }-*/;
-
- public native void detachMousewheelListener(Element element)
- /*
- * Detaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- if (element.addEventListener) {
- // firefox likes "wheel", while others use "mousewheel"
- var eventName = element.onwheel===undefined?"mousewheel":"wheel";
- element.removeEventListener(eventName, this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
- } else {
- // IE8
- element.detachEvent("onmousewheel", this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
- }
- }-*/;
-
- public native void attachTouchListeners(Element element)
- /*
- * Detaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- if (element.addEventListener) {
- element.addEventListener("touchstart", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchStartFunction);
- element.addEventListener("touchmove", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchMoveFunction);
- element.addEventListener("touchend", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction);
- element.addEventListener("touchcancel", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction);
- } else {
- // this would be IE8, but we don't support it with touch
- }
- }-*/;
-
- public native void detachTouchListeners(Element element)
- /*
- * Detaching events with JSNI instead of the GWT event mechanism because
- * GWT didn't provide enough details in events, or triggering the event
- * handlers with GWT bindings was unsuccessful. Maybe, with more time
- * and skill, it could be done with better success. JavaScript overlay
- * types might work. This might also get rid of the JsniWorkaround
- * class.
- */
- /*-{
- if (element.removeEventListener) {
- element.removeEventListener("touchstart", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchStartFunction);
- element.removeEventListener("touchmove", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchMoveFunction);
- element.removeEventListener("touchend", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction);
- element.removeEventListener("touchcancel", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction);
- } else {
- // this would be IE8, but we don't support it with touch
- }
- }-*/;
-
- private void cancelFlickScroll() {
- if (currentFlickScroller != null) {
- currentFlickScroller.cancel();
- }
- }
-
- /**
- * Handles a touch-based flick scroll.
- *
- * @param deltaX
- * the last scrolling delta in the x-axis in a touchmove
- * @param deltaY
- * the last scrolling delta in the y-axis in a touchmove
- * @param lastTime
- * the timestamp of the last touchmove
- */
- public void handleFlickScroll(double deltaX, double deltaY,
- double lastTime) {
- currentFlickScroller = new FlickScrollAnimator(deltaX, deltaY,
- lastTime);
- AnimationScheduler.get()
- .requestAnimationFrame(currentFlickScroller);
- }
-
- public void scrollToColumn(final int columnIndex,
- final ScrollDestination destination, final int padding) {
- assert columnIndex >= columnConfiguration.frozenColumns : "Can't scroll to a frozen column";
-
- /*
- * To cope with frozen columns, we just pretend those columns are
- * not there at all when calculating the position of the target
- * column and the boundaries of the viewport. The resulting
- * scrollLeft will be correct without compensation since the DOM
- * structure effectively means that scrollLeft also ignores the
- * frozen columns.
- */
- final int frozenPixels = columnConfiguration
- .getCalculatedColumnsWidth(Range.withLength(0,
- columnConfiguration.frozenColumns));
-
- final int targetStartPx = columnConfiguration
- .getCalculatedColumnsWidth(Range.withLength(0, columnIndex))
- - frozenPixels;
- final int targetEndPx = targetStartPx
- + columnConfiguration.getColumnWidthActual(columnIndex);
-
- final int viewportStartPx = getScrollLeft();
- int viewportEndPx = viewportStartPx + getElement().getOffsetWidth()
- - frozenPixels;
- if (verticalScrollbar.showsScrollHandle()) {
- viewportEndPx -= Util.getNativeScrollbarSize();
- }
-
- final double scrollLeft = getScrollPos(destination, targetStartPx,
- targetEndPx, viewportStartPx, viewportEndPx, padding);
-
- /*
- * note that it doesn't matter if the scroll would go beyond the
- * content, since the browser will adjust for that, and everything
- * fall into line accordingly.
- */
- setScrollLeft((int) scrollLeft);
- }
-
- public void scrollToRow(final int rowIndex,
- final ScrollDestination destination, final int padding) {
- /*
- * FIXME [[rowheight]]: coded to work only with default row heights
- * - will not work with variable row heights
- */
- final int targetStartPx = body.getDefaultRowHeight() * rowIndex;
- final int targetEndPx = targetStartPx + body.getDefaultRowHeight();
-
- final double viewportStartPx = getScrollTop();
- final double viewportEndPx = viewportStartPx
- + body.calculateHeight();
-
- final double scrollTop = getScrollPos(destination, targetStartPx,
- targetEndPx, viewportStartPx, viewportEndPx, padding);
-
- /*
- * note that it doesn't matter if the scroll would go beyond the
- * content, since the browser will adjust for that, and everything
- * falls into line accordingly.
- */
- setScrollTop(scrollTop);
- }
- }
-
- private abstract class AbstractRowContainer implements RowContainer {
-
- private static final int INITIAL_DEFAULT_ROW_HEIGHT = 20;
-
- private EscalatorUpdater updater = EscalatorUpdater.NULL;
-
- private int rows;
-
- /**
- * The table section element ({@code <thead>}, {@code <tbody>} or
- * {@code <tfoot>}) the rows (i.e. {@code <tr>} tags) are contained in.
- */
- protected final Element root;
-
- /** The height of the combined rows in the DOM. */
- protected double heightOfSection = -1;
-
- /**
- * The primary style name of the escalator. Most commonly provided by
- * Escalator as "v-escalator".
- */
- private String primaryStyleName = null;
-
- /**
- * A map containing cached values of an element's current top position.
- * <p>
- * Don't use this field directly, because it will not take proper care
- * of all the bookkeeping required.
- *
- * @deprecated Use {@link #setRowPosition(Element, int, int)},
- * {@link #getRowTop(Element)} and
- * {@link #removeRowPosition(Element)} instead.
- */
- @Deprecated
- private final Map<Element, Integer> rowTopPositionMap = new HashMap<Element, Integer>();
-
- private int defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT;
-
- public AbstractRowContainer(final Element rowContainerElement) {
- root = rowContainerElement;
- }
-
- /**
- * Gets the tag name of an element to represent a cell in a row.
- * <p>
- * Usually {@code "th"} or {@code "td"}.
- * <p>
- * <em>Note:</em> To actually <em>create</em> such an element, use
- * {@link #createCellElement()} instead.
- *
- * @return the tag name for the element to represent cells as
- * @see #createCellElement()
- */
- protected abstract String getCellElementTagName();
-
- @Override
- public EscalatorUpdater getEscalatorUpdater() {
- return updater;
- }
-
- /**
- * {@inheritDoc}
- * <p>
- * <em>Implementation detail:</em> This method does no DOM modifications
- * (i.e. is very cheap to call) if there is no data for rows or columns
- * when this method is called.
- *
- * @see #hasColumnAndRowData()
- */
- @Override
- public void setEscalatorUpdater(final EscalatorUpdater escalatorUpdater) {
- if (escalatorUpdater == null) {
- throw new IllegalArgumentException(
- "escalator updater cannot be null");
- }
-
- updater = escalatorUpdater;
-
- if (hasColumnAndRowData() && getRowCount() > 0) {
- refreshRows(0, getRowCount());
- }
- }
-
- /**
- * {@inheritDoc}
- * <p>
- * <em>Implementation detail:</em> This method does no DOM modifications
- * (i.e. is very cheap to call) if there are no rows in the DOM when
- * this method is called.
- *
- * @see #hasSomethingInDom()
- */
- @Override
- public void removeRows(final int index, final int numberOfRows) {
- assertArgumentsAreValidAndWithinRange(index, numberOfRows);
-
- rows -= numberOfRows;
-
- if (!isAttached()) {
- return;
- }
-
- if (hasSomethingInDom()) {
- paintRemoveRows(index, numberOfRows);
- }
- }
-
- protected abstract void paintRemoveRows(final int index,
- final int numberOfRows);
-
- private void assertArgumentsAreValidAndWithinRange(final int index,
- final int numberOfRows) throws IllegalArgumentException,
- IndexOutOfBoundsException {
- if (numberOfRows < 1) {
- throw new IllegalArgumentException(
- "Number of rows must be 1 or greater (was "
- + numberOfRows + ")");
- }
-
- if (index < 0 || index + numberOfRows > getRowCount()) {
- throw new IndexOutOfBoundsException("The given "
- + "row range (" + index + ".." + (index + numberOfRows)
- + ") was outside of the current number of rows ("
- + getRowCount() + ")");
- }
- }
-
- @Override
- public int getRowCount() {
- return rows;
- }
-
- /**
- * {@inheritDoc}
- * <p>
- * <em>Implementation detail:</em> This method does no DOM modifications
- * (i.e. is very cheap to call) if there is no data for columns when
- * this method is called.
- *
- * @see #hasColumnAndRowData()
- */
- @Override
- public void insertRows(final int index, final int numberOfRows) {
- if (index < 0 || index > getRowCount()) {
- throw new IndexOutOfBoundsException("The given index (" + index
- + ") was outside of the current number of rows (0.."
- + getRowCount() + ")");
- }
-
- if (numberOfRows < 1) {
- throw new IllegalArgumentException(
- "Number of rows must be 1 or greater (was "
- + numberOfRows + ")");
- }
-
- rows += numberOfRows;
-
- /*
- * only add items in the DOM if the widget itself is attached to the
- * DOM. We can't calculate sizes otherwise.
- */
- if (isAttached()) {
- paintInsertRows(index, numberOfRows);
- }
- }
-
- /**
- * Actually add rows into the DOM, now that everything can be
- * calculated.
- *
- * @param visualIndex
- * the DOM index to add rows into
- * @param numberOfRows
- * the number of rows to insert
- * @return a list of the added row elements
- */
- protected List<Element> paintInsertRows(final int visualIndex,
- final int numberOfRows) {
- assert isAttached() : "Can't paint rows if Escalator is not attached";
-
- final List<Element> addedRows = new ArrayList<Element>();
-
- if (numberOfRows < 1) {
- return addedRows;
- }
-
- Node referenceRow;
- if (root.getChildCount() != 0 && visualIndex != 0) {
- // get the row node we're inserting stuff after
- referenceRow = root.getChild(visualIndex - 1);
- } else {
- // index is 0, so just prepend.
- referenceRow = null;
- }
-
- for (int row = visualIndex; row < visualIndex + numberOfRows; row++) {
- final int rowHeight = getDefaultRowHeight();
- final Element tr = DOM.createTR();
- addedRows.add(tr);
- tr.addClassName(getStylePrimaryName() + "-row");
- referenceRow = insertAfterReferenceAndUpdateIt(root, tr,
- referenceRow);
-
- for (int col = 0; col < columnConfiguration.getColumnCount(); col++) {
- final int colWidth = columnConfiguration
- .getColumnWidthActual(col);
- final Element cellElem = createCellElement(rowHeight,
- colWidth);
- tr.appendChild(cellElem);
-
- // Set stylename and position if new cell is frozen
- if (col < columnConfiguration.frozenColumns) {
- cellElem.addClassName("frozen");
- position.set(cellElem, scroller.lastScrollLeft, 0);
- }
- }
-
- refreshRow(tr, row);
- }
- reapplyRowWidths();
-
- recalculateSectionHeight();
-
- return addedRows;
- }
-
- private Node insertAfterReferenceAndUpdateIt(final Element parent,
- final Element elem, final Node referenceNode) {
- if (referenceNode != null) {
- parent.insertAfter(elem, referenceNode);
- } else {
- /*
- * referencenode being null means we have offset 0, i.e. make it
- * the first row
- */
- /*
- * TODO [[optimize]]: Is insertFirst or append faster for an
- * empty root?
- */
- parent.insertFirst(elem);
- }
- return elem;
- }
-
- abstract protected void recalculateSectionHeight();
-
- /**
- * Returns the estimated height of all rows in the row container.
- * <p>
- * The estimate is promised to be correct as long as there are no rows
- * with calculated heights.
- */
- protected int calculateEstimatedTotalRowHeight() {
- return getDefaultRowHeight() * getRowCount();
- }
-
- /**
- * {@inheritDoc}
- * <p>
- * <em>Implementation detail:</em> This method does no DOM modifications
- * (i.e. is very cheap to call) if there is no data for columns when
- * this method is called.
- *
- * @see #hasColumnAndRowData()
- */
- @Override
- public void refreshRows(final int index, final int numberOfRows) {
- Profiler.enter("Escalator.AbstractRowContainer.refreshRows");
-
- assertArgumentsAreValidAndWithinRange(index, numberOfRows);
-
- if (!isAttached()) {
- return;
- }
-
- /*
- * TODO [[rowheight]]: even if no rows are evaluated in the current
- * viewport, the heights of some unrendered rows might change in a
- * refresh. This would cause the scrollbar to be adjusted (in
- * scrollHeight and/or scrollTop). Do we want to take this into
- * account?
- */
- if (hasColumnAndRowData()) {
- /*
- * TODO [[rowheight]]: nudge rows down with
- * refreshRowPositions() as needed
- */
- for (int row = index; row < index + numberOfRows; row++) {
- final Node tr = getTrByVisualIndex(row);
- refreshRow(tr, row);
- }
- }
-
- Profiler.leave("Escalator.AbstractRowContainer.refreshRows");
- }
-
- void refreshRow(final Node tr, final int logicalRowIndex) {
- flyweightRow.setup((Element) tr, logicalRowIndex,
- columnConfiguration.getCalculatedColumnWidths());
- updater.updateCells(flyweightRow, flyweightRow.getCells());
-
- /*
- * the "assert" guarantees that this code is run only during
- * development/debugging.
- */
- assert flyweightRow.teardown();
- }
-
- /**
- * Create and setup an empty cell element.
- *
- * @param width
- * the width of the cell, in pixels
- * @param height
- * the height of the cell, in pixels
- *
- * @return a set-up empty cell element
- */
- @SuppressWarnings("hiding")
- public Element createCellElement(final int height, final int width) {
- final Element cellElem = DOM.createElement(getCellElementTagName());
- cellElem.getStyle().setHeight(height, Unit.PX);
- cellElem.getStyle().setWidth(width, Unit.PX);
- cellElem.addClassName(getStylePrimaryName() + "-cell");
- return cellElem;
- }
-
- /**
- * Gets the child element that is visually at a certain index
- *
- * @param index
- * the index of the element to retrieve
- * @return the element at position {@code index}
- * @throws IndexOutOfBoundsException
- * if {@code index} is not valid within {@link #root}
- */
- abstract protected Element getTrByVisualIndex(int index)
- throws IndexOutOfBoundsException;
-
- protected void paintRemoveColumns(final int offset,
- final int numberOfColumns,
- final List<ColumnConfigurationImpl.Column> removedColumns) {
- final NodeList<Node> childNodes = root.getChildNodes();
- for (int visualRowIndex = 0; visualRowIndex < childNodes
- .getLength(); visualRowIndex++) {
- final Node tr = childNodes.getItem(visualRowIndex);
-
- for (int column = 0; column < numberOfColumns; column++) {
- Element cellElement = tr.getChild(offset).cast();
- detachPossibleWidgetFromCell(cellElement);
- cellElement.removeFromParent();
- }
- }
- reapplyRowWidths();
-
- final int firstRemovedColumnLeft = columnConfiguration
- .getCalculatedColumnsWidth(Range.withLength(0, offset));
- final boolean columnsWereRemovedFromLeftOfTheViewport = scroller.lastScrollLeft > firstRemovedColumnLeft;
-
- if (columnsWereRemovedFromLeftOfTheViewport) {
- int removedColumnsPxAmount = 0;
- for (ColumnConfigurationImpl.Column removedColumn : removedColumns) {
- removedColumnsPxAmount += removedColumn
- .getCalculatedWidth();
- }
- final int leftByDiff = (int) (scroller.lastScrollLeft - removedColumnsPxAmount);
- final int newScrollLeft = Math.max(firstRemovedColumnLeft,
- leftByDiff);
- horizontalScrollbar.setScrollPos(newScrollLeft);
- }
-
- // this needs to be after the scroll position adjustment above.
- scroller.recalculateScrollbarsForVirtualViewport();
-
- /*
- * Because we might remove columns where affected by colspans, it's
- * easiest to simply redraw everything when columns are modified.
- *
- * Yes, this is a TODO [[optimize]].
- */
- if (getRowCount() > 0
- && getColumnConfiguration().getColumnCount() > 0) {
- refreshRows(0, getRowCount());
- }
- }
-
- void detachPossibleWidgetFromCell(Node cellNode) {
- // Detach possible widget
- Widget widget = getWidgetFromCell(cellNode);
- if (widget != null) {
- // Orphan.
- setParent(widget, null);
-
- // Physical detach.
- cellNode.removeChild(widget.getElement());
- }
- }
-
- protected void paintInsertColumns(final int offset,
- final int numberOfColumns, boolean frozen) {
- final NodeList<Node> childNodes = root.getChildNodes();
-
- for (int row = 0; row < childNodes.getLength(); row++) {
- final int rowHeight = getDefaultRowHeight();
- final Element tr = getTrByVisualIndex(row);
-
- Node referenceCell;
- if (offset != 0) {
- referenceCell = tr.getChild(offset - 1);
- } else {
- referenceCell = null;
- }
-
- for (int col = offset; col < offset + numberOfColumns; col++) {
- final int colWidth = columnConfiguration
- .getColumnWidthActual(col);
- final Element cellElem = createCellElement(rowHeight,
- colWidth);
- referenceCell = insertAfterReferenceAndUpdateIt(tr,
- cellElem, referenceCell);
- }
- }
- reapplyRowWidths();
-
- if (frozen) {
- for (int col = offset; col < offset + numberOfColumns; col++) {
- setColumnFrozen(col, true);
- }
- }
-
- // this needs to be before the scrollbar adjustment.
- scroller.recalculateScrollbarsForVirtualViewport();
-
- int pixelsToInsertedColumn = columnConfiguration
- .getCalculatedColumnsWidth(Range.withLength(0, offset));
- final boolean columnsWereAddedToTheLeftOfViewport = scroller.lastScrollLeft > pixelsToInsertedColumn;
-
- if (columnsWereAddedToTheLeftOfViewport) {
- int insertedColumnsWidth = columnConfiguration
- .getCalculatedColumnsWidth(Range.withLength(offset,
- numberOfColumns));
- horizontalScrollbar
- .setScrollPos((int) (scroller.lastScrollLeft + insertedColumnsWidth));
- }
-
- /*
- * Because we might insert columns where affected by colspans, it's
- * easiest to simply redraw everything when columns are modified.
- *
- * Yes, this is a TODO [[optimize]].
- */
- if (getRowCount() > 0
- && getColumnConfiguration().getColumnCount() > 1) {
- refreshRows(0, getRowCount());
- }
- }
-
- public void setColumnFrozen(int column, boolean frozen) {
- final NodeList<Node> childNodes = root.getChildNodes();
-
- for (int row = 0; row < childNodes.getLength(); row++) {
- final Element tr = childNodes.getItem(row).cast();
-
- Element cell = (Element) tr.getChild(column);
- if (frozen) {
- cell.addClassName("frozen");
- } else {
- cell.removeClassName("frozen");
- position.reset(cell);
- }
- }
-
- if (frozen) {
- updateFreezePosition(column, scroller.lastScrollLeft);
- }
- }
-
- public void updateFreezePosition(int column, double scrollLeft) {
- final NodeList<Node> childNodes = root.getChildNodes();
-
- for (int row = 0; row < childNodes.getLength(); row++) {
- final Element tr = childNodes.getItem(row).cast();
-
- Element cell = (Element) tr.getChild(column);
- position.set(cell, scrollLeft, 0);
- }
- }
-
- /**
- * Iterates through all the cells in a column and returns the width of
- * the widest element in this RowContainer.
- *
- * @param index
- * the index of the column to inspect
- * @return the pixel width of the widest element in the indicated column
- */
- public int calculateMaxColWidth(int index) {
- Element row = root.getFirstChildElement();
- int maxWidth = 0;
- while (row != null) {
- final Element cell = (Element) row.getChild(index);
- final boolean isVisible = !cell.getStyle().getDisplay()
- .equals(Display.NONE.getCssName());
- if (isVisible) {
- maxWidth = Math.max(maxWidth, cell.getScrollWidth());
- }
- row = row.getNextSiblingElement();
- }
- return maxWidth;
- }
-
- /**
- * Reapplies all the cells' widths according to the calculated widths in
- * the column configuration.
- */
- public void reapplyColumnWidths() {
- Element row = root.getFirstChildElement();
- while (row != null) {
- Element cell = row.getFirstChildElement();
- int columnIndex = 0;
- while (cell != null) {
- @SuppressWarnings("hiding")
- final int width = getCalculatedColumnWidthWithColspan(cell,
- columnIndex);
-
- /*
- * TODO Should Escalator implement ProvidesResize at some
- * point, this is where we need to do that.
- */
- cell.getStyle().setWidth(width, Unit.PX);
-
- cell = cell.getNextSiblingElement();
- columnIndex++;
- }
- row = row.getNextSiblingElement();
- }
-
- reapplyRowWidths();
- }
-
- private int getCalculatedColumnWidthWithColspan(final Element cell,
- final int columnIndex) {
- final int colspan = cell.getPropertyInt(FlyweightCell.COLSPAN_ATTR);
- Range spannedColumns = Range.withLength(columnIndex, colspan);
-
- /*
- * Since browsers don't explode with overflowing colspans, escalator
- * shouldn't either.
- */
- if (spannedColumns.getEnd() > columnConfiguration.getColumnCount()) {
- spannedColumns = Range.between(columnIndex,
- columnConfiguration.getColumnCount());
- }
- return columnConfiguration
- .getCalculatedColumnsWidth(spannedColumns);
- }
-
- /**
- * Applies the total length of the columns to each row element.
- * <p>
- * <em>Note:</em> In contrast to {@link #reapplyColumnWidths()}, this
- * method only modifies the width of the {@code <tr>} element, not the
- * cells within.
- */
- protected void reapplyRowWidths() {
- int rowWidth = columnConfiguration.calculateRowWidth();
-
- com.google.gwt.dom.client.Element row = root.getFirstChildElement();
- while (row != null) {
- row.getStyle().setWidth(rowWidth, Unit.PX);
- row = row.getNextSiblingElement();
- }
- }
-
- /**
- * The primary style name for the container.
- *
- * @param primaryStyleName
- * the style name to use as prefix for all row and cell style
- * names.
- */
- protected void setStylePrimaryName(String primaryStyleName) {
- String oldStyle = getStylePrimaryName();
- if (SharedUtil.equals(oldStyle, primaryStyleName)) {
- return;
- }
-
- this.primaryStyleName = primaryStyleName;
-
- // Update already rendered rows and cells
- Node row = root.getFirstChild();
- while (row != null) {
- Element rowElement = row.cast();
- UIObject.setStylePrimaryName(rowElement, primaryStyleName
- + "-row");
- Node cell = row.getFirstChild();
- while (cell != null) {
- Element cellElement = cell.cast();
- UIObject.setStylePrimaryName(cellElement, primaryStyleName
- + "-cell");
- cell = cell.getNextSibling();
- }
- row = row.getNextSibling();
- }
- }
-
- /**
- * Returns the primary style name of the container.
- *
- * @return The primary style name or <code>null</code> if not set.
- */
- protected String getStylePrimaryName() {
- return primaryStyleName;
- }
-
- @Override
- public void setDefaultRowHeight(int px) throws IllegalArgumentException {
- if (px < 1) {
- throw new IllegalArgumentException("Height must be positive. "
- + px + " was given.");
- }
-
- defaultRowHeight = px;
- reapplyDefaultRowHeights();
- }
-
- @Override
- public int getDefaultRowHeight() {
- return defaultRowHeight;
- }
-
- /**
- * The default height of rows has (most probably) changed.
- * <p>
- * Make sure that the displayed rows with a default height are updated
- * in height and top position.
- * <p>
- * <em>Note:</em>This implementation should not call
- * {@link Escalator#recalculateElementSizes()} - it is done by the
- * discretion of the caller of this method.
- */
- protected abstract void reapplyDefaultRowHeights();
-
- protected void reapplyRowHeight(final Element tr, final int heightPx) {
- Element cellElem = tr.getFirstChildElement().cast();
- while (cellElem != null) {
- cellElem.getStyle().setHeight(heightPx, Unit.PX);
- cellElem = cellElem.getNextSiblingElement();
- }
-
- /*
- * no need to apply height to tr-element, it'll be resized
- * implicitly.
- */
- }
-
- @SuppressWarnings("boxing")
- protected void setRowPosition(final Element tr, final int x, final int y) {
- position.set(tr, x, y);
- rowTopPositionMap.put(tr, y);
- }
-
- @SuppressWarnings("boxing")
- protected int getRowTop(final Element tr) {
- return rowTopPositionMap.get(tr);
- }
-
- protected void removeRowPosition(Element tr) {
- rowTopPositionMap.remove(tr);
- }
- }
-
- private abstract class AbstractStaticRowContainer extends
- AbstractRowContainer {
- public AbstractStaticRowContainer(final Element headElement) {
- super(headElement);
- }
-
- @Override
- protected void paintRemoveRows(final int index, final int numberOfRows) {
- for (int i = index; i < index + numberOfRows; i++) {
- final Element tr = (Element) root.getChild(index);
- for (int c = 0; c < tr.getChildCount(); c++) {
- detachPossibleWidgetFromCell((Element) tr.getChild(c)
- .cast());
- }
- tr.removeFromParent();
- }
- recalculateSectionHeight();
- }
-
- @Override
- protected Element getTrByVisualIndex(final int index)
- throws IndexOutOfBoundsException {
- if (index >= 0 && index < root.getChildCount()) {
- return (Element) root.getChild(index);
- } else {
- throw new IndexOutOfBoundsException("No such visual index: "
- + index);
- }
- }
-
- @Override
- public void insertRows(int index, int numberOfRows) {
- super.insertRows(index, numberOfRows);
- recalculateElementSizes();
- }
-
- @Override
- public void removeRows(int index, int numberOfRows) {
- super.removeRows(index, numberOfRows);
- recalculateElementSizes();
- }
-
- @Override
- protected void reapplyDefaultRowHeights() {
- if (root.getChildCount() == 0) {
- return;
- }
-
- Profiler.enter("Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
-
- Element tr = root.getFirstChildElement().cast();
- while (tr != null) {
- reapplyRowHeight(tr, getDefaultRowHeight());
- tr = tr.getNextSiblingElement();
- }
-
- /*
- * Because all rows are immediately displayed in the static row
- * containers, the section's overall height has most probably
- * changed.
- */
- recalculateSectionHeight();
-
- Profiler.leave("Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
- }
-
- @Override
- protected void recalculateSectionHeight() {
- Profiler.enter("Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
-
- int newHeight = calculateEstimatedTotalRowHeight();
- if (newHeight != heightOfSection) {
- heightOfSection = newHeight;
- sectionHeightCalculated();
- body.verifyEscalatorCount();
- }
-
- Profiler.leave("Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
- }
-
- /**
- * Informs the row container that the height of its respective table
- * section has changed.
- * <p>
- * These calculations might affect some layouting logic, such as the
- * body is being offset by the footer, the footer needs to be readjusted
- * according to its height, and so on.
- * <p>
- * A table section is either header, body or footer.
- */
- protected abstract void sectionHeightCalculated();
- }
-
- private class HeaderRowContainer extends AbstractStaticRowContainer {
- public HeaderRowContainer(final Element headElement) {
- super(headElement);
- }
-
- @Override
- protected void sectionHeightCalculated() {
- bodyElem.getStyle().setMarginTop(heightOfSection, Unit.PX);
- verticalScrollbar.getElement().getStyle()
- .setTop(heightOfSection, Unit.PX);
- }
-
- @Override
- protected String getCellElementTagName() {
- return "th";
- }
-
- @Override
- public void setStylePrimaryName(String primaryStyleName) {
- super.setStylePrimaryName(primaryStyleName);
- UIObject.setStylePrimaryName(root, primaryStyleName + "-header");
- }
- }
-
- private class FooterRowContainer extends AbstractStaticRowContainer {
- public FooterRowContainer(final Element footElement) {
- super(footElement);
- }
-
- @Override
- public void setStylePrimaryName(String primaryStyleName) {
- super.setStylePrimaryName(primaryStyleName);
- UIObject.setStylePrimaryName(root, primaryStyleName + "-footer");
- }
-
- @Override
- protected String getCellElementTagName() {
- return "td";
- }
-
- @Override
- protected void sectionHeightCalculated() {
- int vscrollHeight = (int) Math.floor(heightOfEscalator
- - header.heightOfSection - footer.heightOfSection);
-
- final boolean horizontalScrollbarNeeded = columnConfiguration
- .calculateRowWidth() > widthOfEscalator;
- if (horizontalScrollbarNeeded) {
- vscrollHeight -= horizontalScrollbar.getScrollbarThickness();
- }
-
- verticalScrollbar.setOffsetSize(vscrollHeight);
- }
- }
-
- private class BodyRowContainer extends AbstractRowContainer {
- /*
- * TODO [[optimize]]: check whether a native JsArray might be faster
- * than LinkedList
- */
- /**
- * The order in which row elements are rendered visually in the browser,
- * with the help of CSS tricks. Usually has nothing to do with the DOM
- * order.
- */
- private final LinkedList<Element> visualRowOrder = new LinkedList<Element>();
-
- /**
- * The logical index of the topmost row.
- *
- * @deprecated Use the accessors {@link #setTopRowLogicalIndex(int)},
- * {@link #updateTopRowLogicalIndex(int)} and
- * {@link #getTopRowLogicalIndex()} instead
- */
- @Deprecated
- private int topRowLogicalIndex = 0;
-
- private void setTopRowLogicalIndex(int topRowLogicalIndex) {
- if (LogConfiguration.loggingIsEnabled(Level.INFO)) {
- Logger.getLogger("Escalator.BodyRowContainer").fine(
- "topRowLogicalIndex: " + this.topRowLogicalIndex
- + " -> " + topRowLogicalIndex);
- }
- assert topRowLogicalIndex >= 0 : "topRowLogicalIndex became negative";
- /*
- * if there's a smart way of evaluating and asserting the max index,
- * this would be a nice place to put it. I haven't found out an
- * effective and generic solution.
- */
-
- this.topRowLogicalIndex = topRowLogicalIndex;
- }
-
- private int getTopRowLogicalIndex() {
- return topRowLogicalIndex;
- }
-
- private void updateTopRowLogicalIndex(int diff) {
- setTopRowLogicalIndex(topRowLogicalIndex + diff);
- }
-
- public BodyRowContainer(final Element bodyElement) {
- super(bodyElement);
- }
-
- @Override
- public void setStylePrimaryName(String primaryStyleName) {
- super.setStylePrimaryName(primaryStyleName);
- UIObject.setStylePrimaryName(root, primaryStyleName + "-body");
- }
-
- public void updateEscalatorRowsOnScroll() {
- if (visualRowOrder.isEmpty()) {
- return;
- }
-
- boolean rowsWereMoved = false;
-
- final int topRowPos = getRowTop(visualRowOrder.getFirst());
- // TODO [[mpixscroll]]
- final int scrollTop = tBodyScrollTop;
- final int viewportOffset = topRowPos - scrollTop;
-
- /*
- * TODO [[optimize]] this if-else can most probably be refactored
- * into a neater block of code
- */
-
- if (viewportOffset > 0) {
- // there's empty room on top
-
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- int originalRowsToMove = (int) Math.ceil(viewportOffset
- / (double) getDefaultRowHeight());
- int rowsToMove = Math.min(originalRowsToMove,
- root.getChildCount());
-
- final int end = root.getChildCount();
- final int start = end - rowsToMove;
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- final int logicalRowIndex = scrollTop / getDefaultRowHeight();
- moveAndUpdateEscalatorRows(Range.between(start, end), 0,
- logicalRowIndex);
-
- updateTopRowLogicalIndex(-originalRowsToMove);
- }
-
- else if (viewportOffset + getDefaultRowHeight() <= 0) {
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
-
- /*
- * the viewport has been scrolled more than the topmost visual
- * row.
- */
-
- /*
- * Using the fact that integer division has implicit
- * floor-function to our advantage here.
- */
- int originalRowsToMove = Math.abs(viewportOffset
- / getDefaultRowHeight());
- int rowsToMove = Math.min(originalRowsToMove,
- root.getChildCount());
-
- int logicalRowIndex;
- if (rowsToMove < root.getChildCount()) {
- /*
- * We scroll so little that we can just keep adding the rows
- * below the current escalator
- */
- logicalRowIndex = getLogicalRowIndex(visualRowOrder
- .getLast()) + 1;
- } else {
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- /*
- * Since we're moving all escalator rows, we need to
- * calculate the first logical row index from the scroll
- * position.
- */
- logicalRowIndex = scrollTop / getDefaultRowHeight();
- }
-
- /*
- * Since we're moving the viewport downwards, the visual index
- * is always at the bottom. Note: Due to how
- * moveAndUpdateEscalatorRows works, this will work out even if
- * we move all the rows, and try to place them "at the end".
- */
- final int targetVisualIndex = root.getChildCount();
-
- // make sure that we don't move rows over the data boundary
- boolean aRowWasLeftBehind = false;
- if (logicalRowIndex + rowsToMove > getRowCount()) {
- /*
- * TODO [[rowheight]]: with constant row heights, there's
- * always exactly one row that will be moved beyond the data
- * source, when viewport is scrolled to the end. This,
- * however, isn't guaranteed anymore once row heights start
- * varying.
- */
- rowsToMove--;
- aRowWasLeftBehind = true;
- }
-
- moveAndUpdateEscalatorRows(Range.between(0, rowsToMove),
- targetVisualIndex, logicalRowIndex);
-
- if (aRowWasLeftBehind) {
- /*
- * To keep visualRowOrder as a spatially contiguous block of
- * rows, let's make sure that the one row we didn't move
- * visually still stays with the pack.
- */
- final Range strayRow = Range.withOnly(0);
-
- /*
- * We cannot trust getLogicalRowIndex, because it hasn't yet
- * been updated. But since we're leaving rows behind, it
- * means we've scrolled to the bottom. So, instead, we
- * simply count backwards from the end.
- */
- final int topLogicalIndex = getRowCount()
- - visualRowOrder.size();
- moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex);
- }
-
- final int naiveNewLogicalIndex = getTopRowLogicalIndex()
- + originalRowsToMove;
- final int maxLogicalIndex = getRowCount()
- - visualRowOrder.size();
- setTopRowLogicalIndex(Math.min(naiveNewLogicalIndex,
- maxLogicalIndex));
- }
-
- if (rowsWereMoved) {
- fireRowVisibilityChangeEvent();
- }
- }
-
- @Override
- protected List<Element> paintInsertRows(final int index,
- final int numberOfRows) {
- if (numberOfRows == 0) {
- return Collections.emptyList();
- }
-
- /*
- * TODO: this method should probably only add physical rows, and not
- * populate them - let everything be populated as appropriate by the
- * logic that follows.
- *
- * This also would lead to the fact that paintInsertRows wouldn't
- * need to return anything.
- */
- final List<Element> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
- index, numberOfRows);
-
- /*
- * insertRows will always change the number of rows - update the
- * scrollbar sizes.
- */
- scroller.recalculateScrollbarsForVirtualViewport();
-
- /*
- * FIXME [[rowheight]]: coded to work only with default row heights
- * - will not work with variable row heights
- */
- final boolean addedRowsAboveCurrentViewport = index
- * getDefaultRowHeight() < getScrollTop();
- final boolean addedRowsBelowCurrentViewport = index
- * getDefaultRowHeight() > getScrollTop()
- + calculateHeight();
-
- if (addedRowsAboveCurrentViewport) {
- /*
- * We need to tweak the virtual viewport (scroll handle
- * positions, table "scroll position" and row locations), but
- * without re-evaluating any rows.
- */
-
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- final int yDelta = numberOfRows * getDefaultRowHeight();
- adjustScrollPosIgnoreEvents(yDelta);
- updateTopRowLogicalIndex(numberOfRows);
- }
-
- else if (addedRowsBelowCurrentViewport) {
- // NOOP, we already recalculated scrollbars.
- }
-
- else { // some rows were added inside the current viewport
-
- final int unupdatedLogicalStart = index + addedRows.size();
- final int visualOffset = getLogicalRowIndex(visualRowOrder
- .getFirst());
-
- /*
- * At this point, we have added new escalator rows, if so
- * needed.
- *
- * If more rows were added than the new escalator rows can
- * account for, we need to start to spin the escalator to update
- * the remaining rows aswell.
- */
- final int rowsStillNeeded = numberOfRows - addedRows.size();
- final Range unupdatedVisual = convertToVisual(Range.withLength(
- unupdatedLogicalStart, rowsStillNeeded));
- final int end = root.getChildCount();
- final int start = end - unupdatedVisual.length();
- final int visualTargetIndex = unupdatedLogicalStart
- - visualOffset;
- moveAndUpdateEscalatorRows(Range.between(start, end),
- visualTargetIndex, unupdatedLogicalStart);
-
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- // move the surrounding rows to their correct places.
- int rowTop = (unupdatedLogicalStart + (end - start))
- * getDefaultRowHeight();
- final ListIterator<Element> i = visualRowOrder
- .listIterator(visualTargetIndex + (end - start));
- while (i.hasNext()) {
- final Element tr = i.next();
- setRowPosition(tr, 0, rowTop);
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- rowTop += getDefaultRowHeight();
- }
-
- fireRowVisibilityChangeEvent();
- }
- return addedRows;
- }
-
- /**
- * Move escalator rows around, and make sure everything gets
- * appropriately repositioned and repainted.
- *
- * @param visualSourceRange
- * the range of rows to move to a new place
- * @param visualTargetIndex
- * the visual index where the rows will be placed to
- * @param logicalTargetIndex
- * the logical index to be assigned to the first moved row
- * @throws IllegalArgumentException
- * if any of <code>visualSourceRange.getStart()</code>,
- * <code>visualTargetIndex</code> or
- * <code>logicalTargetIndex</code> is a negative number; or
- * if <code>visualTargetInfo</code> is greater than the
- * number of escalator rows.
- */
- private void moveAndUpdateEscalatorRows(final Range visualSourceRange,
- final int visualTargetIndex, final int logicalTargetIndex)
- throws IllegalArgumentException {
-
- if (visualSourceRange.isEmpty()) {
- return;
- }
-
- if (visualSourceRange.getStart() < 0) {
- throw new IllegalArgumentException(
- "Logical source start must be 0 or greater (was "
- + visualSourceRange.getStart() + ")");
- } else if (logicalTargetIndex < 0) {
- throw new IllegalArgumentException(
- "Logical target must be 0 or greater");
- } else if (visualTargetIndex < 0) {
- throw new IllegalArgumentException(
- "Visual target must be 0 or greater");
- } else if (visualTargetIndex > root.getChildCount()) {
- throw new IllegalArgumentException(
- "Visual target must not be greater than the number of escalator rows");
- } else if (logicalTargetIndex + visualSourceRange.length() > getRowCount()) {
- final int logicalEndIndex = logicalTargetIndex
- + visualSourceRange.length() - 1;
- throw new IllegalArgumentException(
- "Logical target leads to rows outside of the data range ("
- + logicalTargetIndex + ".." + logicalEndIndex
- + ")");
- }
-
- /*
- * Since we move a range into another range, the indices might move
- * about. Having 10 rows, if we move 0..1 to index 10 (to the end of
- * the collection), the target range will end up being 8..9, instead
- * of 10..11.
- *
- * This applies only if we move elements forward in the collection,
- * not backward.
- */
- final int adjustedVisualTargetIndex;
- if (visualSourceRange.getStart() < visualTargetIndex) {
- adjustedVisualTargetIndex = visualTargetIndex
- - visualSourceRange.length();
- } else {
- adjustedVisualTargetIndex = visualTargetIndex;
- }
-
- if (visualSourceRange.getStart() != adjustedVisualTargetIndex) {
-
- /*
- * Reorder the rows to their correct places within
- * visualRowOrder (unless rows are moved back to their original
- * places)
- */
-
- /*
- * TODO [[optimize]]: move whichever set is smaller: the ones
- * explicitly moved, or the others. So, with 10 escalator rows,
- * if we are asked to move idx[0..8] to the end of the list,
- * it's faster to just move idx[9] to the beginning.
- */
-
- final List<Element> removedRows = new ArrayList<Element>(
- visualSourceRange.length());
- for (int i = 0; i < visualSourceRange.length(); i++) {
- final Element tr = visualRowOrder.remove(visualSourceRange
- .getStart());
- removedRows.add(tr);
- }
- visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows);
- }
-
- { // Refresh the contents of the affected rows
- final ListIterator<Element> iter = visualRowOrder
- .listIterator(adjustedVisualTargetIndex);
- for (int logicalIndex = logicalTargetIndex; logicalIndex < logicalTargetIndex
- + visualSourceRange.length(); logicalIndex++) {
- final Element tr = iter.next();
- refreshRow(tr, logicalIndex);
- }
- }
-
- { // Reposition the rows that were moved
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- int newRowTop = logicalTargetIndex * getDefaultRowHeight();
-
- final ListIterator<Element> iter = visualRowOrder
- .listIterator(adjustedVisualTargetIndex);
- for (int i = 0; i < visualSourceRange.length(); i++) {
- final Element tr = iter.next();
- setRowPosition(tr, 0, newRowTop);
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- newRowTop += getDefaultRowHeight();
- }
- }
- }
-
- /**
- * Adjust the scroll position without having the scroll handler have any
- * side-effects.
- * <p>
- * <em>Note:</em> {@link Scroller#onScroll(double, double)}
- * <em>will</em> be triggered, but it will not do anything, with the
- * help of {@link Escalator#internalScrollEventCalls}.
- *
- * @param yDelta
- * the delta of pixels to scrolls. A positive value moves the
- * viewport downwards, while a negative value moves the
- * viewport upwards
- */
- public void adjustScrollPosIgnoreEvents(final int yDelta) {
- if (yDelta == 0) {
- return;
- }
-
- internalScrollEventCalls++;
- verticalScrollbar.setScrollPosByDelta(yDelta);
-
- /*
- * FIXME [[rowheight]]: coded to work only with default row heights
- * - will not work with variable row heights
- */
- final int rowTopPos = yDelta - yDelta % getDefaultRowHeight();
- for (final Element tr : visualRowOrder) {
- setRowPosition(tr, 0, getRowTop(tr) + rowTopPos);
- }
- setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + yDelta);
- }
-
- /**
- * Adds new physical escalator rows to the DOM at the given index if
- * there's still a need for more escalator rows.
- * <p>
- * If Escalator already is at (or beyond) max capacity, this method does
- * nothing to the DOM.
- *
- * @param index
- * the index at which to add new escalator rows.
- * <em>Note:</em>It is assumed that the index is both the
- * visual index and the logical index.
- * @param numberOfRows
- * the number of rows to add at <code>index</code>
- * @return a list of the added rows
- */
- private List<Element> fillAndPopulateEscalatorRowsIfNeeded(
- final int index, final int numberOfRows) {
-
- final int escalatorRowsStillFit = getMaxEscalatorRowCapacity()
- - root.getChildCount();
- final int escalatorRowsNeeded = Math.min(numberOfRows,
- escalatorRowsStillFit);
-
- if (escalatorRowsNeeded > 0) {
-
- final List<Element> addedRows = super.paintInsertRows(index,
- escalatorRowsNeeded);
- visualRowOrder.addAll(index, addedRows);
-
- /*
- * We need to figure out the top positions for the rows we just
- * added.
- */
- for (int i = 0; i < addedRows.size(); i++) {
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- setRowPosition(addedRows.get(i), 0, (index + i)
- * getDefaultRowHeight());
- }
-
- /* Move the other rows away from above the added escalator rows */
- for (int i = index + addedRows.size(); i < visualRowOrder
- .size(); i++) {
- final Element tr = visualRowOrder.get(i);
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- setRowPosition(tr, 0, i * getDefaultRowHeight());
- }
-
- return addedRows;
- } else {
- return new ArrayList<Element>();
- }
- }
-
- private int getMaxEscalatorRowCapacity() {
- /*
- * FIXME [[rowheight]]: coded to work only with default row heights
- * - will not work with variable row heights
- */
- final int maxEscalatorRowCapacity = (int) Math
- .ceil(calculateHeight() / getDefaultRowHeight()) + 1;
-
- /*
- * maxEscalatorRowCapacity can become negative if the headers and
- * footers start to overlap. This is a crazy situation, but Vaadin
- * blinks the components a lot, so it's feasible.
- */
- return Math.max(0, maxEscalatorRowCapacity);
- }
-
- @Override
- protected void paintRemoveRows(final int index, final int numberOfRows) {
-
- final Range viewportRange = Range.withLength(
- getLogicalRowIndex(visualRowOrder.getFirst()),
- visualRowOrder.size());
-
- final Range removedRowsRange = Range
- .withLength(index, numberOfRows);
-
- final Range[] partitions = removedRowsRange
- .partitionWith(viewportRange);
- final Range removedAbove = partitions[0];
- final Range removedLogicalInside = partitions[1];
- final Range removedVisualInside = convertToVisual(removedLogicalInside);
-
- /*
- * TODO: extract the following if-block to a separate method. I'll
- * leave this be inlined for now, to make linediff-based code
- * reviewing easier. Probably will be moved in the following patch
- * set.
- */
-
- /*
- * Adjust scroll position in one of two scenarios:
- *
- * 1) Rows were removed above. Then we just need to adjust the
- * scrollbar by the height of the removed rows.
- *
- * 2) There are no logical rows above, and at least the first (if
- * not more) visual row is removed. Then we need to snap the scroll
- * position to the first visible row (i.e. reset scroll position to
- * absolute 0)
- *
- * The logic is optimized in such a way that the
- * adjustScrollPosIgnoreEvents is called only once, to avoid extra
- * reflows, and thus the code might seem a bit obscure.
- */
- final boolean firstVisualRowIsRemoved = !removedVisualInside
- .isEmpty() && removedVisualInside.getStart() == 0;
-
- if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) {
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- final int yDelta = removedAbove.length()
- * getDefaultRowHeight();
- final int firstLogicalRowHeight = getDefaultRowHeight();
- final boolean removalScrollsToShowFirstLogicalRow = verticalScrollbar
- .getScrollPos() - yDelta < firstLogicalRowHeight;
-
- if (removedVisualInside.isEmpty()
- && (!removalScrollsToShowFirstLogicalRow || !firstVisualRowIsRemoved)) {
- /*
- * rows were removed from above the viewport, so all we need
- * to do is to adjust the scroll position to account for the
- * removed rows
- */
- adjustScrollPosIgnoreEvents(-yDelta);
- } else if (removalScrollsToShowFirstLogicalRow) {
- /*
- * It seems like we've removed all rows from above, and also
- * into the current viewport. This means we'll need to even
- * out the scroll position to exactly 0 (i.e. adjust by the
- * current negative scrolltop, presto!), so that it isn't
- * aligned funnily
- */
- adjustScrollPosIgnoreEvents(-verticalScrollbar
- .getScrollPos());
- }
- }
-
- // ranges evaluated, let's do things.
- if (!removedVisualInside.isEmpty()) {
- int escalatorRowCount = bodyElem.getChildCount();
-
- /*
- * If we're left with less rows than the number of escalators,
- * remove the unused ones.
- */
- final int escalatorRowsToRemove = escalatorRowCount
- - getRowCount();
- if (escalatorRowsToRemove > 0) {
- for (int i = 0; i < escalatorRowsToRemove; i++) {
- final Element tr = visualRowOrder
- .remove(removedVisualInside.getStart());
- for (int c = 0; c < tr.getChildCount(); c++) {
- detachPossibleWidgetFromCell((Element) tr.getChild(
- c).cast());
- }
- tr.removeFromParent();
- removeRowPosition(tr);
- }
- escalatorRowCount -= escalatorRowsToRemove;
-
- /*
- * Because we're removing escalator rows, we don't have
- * anything to scroll by. Let's make sure the viewport is
- * scrolled to top, to render any rows possibly left above.
- */
- body.setBodyScrollPosition(tBodyScrollLeft, 0);
-
- /*
- * We might have removed some rows from the middle, so let's
- * make sure we're not left with any holes. Also remember:
- * visualIndex == logicalIndex applies now.
- */
- final int dirtyRowsStart = removedLogicalInside.getStart();
- for (int i = dirtyRowsStart; i < escalatorRowCount; i++) {
- final Element tr = visualRowOrder.get(i);
- /*
- * FIXME [[rowheight]]: coded to work only with default
- * row heights - will not work with variable row heights
- */
- setRowPosition(tr, 0, i * getDefaultRowHeight());
- }
-
- /*
- * this is how many rows appeared into the viewport from
- * below
- */
- final int rowsToUpdateDataOn = numberOfRows
- - escalatorRowsToRemove;
- final int start = Math.max(0, escalatorRowCount
- - rowsToUpdateDataOn);
- final int end = escalatorRowCount;
- for (int i = start; i < end; i++) {
- final Element tr = visualRowOrder.get(i);
- refreshRow(tr, i);
- }
- }
-
- else {
- // No escalator rows need to be removed.
-
- /*
- * Two things (or a combination thereof) can happen:
- *
- * 1) We're scrolled to the bottom, the last rows are
- * removed. SOLUTION: moveAndUpdateEscalatorRows the
- * bottommost rows, and place them at the top to be
- * refreshed.
- *
- * 2) We're scrolled somewhere in the middle, arbitrary rows
- * are removed. SOLUTION: moveAndUpdateEscalatorRows the
- * removed rows, and place them at the bottom to be
- * refreshed.
- *
- * Since a combination can also happen, we need to handle
- * this in a smart way, all while avoiding
- * double-refreshing.
- */
-
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- final int contentBottom = getRowCount()
- * getDefaultRowHeight();
- final int viewportBottom = (int) (tBodyScrollTop + calculateHeight());
- if (viewportBottom <= contentBottom) {
- /*
- * We're in the middle of the row container, everything
- * is added to the bottom
- */
- paintRemoveRowsAtMiddle(removedLogicalInside,
- removedVisualInside, 0);
- }
-
- else if (contentBottom
- + (numberOfRows * getDefaultRowHeight())
- - viewportBottom < getDefaultRowHeight()) {
- /*
- * FIXME [[rowheight]]: above coded to work only with
- * default row heights - will not work with variable row
- * heights
- */
-
- /*
- * We're at the end of the row container, everything is
- * added to the top.
- */
- paintRemoveRowsAtBottom(removedLogicalInside,
- removedVisualInside);
- updateTopRowLogicalIndex(-removedLogicalInside.length());
- }
-
- else {
- /*
- * We're in a combination, where we need to both scroll
- * up AND show new rows at the bottom.
- *
- * Example: Scrolled down to show the second to last
- * row. Remove two. Viewport scrolls up, revealing the
- * row above row. The last element collapses up and into
- * view.
- *
- * Reminder: this use case handles only the case when
- * there are enough escalator rows to still render a
- * full view. I.e. all escalator rows will _always_ be
- * populated
- */
- /*-
- * 1 1 |1| <- newly rendered
- * |2| |2| |2|
- * |3| ==> |*| ==> |5| <- newly rendered
- * |4| |*|
- * 5 5
- *
- * 1 1 |1| <- newly rendered
- * |2| |*| |4|
- * |3| ==> |*| ==> |5| <- newly rendered
- * |4| |4|
- * 5 5
- */
-
- /*
- * STEP 1:
- *
- * reorganize deprecated escalator rows to bottom, but
- * don't re-render anything yet
- */
- /*-
- * 1 1 1
- * |2| |*| |4|
- * |3| ==> |*| ==> |*|
- * |4| |4| |*|
- * 5 5 5
- */
- int newTop = getRowTop(visualRowOrder
- .get(removedVisualInside.getStart()));
- for (int i = 0; i < removedVisualInside.length(); i++) {
- final Element tr = visualRowOrder
- .remove(removedVisualInside.getStart());
- visualRowOrder.addLast(tr);
- }
-
- for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) {
- final Element tr = visualRowOrder.get(i);
- setRowPosition(tr, 0, newTop);
-
- /*
- * FIXME [[rowheight]]: coded to work only with
- * default row heights - will not work with variable
- * row heights
- */
- newTop += getDefaultRowHeight();
- }
-
- /*
- * STEP 2:
- *
- * manually scroll
- */
- /*-
- * 1 |1| <-- newly rendered (by scrolling)
- * |4| |4|
- * |*| ==> |*|
- * |*|
- * 5 5
- */
- final double newScrollTop = contentBottom
- - calculateHeight();
- setScrollTop(newScrollTop);
- /*
- * Manually call the scroll handler, so we get immediate
- * effects in the escalator.
- */
- scroller.onScroll();
- internalScrollEventCalls++;
-
- /*
- * Move the bottommost (n+1:th) escalator row to top,
- * because scrolling up doesn't handle that for us
- * automatically
- */
- moveAndUpdateEscalatorRows(
- Range.withOnly(escalatorRowCount - 1),
- 0,
- getLogicalRowIndex(visualRowOrder.getFirst()) - 1);
- updateTopRowLogicalIndex(-1);
-
- /*
- * STEP 3:
- *
- * update remaining escalator rows
- */
- /*-
- * |1| |1|
- * |4| ==> |4|
- * |*| |5| <-- newly rendered
- *
- * 5
- */
-
- /*
- * FIXME [[rowheight]]: coded to work only with default
- * row heights - will not work with variable row heights
- */
- final int rowsScrolled = (int) (Math
- .ceil((viewportBottom - (double) contentBottom)
- / getDefaultRowHeight()));
- final int start = escalatorRowCount
- - (removedVisualInside.length() - rowsScrolled);
- final Range visualRefreshRange = Range.between(start,
- escalatorRowCount);
- final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
- .getFirst()) + start;
- // in-place move simply re-renders the rows.
- moveAndUpdateEscalatorRows(visualRefreshRange, start,
- logicalTargetIndex);
- }
- }
- }
-
- updateTopRowLogicalIndex(-removedAbove.length());
-
- /*
- * this needs to be done after the escalator has been shrunk down,
- * or it won't work correctly (due to setScrollTop invocation)
- */
- scroller.recalculateScrollbarsForVirtualViewport();
-
- fireRowVisibilityChangeEvent();
- }
-
- private void paintRemoveRowsAtMiddle(final Range removedLogicalInside,
- final Range removedVisualInside, final int logicalOffset) {
- /*-
- * : : :
- * |2| |2| |2|
- * |3| ==> |*| ==> |4|
- * |4| |4| |6| <- newly rendered
- * : : :
- */
-
- final int escalatorRowCount = visualRowOrder.size();
-
- final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
- .getLast())
- - (removedVisualInside.length() - 1)
- + logicalOffset;
- moveAndUpdateEscalatorRows(removedVisualInside, escalatorRowCount,
- logicalTargetIndex);
-
- // move the surrounding rows to their correct places.
- final ListIterator<Element> iterator = visualRowOrder
- .listIterator(removedVisualInside.getStart());
-
- /*
- * FIXME [[rowheight]]: coded to work only with default row heights
- * - will not work with variable row heights
- */
- int rowTop = (removedLogicalInside.getStart() + logicalOffset)
- * getDefaultRowHeight();
- for (int i = removedVisualInside.getStart(); i < escalatorRowCount
- - removedVisualInside.length(); i++) {
- final Element tr = iterator.next();
- setRowPosition(tr, 0, rowTop);
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- rowTop += getDefaultRowHeight();
- }
- }
-
- private void paintRemoveRowsAtBottom(final Range removedLogicalInside,
- final Range removedVisualInside) {
- /*-
- * :
- * : : |4| <- newly rendered
- * |5| |5| |5|
- * |6| ==> |*| ==> |7|
- * |7| |7|
- */
-
- final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
- .getFirst()) - removedVisualInside.length();
- moveAndUpdateEscalatorRows(removedVisualInside, 0,
- logicalTargetIndex);
-
- // move the surrounding rows to their correct places.
- final ListIterator<Element> iterator = visualRowOrder
- .listIterator(removedVisualInside.getEnd());
- /*
- * FIXME [[rowheight]]: coded to work only with default row heights
- * - will not work with variable row heights
- */
- int rowTop = removedLogicalInside.getStart()
- * getDefaultRowHeight();
- while (iterator.hasNext()) {
- final Element tr = iterator.next();
- setRowPosition(tr, 0, rowTop);
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- rowTop += getDefaultRowHeight();
- }
- }
-
- private int getLogicalRowIndex(final Element element) {
- assert element.getParentNode() == root : "The given element isn't a row element in the body";
- int internalIndex = visualRowOrder.indexOf(element);
- return getTopRowLogicalIndex() + internalIndex;
- }
-
- @Override
- protected void recalculateSectionHeight() {
- // NOOP for body, since it doesn't make any sense.
- }
-
- /**
- * Adjusts the row index and number to be relevant for the current
- * virtual viewport.
- * <p>
- * It converts a logical range of rows index to the matching visual
- * range, truncating the resulting range with the viewport.
- * <p>
- * <ul>
- * <li>Escalator contains logical rows 0..100
- * <li>Current viewport showing logical rows 20..29
- * <li>convertToVisual([20..29]) → [0..9]
- * <li>convertToVisual([15..24]) → [0..4]
- * <li>convertToVisual([25..29]) → [5..9]
- * <li>convertToVisual([26..39]) → [6..9]
- * <li>convertToVisual([0..5]) → [0..-1] <em>(empty)</em>
- * <li>convertToVisual([35..1]) → [0..-1] <em>(empty)</em>
- * <li>convertToVisual([0..100]) → [0..9]
- * </ul>
- *
- * @return a logical range converted to a visual range, truncated to the
- * current viewport. The first visual row has the index 0.
- */
- private Range convertToVisual(final Range logicalRange) {
- if (logicalRange.isEmpty()) {
- return logicalRange;
- } else if (visualRowOrder.isEmpty()) {
- // empty range
- return Range.withLength(0, 0);
- }
-
- /*
- * TODO [[rowheight]]: these assumptions will be totally broken with
- * variable row heights.
- */
- final int maxEscalatorRows = getMaxEscalatorRowCapacity();
- final int currentTopRowIndex = getLogicalRowIndex(visualRowOrder
- .getFirst());
-
- final Range[] partitions = logicalRange.partitionWith(Range
- .withLength(currentTopRowIndex, maxEscalatorRows));
- final Range insideRange = partitions[1];
- return insideRange.offsetBy(-currentTopRowIndex);
- }
-
- @Override
- protected String getCellElementTagName() {
- return "td";
- }
-
- /**
- * Calculates the height of the {@code <tbody>} as it should be rendered
- * in the DOM.
- */
- private double calculateHeight() {
- final int tableHeight = tableWrapper.getOffsetHeight();
- final double footerHeight = footer.heightOfSection;
- final double headerHeight = header.heightOfSection;
- return tableHeight - footerHeight - headerHeight;
- }
-
- @Override
- public void refreshRows(final int index, final int numberOfRows) {
- Profiler.enter("Escalator.BodyRowContainer.refreshRows");
-
- final Range visualRange = convertToVisual(Range.withLength(index,
- numberOfRows));
-
- if (!visualRange.isEmpty()) {
- final int firstLogicalRowIndex = getLogicalRowIndex(visualRowOrder
- .getFirst());
- for (int rowNumber = visualRange.getStart(); rowNumber < visualRange
- .getEnd(); rowNumber++) {
- refreshRow(visualRowOrder.get(rowNumber),
- firstLogicalRowIndex + rowNumber);
- }
- }
-
- Profiler.leave("Escalator.BodyRowContainer.refreshRows");
- }
-
- @Override
- protected Element getTrByVisualIndex(final int index)
- throws IndexOutOfBoundsException {
- if (index >= 0 && index < visualRowOrder.size()) {
- return visualRowOrder.get(index);
- } else {
- throw new IndexOutOfBoundsException("No such visual index: "
- + index);
- }
- }
-
- private void setBodyScrollPosition(final int scrollLeft,
- final int scrollTop) {
- tBodyScrollLeft = scrollLeft;
- tBodyScrollTop = scrollTop;
- position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop);
- }
-
- /**
- * Make sure that there is a correct amount of escalator rows: Add more
- * if needed, or remove any superfluous ones.
- * <p>
- * This method should be called when e.g. the height of the Escalator
- * changes.
- * <p>
- * <em>Note:</em> This method will make sure that the escalator rows are
- * placed in the proper places. By default new rows are added below, but
- * if the content is scrolled down, the rows are populated on top
- * instead.
- */
- public void verifyEscalatorCount() {
- /*
- * This method indeed has a smell very similar to paintRemoveRows
- * and paintInsertRows.
- *
- * Unfortunately, those the code can't trivially be shared, since
- * there are some slight differences in the respective
- * responsibilities. The "paint" methods fake the addition and
- * removal of rows, and make sure to either push existing data out
- * of view, or draw new data into view. Only in some special cases
- * will the DOM element count change.
- *
- * This method, however, has the explicit responsibility to verify
- * that when "something" happens, we still have the correct amount
- * of escalator rows in the DOM, and if not, we make sure to modify
- * that count. Only in some special cases do we need to take into
- * account other things than simply modifying the DOM element count.
- */
-
- Profiler.enter("Escalator.BodyRowContainer.verifyEscalatorCount");
-
- if (!isAttached()) {
- return;
- }
-
- final int maxEscalatorRows = getMaxEscalatorRowCapacity();
- final int neededEscalatorRows = Math.min(maxEscalatorRows,
- body.getRowCount());
- final int neededEscalatorRowsDiff = neededEscalatorRows
- - visualRowOrder.size();
-
- if (neededEscalatorRowsDiff > 0) {
- // needs more
-
- /*
- * This is a workaround for the issue where we might be scrolled
- * to the bottom, and the widget expands beyond the content
- * range
- */
-
- final int index = visualRowOrder.size();
- final int nextLastLogicalIndex;
- if (!visualRowOrder.isEmpty()) {
- nextLastLogicalIndex = getLogicalRowIndex(visualRowOrder
- .getLast()) + 1;
- } else {
- nextLastLogicalIndex = 0;
- }
-
- final boolean contentWillFit = nextLastLogicalIndex < getRowCount()
- - neededEscalatorRowsDiff;
- if (contentWillFit) {
- final List<Element> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
- index, neededEscalatorRowsDiff);
-
- /*
- * Since fillAndPopulateEscalatorRowsIfNeeded operates on
- * the assumption that index == visual index == logical
- * index, we thank for the added escalator rows, but since
- * they're painted in the wrong CSS position, we need to
- * move them to their actual locations.
- *
- * Note: this is the second (see body.paintInsertRows)
- * occasion where fillAndPopulateEscalatorRowsIfNeeded would
- * behave "more correctly" if it only would add escalator
- * rows to the DOM and appropriate bookkeping, and not
- * actually populate them :/
- */
- moveAndUpdateEscalatorRows(
- Range.withLength(index, addedRows.size()), index,
- nextLastLogicalIndex);
- } else {
- /*
- * TODO [[optimize]]
- *
- * We're scrolled so far down that all rows can't be simply
- * appended at the end, since we might start displaying
- * escalator rows that don't exist. To avoid the mess that
- * is body.paintRemoveRows, this is a dirty hack that dumbs
- * the problem down to a more basic and already-solved
- * problem:
- *
- * 1) scroll all the way up 2) add the missing escalator
- * rows 3) scroll back to the original position.
- *
- * Letting the browser scroll back to our original position
- * will automatically solve any possible overflow problems,
- * since the browser will not allow us to scroll beyond the
- * actual content.
- */
-
- final double oldScrollTop = getScrollTop();
- setScrollTop(0);
- scroller.onScroll();
- fillAndPopulateEscalatorRowsIfNeeded(index,
- neededEscalatorRowsDiff);
- setScrollTop(oldScrollTop);
- scroller.onScroll();
- internalScrollEventCalls++;
- }
- }
-
- else if (neededEscalatorRowsDiff < 0) {
- // needs less
-
- final ListIterator<Element> iter = visualRowOrder
- .listIterator(visualRowOrder.size());
- for (int i = 0; i < -neededEscalatorRowsDiff; i++) {
- final Element last = iter.previous();
- for (int c = 0; c < last.getChildCount(); c++) {
- detachPossibleWidgetFromCell((Element) last.getChild(c)
- .cast());
- }
- last.removeFromParent();
- iter.remove();
- }
-
- /*
- * If we were scrolled to the bottom so that we didn't have an
- * extra escalator row at the bottom, we'll probably end up with
- * blank space at the bottom of the escalator, and one extra row
- * above the header.
- *
- * Experimentation idea #1: calculate "scrollbottom" vs content
- * bottom and remove one row from top, rest from bottom. This
- * FAILED, since setHeight has already happened, thus we never
- * will detect ourselves having been scrolled all the way to the
- * bottom.
- */
-
- if (!visualRowOrder.isEmpty()) {
- final int firstRowTop = getRowTop(visualRowOrder.getFirst());
- /*
- * FIXME [[rowheight]]: coded to work only with default row
- * heights - will not work with variable row heights
- */
- final double firstRowMinTop = tBodyScrollTop
- - getDefaultRowHeight();
- if (firstRowTop < firstRowMinTop) {
- final int newLogicalIndex = getLogicalRowIndex(visualRowOrder
- .getLast()) + 1;
- moveAndUpdateEscalatorRows(Range.withOnly(0),
- visualRowOrder.size(), newLogicalIndex);
- }
- }
- }
-
- if (neededEscalatorRowsDiff != 0) {
- fireRowVisibilityChangeEvent();
- }
-
- Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount");
- }
-
- @Override
- protected void reapplyDefaultRowHeights() {
- if (visualRowOrder.isEmpty()) {
- return;
- }
-
- /*
- * As an intermediate step between hard-coded row heights to crazily
- * varying row heights, Escalator will support the modification of
- * the default row height (which is applied to all rows).
- *
- * This allows us to do some assumptions and simplifications for
- * now. This code is intended to be quite short-lived, but gives
- * insight into what needs to be done when row heights change in the
- * body, in a general sense.
- *
- * TODO [[rowheight]] remove this comment once row heights may
- * genuinely vary.
- */
-
- Profiler.enter("Escalator.BodyRowContainer.reapplyDefaultRowHeights");
-
- /* step 1: resize and reposition rows */
- for (int i = 0; i < visualRowOrder.size(); i++) {
- Element tr = visualRowOrder.get(i);
- reapplyRowHeight(tr, getDefaultRowHeight());
-
- final int logicalIndex = getTopRowLogicalIndex() + i;
- setRowPosition(tr, 0, logicalIndex * getDefaultRowHeight());
- }
-
- /*
- * step 2: move scrollbar so that it corresponds to its previous
- * place
- */
-
- /*
- * This ratio needs to be calculated with the scrollsize (not max
- * scroll position) in order to align the top row with the new
- * scroll position.
- */
- double scrollRatio = (double) verticalScrollbar.getScrollPos()
- / (double) verticalScrollbar.getScrollSize();
- scroller.recalculateScrollbarsForVirtualViewport();
- internalScrollEventCalls++;
- verticalScrollbar.setScrollPos((int) (getDefaultRowHeight()
- * getRowCount() * scrollRatio));
- setBodyScrollPosition(horizontalScrollbar.getScrollPos(),
- verticalScrollbar.getScrollPos());
- scroller.onScroll();
-
- /* step 3: make sure we have the correct amount of escalator rows. */
- verifyEscalatorCount();
-
- /*
- * TODO [[rowheight]] This simply doesn't work with variable rows
- * heights.
- */
- setTopRowLogicalIndex(getRowTop(visualRowOrder.getFirst())
- / getDefaultRowHeight());
-
- Profiler.leave("Escalator.BodyRowContainer.reapplyDefaultRowHeights");
- }
- }
-
- private class ColumnConfigurationImpl implements ColumnConfiguration {
- public class Column {
- private static final int DEFAULT_COLUMN_WIDTH_PX = 100;
-
- private int definedWidth = -1;
- private int calculatedWidth = DEFAULT_COLUMN_WIDTH_PX;
-
- public void setWidth(int px) {
- definedWidth = px;
- calculatedWidth = (px >= 0) ? px : DEFAULT_COLUMN_WIDTH_PX;
- }
-
- public int getDefinedWidth() {
- return definedWidth;
- }
-
- public int getCalculatedWidth() {
- return calculatedWidth;
- }
- }
-
- private final List<Column> columns = new ArrayList<Column>();
- private int frozenColumns = 0;
-
- /**
- * A cached array of all the calculated column widths.
- *
- * @see #getCalculatedColumnWidths()
- */
- private int[] widthsArray = null;
-
- /**
- * {@inheritDoc}
- * <p>
- * <em>Implementation detail:</em> This method does no DOM modifications
- * (i.e. is very cheap to call) if there are no rows in the DOM when
- * this method is called.
- *
- * @see #hasSomethingInDom()
- */
- @Override
- public void removeColumns(final int index, final int numberOfColumns) {
- assertArgumentsAreValidAndWithinRange(index, numberOfColumns);
-
- flyweightRow.removeCells(index, numberOfColumns);
-
- // Cope with removing frozen columns
- if (index < frozenColumns) {
- if (index + numberOfColumns < frozenColumns) {
- /*
- * Last removed column was frozen, meaning that all removed
- * columns were frozen. Just decrement the number of frozen
- * columns accordingly.
- */
- frozenColumns -= numberOfColumns;
- } else {
- /*
- * If last removed column was not frozen, we have removed
- * columns beyond the frozen range, so all remaining frozen
- * columns are to the left of the removed columns.
- */
- frozenColumns = index;
- }
- }
-
- List<Column> removedColumns = new ArrayList<Column>();
- for (int i = 0; i < numberOfColumns; i++) {
- removedColumns.add(columns.remove(index));
- }
-
- if (hasSomethingInDom()) {
- for (final AbstractRowContainer rowContainer : rowContainers) {
- rowContainer.paintRemoveColumns(index, numberOfColumns,
- removedColumns);
- }
- }
- }
-
- /**
- * Calculate the width of a row, as the sum of columns' widths.
- *
- * @return the width of a row, in pixels
- */
- public int calculateRowWidth() {
- return getCalculatedColumnsWidth(Range.between(0, getColumnCount()));
- }
-
- private void assertArgumentsAreValidAndWithinRange(final int index,
- final int numberOfColumns) {
- if (numberOfColumns < 1) {
- throw new IllegalArgumentException(
- "Number of columns can't be less than 1 (was "
- + numberOfColumns + ")");
- }
-
- if (index < 0 || index + numberOfColumns > getColumnCount()) {
- throw new IndexOutOfBoundsException("The given "
- + "column range (" + index + ".."
- + (index + numberOfColumns)
- + ") was outside of the current "
- + "number of columns (" + getColumnCount() + ")");
- }
- }
-
- /**
- * {@inheritDoc}
- * <p>
- * <em>Implementation detail:</em> This method does no DOM modifications
- * (i.e. is very cheap to call) if there is no data for rows when this
- * method is called.
- *
- * @see #hasColumnAndRowData()
- */
- @Override
- public void insertColumns(final int index, final int numberOfColumns) {
- if (index < 0 || index > getColumnCount()) {
- throw new IndexOutOfBoundsException("The given index(" + index
- + ") was outside of the current number of columns (0.."
- + getColumnCount() + ")");
- }
-
- if (numberOfColumns < 1) {
- throw new IllegalArgumentException(
- "Number of columns must be 1 or greater (was "
- + numberOfColumns);
- }
-
- flyweightRow.addCells(index, numberOfColumns);
-
- for (int i = 0; i < numberOfColumns; i++) {
- columns.add(index, new Column());
- }
-
- // Either all or none of the new columns are frozen
- boolean frozen = index < frozenColumns;
- if (frozen) {
- frozenColumns += numberOfColumns;
- }
-
- if (hasColumnAndRowData()) {
- for (final AbstractRowContainer rowContainer : rowContainers) {
- rowContainer.paintInsertColumns(index, numberOfColumns,
- frozen);
- }
- }
- }
-
- @Override
- public int getColumnCount() {
- return columns.size();
- }
-
- @Override
- public void setFrozenColumnCount(int count)
- throws IllegalArgumentException {
- if (count < 0 || count > getColumnCount()) {
- throw new IllegalArgumentException(
- "count must be between 0 and the current number of columns ("
- + columns + ")");
- }
- int oldCount = frozenColumns;
- if (count == oldCount) {
- return;
- }
-
- frozenColumns = count;
-
- if (hasSomethingInDom()) {
- // Are we freezing or unfreezing?
- boolean frozen = count > oldCount;
-
- int firstAffectedCol;
- int firstUnaffectedCol;
-
- if (frozen) {
- firstAffectedCol = oldCount;
- firstUnaffectedCol = count;
- } else {
- firstAffectedCol = count;
- firstUnaffectedCol = oldCount;
- }
-
- for (int col = firstAffectedCol; col < firstUnaffectedCol; col++) {
- header.setColumnFrozen(col, frozen);
- body.setColumnFrozen(col, frozen);
- footer.setColumnFrozen(col, frozen);
- }
- }
-
- scroller.recalculateScrollbarsForVirtualViewport();
- }
-
- @Override
- public int getFrozenColumnCount() {
- return frozenColumns;
- }
-
- @Override
- public void setColumnWidth(int index, int px)
- throws IllegalArgumentException {
- checkValidColumnIndex(index);
-
- columns.get(index).setWidth(px);
- widthsArray = null;
-
- /*
- * TODO [[optimize]]: only modify the elements that are actually
- * modified.
- */
- header.reapplyColumnWidths();
- body.reapplyColumnWidths();
- footer.reapplyColumnWidths();
- recalculateElementSizes();
- }
-
- private void checkValidColumnIndex(int index)
- throws IllegalArgumentException {
- if (!Range.withLength(0, getColumnCount()).contains(index)) {
- throw new IllegalArgumentException("The given column index ("
- + index + ") does not exist");
- }
- }
-
- @Override
- public int getColumnWidth(int index) throws IllegalArgumentException {
- checkValidColumnIndex(index);
- return columns.get(index).getDefinedWidth();
- }
-
- @Override
- public int getColumnWidthActual(int index) {
- return columns.get(index).getCalculatedWidth();
- }
-
- /**
- * Calculates the width of the columns in a given range.
- *
- * @param columns
- * the columns to calculate
- * @return the total width of the columns in the given
- * <code>columns</code>
- */
- int getCalculatedColumnsWidth(@SuppressWarnings("hiding")
- final Range columns) {
- /*
- * This is an assert instead of an exception, since this is an
- * internal method.
- */
- assert columns.isSubsetOf(Range.between(0, getColumnCount())) : "Range "
- + "was outside of current column range (i.e.: "
- + Range.between(0, getColumnCount())
- + ", but was given :"
- + columns;
-
- int sum = 0;
- for (int i = columns.getStart(); i < columns.getEnd(); i++) {
- sum += getColumnWidthActual(i);
- }
- return sum;
- }
-
- void setCalculatedColumnWidth(int index, int width) {
- columns.get(index).calculatedWidth = width;
- widthsArray = null;
- }
-
- int[] getCalculatedColumnWidths() {
- if (widthsArray == null || widthsArray.length != getColumnCount()) {
- widthsArray = new int[getColumnCount()];
- for (int i = 0; i < columns.size(); i++) {
- widthsArray[i] = columns.get(i).getCalculatedWidth();
- }
- }
- return widthsArray;
- }
- }
-
- // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y
- /**
- * The solution to
- * <code>|tan<sup>-1</sup>(<i>x</i>)|×(180/π) = 30</code>
- * .
- * <p>
- * This constant is placed in the Escalator class, instead of an inner
- * class, since even mathematical expressions aren't allowed in non-static
- * inner classes for constants.
- */
- private static final double RATIO_OF_30_DEGREES = 1 / Math.sqrt(3);
- /**
- * The solution to
- * <code>|tan<sup>-1</sup>(<i>x</i>)|×(180/π) = 40</code>
- * .
- * <p>
- * This constant is placed in the Escalator class, instead of an inner
- * class, since even mathematical expressions aren't allowed in non-static
- * inner classes for constants.
- */
- private static final double RATIO_OF_40_DEGREES = Math.tan(2 * Math.PI / 9);
-
- private static final String DEFAULT_WIDTH = "400.0px";
- private static final String DEFAULT_HEIGHT = "400.0px";
-
- private FlyweightRow flyweightRow = new FlyweightRow(this);
-
- /** The {@code <thead/>} tag. */
- private final Element headElem = DOM.createTHead();
- /** The {@code <tbody/>} tag. */
- private final Element bodyElem = DOM.createTBody();
- /** The {@code <tfoot/>} tag. */
- private final Element footElem = DOM.createTFoot();
-
- /**
- * TODO: investigate whether this field is now unnecessary, as
- * {@link ScrollbarBundle} now caches its values.
- *
- * @deprecated maybe...
- */
- @Deprecated
- private int tBodyScrollTop = 0;
-
- /**
- * TODO: investigate whether this field is now unnecessary, as
- * {@link ScrollbarBundle} now caches its values.
- *
- * @deprecated maybe...
- */
- @Deprecated
- private int tBodyScrollLeft = 0;
-
- private final VerticalScrollbarBundle verticalScrollbar = new VerticalScrollbarBundle();
- private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle();
-
- private final HeaderRowContainer header = new HeaderRowContainer(headElem);
- private final BodyRowContainer body = new BodyRowContainer(bodyElem);
- private final FooterRowContainer footer = new FooterRowContainer(footElem);
-
- private final Scroller scroller = new Scroller();
-
- private final AbstractRowContainer[] rowContainers = new AbstractRowContainer[] {
- header, body, footer };
-
- private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl();
- private final Element tableWrapper;
-
- private PositionFunction position;
-
- private int internalScrollEventCalls = 0;
-
- /** The cached width of the escalator, in pixels. */
- private double widthOfEscalator;
- /** The cached height of the escalator, in pixels. */
- private double heightOfEscalator;
-
- private static native double getPreciseWidth(Element element)
- /*-{
- if (element.getBoundingClientRect) {
- var rect = element.getBoundingClientRect();
- return rect.right - rect.left;
- } else {
- return element.offsetWidth;
- }
- }-*/;
-
- private static native double getPreciseHeight(Element element)
- /*-{
- if (element.getBoundingClientRect) {
- var rect = element.getBoundingClientRect();
- return rect.bottom - rect.top;
- } else {
- return element.offsetHeight;
- }
- }-*/;
-
- /**
- * Creates a new Escalator widget instance.
- */
- public Escalator() {
-
- detectAndApplyPositionFunction();
- getLogger().info(
- "Using " + position.getClass().getSimpleName()
- + " for position");
-
- final Element root = DOM.createDiv();
- setElement(root);
-
- root.appendChild(verticalScrollbar.getElement());
- root.appendChild(horizontalScrollbar.getElement());
- verticalScrollbar.setScrollbarThickness(Util.getNativeScrollbarSize());
- horizontalScrollbar
- .setScrollbarThickness(Util.getNativeScrollbarSize());
-
- tableWrapper = DOM.createDiv();
-
- root.appendChild(tableWrapper);
-
- final Element table = DOM.createTable();
- tableWrapper.appendChild(table);
-
- table.appendChild(headElem);
- table.appendChild(bodyElem);
- table.appendChild(footElem);
-
- setStylePrimaryName("v-escalator");
-
- // init default dimensions
- setHeight(null);
- setWidth(null);
- }
-
- @Override
- protected void onLoad() {
- super.onLoad();
-
- header.paintInsertRows(0, header.getRowCount());
- footer.paintInsertRows(0, footer.getRowCount());
- recalculateElementSizes();
- /*
- * Note: There's no need to explicitly insert rows into the body.
- *
- * recalculateElementSizes will recalculate the height of the body. This
- * has the side-effect that as the body's size grows bigger (i.e. from 0
- * to its actual height), more escalator rows are populated. Those
- * escalator rows are then immediately rendered. This, in effect, is the
- * same thing as inserting those rows.
- *
- * In fact, having an extra paintInsertRows here would lead to duplicate
- * rows.
- */
-
- scroller.attachScrollListener(verticalScrollbar.getElement());
- scroller.attachScrollListener(horizontalScrollbar.getElement());
- scroller.attachMousewheelListener(getElement());
- scroller.attachTouchListeners(getElement());
- }
-
- @Override
- protected void onUnload() {
-
- scroller.detachScrollListener(verticalScrollbar.getElement());
- scroller.detachScrollListener(horizontalScrollbar.getElement());
- scroller.detachMousewheelListener(getElement());
- scroller.detachTouchListeners(getElement());
-
- header.paintRemoveRows(0, header.getRowCount());
- footer.paintRemoveRows(0, footer.getRowCount());
- body.paintRemoveRows(0, body.getRowCount());
-
- super.onUnload();
- }
-
- private void detectAndApplyPositionFunction() {
- /*
- * firefox has a bug in its translate operation, showing white space
- * when adjusting the scrollbar in BodyRowContainer.paintInsertRows
- */
- if (Window.Navigator.getUserAgent().contains("Firefox")) {
- position = new AbsolutePosition();
- return;
- }
-
- final Style docStyle = Document.get().getBody().getStyle();
- if (hasProperty(docStyle, "transform")) {
- if (hasProperty(docStyle, "transformStyle")) {
- position = new Translate3DPosition();
- } else {
- position = new TranslatePosition();
- }
- } else if (hasProperty(docStyle, "webkitTransform")) {
- position = new WebkitTranslate3DPosition();
- } else {
- position = new AbsolutePosition();
- }
- }
-
- private Logger getLogger() {
- return Logger.getLogger(getClass().getName());
- }
-
- private static native boolean hasProperty(Style style, String name)
- /*-{
- return style[name] !== undefined;
- }-*/;
-
- /**
- * Check whether there are both columns and any row data (for either
- * headers, body or footer).
- *
- * @return <code>true</code> iff header, body or footer has rows && there
- * are columns
- */
- private boolean hasColumnAndRowData() {
- return (header.getRowCount() > 0 || body.getRowCount() > 0 || footer
- .getRowCount() > 0) && columnConfiguration.getColumnCount() > 0;
- }
-
- /**
- * Check whether there are any cells in the DOM.
- *
- * @return <code>true</code> iff header, body or footer has any child
- * elements
- */
- private boolean hasSomethingInDom() {
- return headElem.hasChildNodes() || bodyElem.hasChildNodes()
- || footElem.hasChildNodes();
- }
-
- /**
- * Returns the representation of this Escalator header.
- *
- * @return the header. Never <code>null</code>
- */
- public RowContainer getHeader() {
- return header;
- }
-
- /**
- * Returns the representation of this Escalator body.
- *
- * @return the body. Never <code>null</code>
- */
- public RowContainer getBody() {
- return body;
- }
-
- /**
- * Returns the representation of this Escalator footer.
- *
- * @return the footer. Never <code>null</code>
- */
- public RowContainer getFooter() {
- return footer;
- }
-
- /**
- * Returns the configuration object for the columns in this Escalator.
- *
- * @return the configuration object for the columns in this Escalator. Never
- * <code>null</code>
- */
- public ColumnConfiguration getColumnConfiguration() {
- return columnConfiguration;
- }
-
- /*
- * TODO remove method once RequiresResize and the Vaadin layoutmanager
- * listening mechanisms are implemented (https://trello.com/c/r3Kh0Kfy)
- */
- @Override
- public void setWidth(final String width) {
- super.setWidth(width != null && !width.isEmpty() ? width
- : DEFAULT_WIDTH);
- recalculateElementSizes();
- }
-
- /*
- * TODO remove method once RequiresResize and the Vaadin layoutmanager
- * listening mechanisms are implemented (https://trello.com/c/r3Kh0Kfy)
- */
- @Override
- public void setHeight(final String height) {
- final int escalatorRowsBefore = body.visualRowOrder.size();
-
- super.setHeight(height != null && !height.isEmpty() ? height
- : DEFAULT_HEIGHT);
- recalculateElementSizes();
-
- if (escalatorRowsBefore != body.visualRowOrder.size()) {
- fireRowVisibilityChangeEvent();
- }
- }
-
- /**
- * Returns the vertical scroll offset. Note that this is not necessarily the
- * same as the scroll top in the DOM
- *
- * @return the logical vertical scroll offset
- */
- public double getScrollTop() {
- return verticalScrollbar.getScrollPos();
- }
-
- /**
- * Sets the vertical scroll offset. Note that this is not necessarily the
- * same as the scroll top in the DOM
- *
- * @param scrollTop
- * the number of pixels to scroll vertically
- */
- public void setScrollTop(final double scrollTop) {
- verticalScrollbar.setScrollPos((int) scrollTop);
- }
-
- /**
- * Returns the logical horizontal scroll offset. Note that this is not
- * necessarily the same as the scroll left in the DOM.
- *
- * @return the logical horizontal scroll offset
- */
- public int getScrollLeft() {
- return horizontalScrollbar.getScrollPos();
- }
-
- /**
- * Sets the logical horizontal scroll offset. Note that this is not
- * necessarily the same as the scroll left in the DOM.
- *
- * @param scrollLeft
- * the number of pixels to scroll horizontally
- */
- public void setScrollLeft(final int scrollLeft) {
- horizontalScrollbar.setScrollPos(scrollLeft);
- }
-
- /**
- * Scrolls the body horizontally so that the column at the given index is
- * visible and there is at least {@code padding} pixels to the given scroll
- * destination.
- *
- * @param columnIndex
- * the index of the column to scroll to
- * @param destination
- * where the column should be aligned visually after scrolling
- * @param padding
- * the number pixels to place between the scrolled-to column and
- * the viewport edge.
- * @throws IndexOutOfBoundsException
- * if {@code columnIndex} is not a valid index for an existing
- * column
- * @throws IllegalArgumentException
- * if {@code destination} is {@link ScrollDestination#MIDDLE}
- * and padding is nonzero, because having a padding on a
- * centered column is undefined behavior, or if the column is
- * frozen
- */
- public void scrollToColumn(final int columnIndex,
- final ScrollDestination destination, final int padding)
- throws IndexOutOfBoundsException, IllegalArgumentException {
- if (destination == ScrollDestination.MIDDLE && padding != 0) {
- throw new IllegalArgumentException(
- "You cannot have a padding with a MIDDLE destination");
- }
- verifyValidColumnIndex(columnIndex);
-
- if (columnIndex < columnConfiguration.frozenColumns) {
- throw new IllegalArgumentException("The given column index "
- + columnIndex + " is frozen.");
- }
-
- scroller.scrollToColumn(columnIndex, destination, padding);
- }
-
- private void verifyValidColumnIndex(final int columnIndex)
- throws IndexOutOfBoundsException {
- if (columnIndex < 0
- || columnIndex >= columnConfiguration.getColumnCount()) {
- throw new IndexOutOfBoundsException("The given column index "
- + columnIndex + " does not exist.");
- }
- }
-
- /**
- * Scrolls the body vertically so that the row at the given index is visible
- * and there is at least {@literal padding} pixels to the given scroll
- * destination.
- *
- * @param rowIndex
- * the index of the logical row to scroll to
- * @param destination
- * where the row should be aligned visually after scrolling
- * @param padding
- * the number pixels to place between the scrolled-to row and the
- * viewport edge.
- * @throws IndexOutOfBoundsException
- * if {@code rowIndex} is not a valid index for an existing row
- * @throws IllegalArgumentException
- * if {@code destination} is {@link ScrollDestination#MIDDLE}
- * and padding is nonzero, because having a padding on a
- * centered row is undefined behavior
- */
- public void scrollToRow(final int rowIndex,
- final ScrollDestination destination, final int padding)
- throws IndexOutOfBoundsException, IllegalArgumentException {
- if (destination == ScrollDestination.MIDDLE && padding != 0) {
- throw new IllegalArgumentException(
- "You cannot have a padding with a MIDDLE destination");
- }
- verifyValidRowIndex(rowIndex);
-
- scroller.scrollToRow(rowIndex, destination, padding);
- }
-
- private void verifyValidRowIndex(final int rowIndex) {
- if (rowIndex < 0 || rowIndex >= body.getRowCount()) {
- throw new IndexOutOfBoundsException("The given row index "
- + rowIndex + " does not exist.");
- }
- }
-
- /**
- * Recalculates the dimensions for all elements that require manual
- * calculations. Also updates the dimension caches.
- * <p>
- * <em>Note:</em> This method has the <strong>side-effect</strong>
- * automatically makes sure that an appropriate amount of escalator rows are
- * present. So, if the body area grows, more <strong>escalator rows might be
- * inserted</strong>. Conversely, if the body area shrinks,
- * <strong>escalator rows might be removed</strong>.
- */
- private void recalculateElementSizes() {
- if (!isAttached()) {
- return;
- }
-
- Profiler.enter("Escalator.recalculateElementSizes");
- widthOfEscalator = getPreciseWidth(getElement());
- heightOfEscalator = getPreciseHeight(getElement());
- for (final AbstractRowContainer rowContainer : rowContainers) {
- rowContainer.recalculateSectionHeight();
- }
-
- scroller.recalculateScrollbarsForVirtualViewport();
- body.verifyEscalatorCount();
- Profiler.leave("Escalator.recalculateElementSizes");
- }
-
- /**
- * A routing method for {@link Scroller#onScroll(double, double)}.
- * <p>
- * This is a workaround for GWT and JSNI unable to properly handle inner
- * classes, so instead we call the outer class' method, which calls the
- * inner class' respective method.
- * <p>
- * Ideally, this method would not exist, and
- * {@link Scroller#onScroll(double, double)} would be called directly.
- */
- private void onScroll() {
- scroller.onScroll();
- }
-
- /**
- * Snap deltas of x and y to the major four axes (up, down, left, right)
- * with a threshold of a number of degrees from those axes.
- *
- * @param deltaX
- * the delta in the x axis
- * @param deltaY
- * the delta in the y axis
- * @param thresholdRatio
- * the threshold in ratio (0..1) between x and y for when to snap
- * @return a two-element array: <code>[snappedX, snappedY]</code>
- */
- private static double[] snapDeltas(final double deltaX,
- final double deltaY, final double thresholdRatio) {
-
- final double[] array = new double[2];
- if (deltaX != 0 && deltaY != 0) {
- final double aDeltaX = Math.abs(deltaX);
- final double aDeltaY = Math.abs(deltaY);
- final double yRatio = aDeltaY / aDeltaX;
- final double xRatio = aDeltaX / aDeltaY;
-
- array[0] = (xRatio < thresholdRatio) ? 0 : deltaX;
- array[1] = (yRatio < thresholdRatio) ? 0 : deltaY;
- } else {
- array[0] = deltaX;
- array[1] = deltaY;
- }
-
- return array;
- }
-
- /**
- * Adds an event handler that gets notified when the range of visible rows
- * changes e.g. because of scrolling.
- *
- * @param rowVisibilityChangeHandler
- * the event handler
- * @return a handler registration for the added handler
- */
- public HandlerRegistration addRowVisibilityChangeHandler(
- RowVisibilityChangeHandler rowVisibilityChangeHandler) {
- return addHandler(rowVisibilityChangeHandler,
- RowVisibilityChangeEvent.TYPE);
- }
-
- private void fireRowVisibilityChangeEvent() {
- if (!body.visualRowOrder.isEmpty()) {
- int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder
- .getFirst());
- int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder
- .getLast()) + 1;
-
- int visibleRowCount = visibleRangeEnd - visibleRangeStart;
-
- fireEvent(new RowVisibilityChangeEvent(visibleRangeStart,
- visibleRowCount));
- } else {
- fireEvent(new RowVisibilityChangeEvent(0, 0));
- }
- }
-
- /**
- * Accesses the package private method Widget#setParent()
- *
- * @param widget
- * The widget to access
- * @param parent
- * The parent to set
- */
- static native final void setParent(Widget widget, Widget parent)
- /*-{
- widget.@com.google.gwt.user.client.ui.Widget::setParent(Lcom/google/gwt/user/client/ui/Widget;)(parent);
- }-*/;
-
- /**
- * Returns the widget from a cell node or <code>null</code> if there is no
- * widget in the cell
- *
- * @param cellNode
- * The cell node
- */
- static Widget getWidgetFromCell(Node cellNode) {
- Node possibleWidgetNode = cellNode.getFirstChild();
- if (possibleWidgetNode != null
- && possibleWidgetNode.getNodeType() == Node.ELEMENT_NODE) {
- @SuppressWarnings("deprecation")
- com.google.gwt.user.client.Element castElement = (com.google.gwt.user.client.Element) possibleWidgetNode
- .cast();
- return Util.findWidget(castElement, null);
- }
- return null;
- }
-
- /**
- * Forces the escalator to recalculate the widths of its columns.
- * <p>
- * All columns that haven't been assigned an explicit width will be resized
- * to fit all currently visible contents.
- *
- * @see ColumnConfiguration#setColumnWidth(int, int)
- */
- public void calculateColumnWidths() {
- boolean widthsHaveChanged = false;
- for (int colIndex = 0; colIndex < columnConfiguration.getColumnCount(); colIndex++) {
- if (columnConfiguration.getColumnWidth(colIndex) >= 0) {
- continue;
- }
-
- final int oldColumnWidth = columnConfiguration
- .getColumnWidthActual(colIndex);
-
- int maxColumnWidth = 0;
- maxColumnWidth = Math.max(maxColumnWidth,
- header.calculateMaxColWidth(colIndex));
- maxColumnWidth = Math.max(maxColumnWidth,
- body.calculateMaxColWidth(colIndex));
- maxColumnWidth = Math.max(maxColumnWidth,
- footer.calculateMaxColWidth(colIndex));
-
- Logger.getLogger("Escalator.calculateColumnWidths").info(
- "#" + colIndex + ": " + maxColumnWidth + "px");
-
- if (oldColumnWidth != maxColumnWidth) {
- columnConfiguration.setCalculatedColumnWidth(colIndex,
- maxColumnWidth);
- widthsHaveChanged = true;
- }
- }
-
- if (widthsHaveChanged) {
- header.reapplyColumnWidths();
- body.reapplyColumnWidths();
- footer.reapplyColumnWidths();
- recalculateElementSizes();
- }
- }
-
- @Override
- public void setStylePrimaryName(String style) {
- super.setStylePrimaryName(style);
-
- verticalScrollbar.setStylePrimaryName(style);
- horizontalScrollbar.setStylePrimaryName(style);
-
- UIObject.setStylePrimaryName(tableWrapper, style + "-tablewrapper");
-
- header.setStylePrimaryName(style);
- body.setStylePrimaryName(style);
- footer.setStylePrimaryName(style);
- }
- }
|