Replacing client-side vaadin widget?

I want to tweak the behaviour of a client-side vaadin widget.

Some tweaks can be done by creating a subclass and use in the gwt.xml.

Now, however, I want to do a tweak that requires access to private variables in the original class.
In this specific instance it is com.vaadin.client.ui.VPopupCalendar that I want to tweak.

As an experiment I have copied the source of the vaadin class to /com/vaadin/client/… in my source directory.
Next I moved my widgetset.xml to my root source directory, and added one entry that points to my own client stuff, and one that points to the vaadin client.

[code]

<source path="com/ec/ptsmc/widgetset/client"/>
<source path="com/vaadin/client"/>

[/code]So, there are now two sources for com.vaadin.client.VPopupCalendar in my project. One from DefaultWidgetSet, or wherever vaadin sources normally come from, and one from my /com/vaadin/client directory

Next I did the required changes to my copy of VPopupCalendar and compiled.

I have now tested both running with superDevMode, and with a regular build.
Everything seems to work as expected. It is my copy of VPopupCalendar that is used.

However, this feels hacky, and I’m not sure if I can trust it…
Could anyone with more experience with gwt or with hacking the vaadin client side comment on this?

Hi,

If you need to access private fields or methods in your GWT code, I would recommend you to use a “violator” pattern. It means that you use JSNI (JavaScript Native Interface) to access those. For example, to access a private field called selectedDate (which type is Label) in your class that extends com.vaadin.client.ui.VPopupCalendar, you could create the following method:

public native Label getSelectedDate() /*-{
    return this.@com.vaadin.client.VPopupCalendar::selectedDate;
}-*/;

And by calling that method in you made the field more visible in your code.


http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html

Nice, thank you for that.

Now I have created an adjusted VPopupCalendar version in the regular way.

My version fixes two things:

  1. It now checks the key modifiers, so it only opens on Alt-Down and not on (anything)-Down
  2. It opens relative to the textfield instead of the button, so that I can have the button on the right hand side

My gwt.xml:

    <replace-with class="com.ec.ptsmc.widgetset.client.MyVPopupCalendar">
        <when-type-is class="com.vaadin.client.ui.VPopupCalendar" />
    </replace-with>

MyVPopupCalendar:
Most of the code is copied from the vaadin version.
openCalendarPanel is only overridden so that I can provide my own PopupPositionCallback.
openCalendarPanel is where I needed the workaround provided by Henri to access a private instance variable in the parent class.

package com.ec.ptsmc.widgetset.client;

import java.util.Date;

import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ComputedStyle;
import com.vaadin.client.VConsole;
import com.vaadin.client.ui.VPopupCalendar;

public class MyVPopupCalendar extends VPopupCalendar {

    @Override
    public void onBrowserEvent(com.google.gwt.user.client.Event event) {
        if (DOM.eventGetType(event) == Event.ONKEYDOWN
                && event.getKeyCode() == getOpenCalenderPanelKey()) {
            
            if((event.getAltKey() || event.getMetaKey()) && !event.getCtrlKey() && !event.getShiftKey()) {
                openCalendarPanel();
                event.preventDefault();
            }
            
        }
        else {
            super.onBrowserEvent(event);
        }
    }

    public native boolean isOpen() /*-{
        return this.@com.vaadin.client.ui.VPopupCalendar::open;
    }-*/;

    public native void setOpen() /*-{
        this.@com.vaadin.client.ui.VPopupCalendar::open = true;
    }-*/;

    /**
     * Opens the calendar panel popup
     */
    public void openCalendarPanel() {

        if (!isOpen() && !readonly && isEnabled()) {
            setOpen();

            if (getCurrentDate() != null) {
                calendar.setDate((Date) getCurrentDate().clone());
            } else {
                calendar.setDate(new Date());
            }

            // clear previous values
            popup.setWidth("");
            popup.setHeight("");
            popup.setPopupPositionAndShow(new PopupPositionCallback());
        } else {
            VConsole.error("Cannot reopen popup, it is already open!");
        }
    }

    private class PopupPositionCallback implements PositionCallback {

        @Override
        public void setPosition(int offsetWidth, int offsetHeight) {
            
            Widget relativeTo = MyVPopupCalendar.this.text;
            
            final int width = offsetWidth;
            final int height = offsetHeight;
            final int browserWindowWidth = Window.getClientWidth()
                    + Window.getScrollLeft();
            final int windowHeight = Window.getClientHeight()
                    + Window.getScrollTop();
            int left = relativeTo.getAbsoluteLeft();

            // Add a little extra space to the right to avoid
            // problems with IE7 scrollbars and to make it look
            // nicer.
            int extraSpace = 30;

            boolean overflow = left + width + extraSpace > browserWindowWidth;
            if (overflow) {
                // Part of the popup is outside the browser window
                // (to the right)
                left = browserWindowWidth - width - extraSpace;
            }

            int extraHeight = 2;
            ComputedStyle style = new ComputedStyle(popup.getElement());
            int[] margins = style.getMargin();

            int top = relativeTo.getAbsoluteTop() + relativeTo.getOffsetHeight() + extraHeight;
            int desiredPopupBottom = top + height + margins[0]
 + margins[2]
;

            if (desiredPopupBottom > windowHeight) {
                
                // Move popup above instead
                top -= height + relativeTo.getOffsetHeight() + margins[0]
 + margins[2]
 + extraHeight;
                
            }

            popup.setPopupPosition(left, top);

            doSetFocus();
        }

        private void doSetFocus() {
            /*
             * We have to wait a while before focusing since the popup needs to
             * be opened before we can focus
             */
            Timer focusTimer = new Timer() {
                @Override
                public void run() {
                    setFocus(true);
                }
            };

            focusTimer.schedule(100);
        }
    }
}

And finally my adjustments to the scss:
This adjusts the placement in the Valo theme like we use it.
The constants should probably refer to some valo variables to be more generic.

    // Override position of datefield icon to be on the right

    .v-datefield [class*="textfield"]
 {
        padding-left: 6px;
        padding-right: 26px;    
    }
    
    .v-datefield [class*="button"]
 {
        position: absolute;
        top: 1px;
        right: 1px;
        left:initial;
        border:none;
        border-left: 1px solid #8498bc;
        border-radius:initial;  
    }

Great that you got it working and thank you for sharing your solution here!

-Henri