New zoom feature for the SpreadsheetView

Work on the SpreadsheetView have been quiet lately. I’m proud to add the zoom feature!

Technical details ahead.

I first tumbled into this code for a zooming pane.

And I thought it would do the trick for the SpreadsheetView. However, this implementation was shrinking or enlarging everything and it gave me two problems:

  • I wanted the zoom as a feature of the SpreadsheetView, and not a pane outside.
  • The scrollBars were also shrunk or enlarged. I wanted a behavior like Microsoft Excel, where the cells and headers are modified but not the scrollBars.
  • So first of all, I added the zoomFactor property inside, and override the layoutChildren method in order to layout the shrunk (or enlarged) part in the same space as before. This is done by binding the zoomFactor property to a Scale added to the SpreadsheetView.
    In the constructor:

    Scale scale = new Scale(1, 1);
                getTransforms().add(scale);
    
                zoomFactor.addListener(new ChangeListener<Number>() {
                    public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                        scale.setX(newValue.doubleValue());
                        scale.setY(newValue.doubleValue());
                        requestLayout();
                    }
                });
    

    And in the code :

    @Override
        protected void layoutChildren() {
            super.layoutChildren();
            Pos pos = Pos.TOP_LEFT;
            double width = getWidth();
            double height = getHeight();
            double top = getInsets().getTop();
            double right = getInsets().getRight();
            double left = getInsets().getLeft();
            double bottom = getInsets().getBottom();
            double contentWidth = (width - left - right) / zoomFactor.get();
            double contentHeight = (height - top - bottom) / zoomFactor.get();
            layoutInArea(getChildren().get(0), left, top,
                    contentWidth, contentHeight,
                    0, null,
                    pos.getHpos(),
                    pos.getVpos());
        }
    
        /**
         * Return the zoomFactor used for the SpreadsheetView.
         * @return 
         */
        public final Double getZoomFactor() {
            return zoomFactor.get();
        }
    
        /**
         * Set a new zoomFactor for the SpreadsheetView. 
         * Advice is not to go beyond 3 and below 0.25.
         * @param zoomFactor 
         */
        public final void setZoomFactor(Double zoomFactor) {
            this.zoomFactor.set(zoomFactor);
        }
    
        /**
         * Return the zoomFactor used for the SpreadsheetView.
         * @return 
         */
        public final DoubleProperty zoomFactorProperty() {
            return zoomFactor;
        }
    

    Regarding the second part, this is where it becomes tricky and a bit a lot hacky.

    Inside the SpreadsheetView’s VirtualFlow, I had to create a Scale object in order to do the opposite of the SpreadsheetView. When my SpreadsheetView is shrinking, I have to enlarge my scrollbars.
    Then I applied it on both ScrollBars but also the little bottom-right corner where scroll bars meet :

     scale = new Scale(1 / spv.getZoomFactor(), 1 / spv.getZoomFactor());
            scale.setPivotX(getHbar().getWidth() / 2);
            getHbar().getTransforms().add(scale);
            getVbar().getTransforms().add(scale);
            corner.getTransforms().add(scale);
            
            spreadSheetView.zoomFactorProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
                scale.setX(1 / newValue.doubleValue());
                scale.setY(1 / newValue.doubleValue());
            });
    

    Now, only thing left is to layout the scrollbars into the proper space like we did in the SpreadsheetView. It should be easy but somehow, I ended up by adding some magic numbers inside…
    For example, my horizontal scrollbar was gently sliding on the right when zooming :



    I have no idea why and I had to add some fixed numbers to correct this behavior. I’m sure I’m missing something here but my solution is working between 25% and 300% of zoom so this is fine for me.

    So I added this in layoutChildren (of VirtualFlow):

    /**
             * Magic numbers coming out of nowhere but I don't understand why
             * the bar are shifting away when zooming...
             */
            layoutInArea(getHbar(), 0 - shift * 10,
                    height - (getHbar().getHeight() * scaleX),
                    contentWidth, contentHeight,
                    0, null,
                    pos.getHpos(),
                    pos.getVpos());
            //VBAR
            layoutInArea(getVbar(), width - getVbar().getWidth() + shift,
                    0,
                    contentWidth, contentHeight,
                    0, null,
                    pos.getHpos(),
                    pos.getVpos());
    
            //CORNER
            if (corner != null) {
                layoutInArea(corner, width - getVbar().getWidth() + shift,
                        getHeight() - (getHbar().getHeight() * scaleX),
                        corner.getWidth(), corner.getHeight(),
                        0, null,
                        pos.getHpos(),
                        pos.getVpos());
            }
    

    Last but not least, I added some shortcut in order to zoom with the keyboard and the mouse wheel. I did it like Excel, that is to say the increment value is 15%. So if you are at 82%, you will go to 85%. So you always step on a normal value instead of having 87% or 102%.

    In the keyPressed handler I added :

    if(keyEvent.isShortcutDown() && KeyCode.NUMPAD0.equals(keyEvent.getCode())){
                //Reset zoom to zero.
                setZoomFactor(1.0);
            }else if(keyEvent.isShortcutDown() && KeyCode.ADD.equals(keyEvent.getCode())){
                incrementZoom();
            }else if(keyEvent.isShortcutDown() && KeyCode.SUBTRACT.equals(keyEvent.getCode())){
                decrementZoom();
            }
    

    Also added these two methods :

     /**
         * Increment the level of zoom by 0.15. The base is 1 so we will try to stay
         * of the intervals (0.1-0.25-0.4-0.55-0.7-0.85-1 etc).
         *
         */
        public void incrementZoom() {
            double newZoom = getZoomFactor();
            int prevValue = (int) ((newZoom - MIN_ZOOM) / STEP_ZOOM);
            newZoom = (prevValue + 1) * STEP_ZOOM + MIN_ZOOM;
            setZoomFactor(newZoom > MAX_ZOOM ? MAX_ZOOM : newZoom);
        }
    
        /**
         * Decrement the level of zoom by 0.15. It will block at 0.25.The base is 1
         * so we will try to stay of the intervals (0.1-0.25-0.4-0.55-0.7-0.85-1
         * etc).
         */
        public void decrementZoom() {
            double newZoom = getZoomFactor()- 0.01;
            int prevValue = (int) ((newZoom - MIN_ZOOM) / STEP_ZOOM);
            newZoom = (prevValue) * STEP_ZOOM + MIN_ZOOM;
            setZoomFactor(newZoom < MIN_ZOOM ? MIN_ZOOM : newZoom);
        }
    

    And here is the part for the mouse wheel :

    //Zoom
            addEventFilter(ScrollEvent.ANY, (ScrollEvent event) -> {
                if (event.isShortcutDown()) {
                    if (event.getTextDeltaY() > 0) {
                        incrementZoom();
                    } else {
                        decrementZoom();
                    }
                    event.consume();
                }
            });
    

    I consume the event so that the SpreadsheetView doesn’t actually scroll vertically.

    Because the SpreadsheetView is part of ControlsFX, you can review all the changes by looking at the pull request or by forking the sources!

    Cheers

    Leave a Reply

    Your email address will not be published. Required fields are marked *