понедельник, 31 декабря 2012 г.

HTML5 offline web application with GWT Linker

   Как научить веб-проложения работать как в online, так и в offline режимах? С развитием HTML 5 технологии у разработчиков появилась возможность создавать свои приложения работающие в автономном режиме. Такая особенность кажется очень привлекательной. Что же нужно сделать, чтобы добавить такую функциональность в GWT приложения? Рассмотрим возможность добавления offline режима. 
     Для начала рассмотрим, как приложения работают в offline режиме. Иногда, по разным причинам, когда пользователи работают с веб-приложением пропадает интернет и все взаимодействия приостанавливаются. Для решения этой проблемы разработчику необходимо определить manifest файл(декларация кэша), где перечислены все необходимые ресурсы для автономного режима. Например, представим себе простой апплет погоды состоящий из HTML страницы “weather.html”, файла CSS стилей “weather.css” и JavaScript файла “weather.js”. До того как мы добавим эти файлы в manifest, они будут находится в каталоге все по отдельности. Если пользователь попытается открыть файл weather.html в offline режиме, то получит ошибку. Но можно дописать в manifest эти три файла, как “clock.appcache”. После этого, когда пользователь попытается еще раз открыть страницу с апплетом, то браузер закеширует ресурсы и сделает их доступными, даже если пользователь обратиться к странице в автономном режиме. Полная документация по offline web application
     Чтобы GWT приложение могло работать подобным образом, необходимо кроме файла декларации кэша (в нем описываются файлы и запросы приложения, которые будут закешированны) написать свой Linker(компоновщик). 
     GWT компилирует и оптимизирует код для каждого браузера и сохраняет скомпилированный код в соответствующем каталоге. Все файлы создаются на основе контрольной суммы контента. Проблема заключается в том, что каждый раз, когда были сделаны изменения или перекомпиляция, необходимо также изменить имя файла манифейста декларации кэша. Одно из решений состоит в том, чтобы написать свой Linker. Что же такое linker и как его написать?
     Рассмотрим последовательные шаги по созданию linker-а:
- определить каталог, где будет размещен компоновщик
- создать класс наследуемый от AbstractLinker или Linker
- установить порядок использования LinkerOrder.
Аннотация позволяет устанавливать приоритет выполнения.
Linker.Order.Post означает выполнение после первичного компоновщика.
Linker.Order.Pre означает выполнения до первичного компоновщика.
Linker.Order.Primary означает, что компоновщик будет выполнен вместо первичного.
- создать новый файл модуля <module>.gwt.xml.
     После создания linker-а для его использования необходимо выполнить следующее: 
- наследовать его внутри основного модуля <main_module>.gwt.xml 
- и добавить linker <add-linker name=”my_linker_name” />
     Компоновщик PersonalLinker создан по примеру SimpleAppCacheLinker из GWT trunk code.
/**
* Linker for public path resources in the Application cache.
* based on code from com.google.gwt.core.linker.SimpleAppCacheLinker
*
* @author Dmitry Nikolaenko
*
*/
@LinkerOrder(LinkerOrder.Order.POST)
public class PersonalLinker extends AbstractLinker {

    private static final String MANIFEST = "spaceshootercache.manifest";
    private static final String DEFAULT_MANIFEST_TEMPLATE = "cache.manifest.template";
    private static final String CONFIGURATION_PROPERTY = "cache.manifest";

    @Override
    public ArtifactSet link(TreeLogger logger, LinkerContext context,
            ArtifactSet artifacts, boolean onePermutation) throws UnableToCompleteException {

        // provides stable ordering and de-duplication of artifacts
        // that we can modify and return
        ArtifactSet artifact = new ArtifactSet(artifacts);

        if (onePermutation) {
            return artifact;
        }

        // Find all Artifacts assignable to some base type.
        // The returned value will be a snapshot of the values
        // in the ArtifactSet.
        if (artifact.find(SelectionInformation.class).isEmpty()) {
            logger.log(TreeLogger.INFO, "Warning: " + MANIFEST +
                    " to allow debuging. " + "Recompile before deploying your app!");
            artifacts = null;
        } else {
            // create the general cache-manifest resource for the landing page:
            artifact.add(emitLandingPageCacheManifest(context, logger, artifacts));
        }

        return artifact;
    }

    @Override
    public String getDescription() {
        return "My personal linker description";
    }

    /**
     * Creates the cache-manifest resource specific for the landing page.
     *
     * @param context the linker environment
     * @param logger the tree logger to record to
     * @param artifacts {@code null} to generate an empty cache manifest
     */
    private Artifact<?> emitLandingPageCacheManifest(LinkerContext context,
            TreeLogger logger, ArtifactSet artifacts)
             throws UnableToCompleteException {
        StringBuilder publicSourcesSb = new StringBuilder();
        StringBuilder staticResoucesSb = new StringBuilder();

        if (artifacts != null) {
         // Iterate over all emitted artifacts, and collect all cacheable artifacts
         for (@SuppressWarnings("rawtypes") Artifact artifact : artifacts) {
            if (artifact instanceof EmittedArtifact) {
             EmittedArtifact ea = (EmittedArtifact) artifact;
             String pathName = ea.getPartialPath();
             if (pathName.endsWith("symbolMap")
                 || pathName.endsWith(".xml.gz")
                 || pathName.endsWith("rpc.log")
                 || pathName.endsWith("gwt.rpc")
                 || pathName.endsWith("manifest.txt")
                 || pathName.startsWith("rpcPolicyManifest")) {
                // skip these resources
             } else {
                publicSourcesSb.append(pathName + "\n");
             }
            }
         }

         String[] cacheExtraFiles = getPropertiesExtraFiles(context);

         if (cacheExtraFiles.length == 0) {
             cacheExtraFiles = getCacheExtraFiles();
         }

         for (int i = 0; i < cacheExtraFiles.length; i++) {
            staticResoucesSb.append(cacheExtraFiles[i]);
            staticResoucesSb.append("\n");
         }
        }

        String cacheManifest = createApplicationCache(logger, context, publicSourcesSb, staticResoucesSb);

        logger.log(TreeLogger.INFO, "Be sure your landing page's <html> tag declares a manifest:"
            + " <html manifest=" + context.getModuleFunctionName() + "/" + MANIFEST + "\">");

        // Create the manifest as a new artifact and return it:
        return emitString(logger, cacheManifest, MANIFEST);
    }

    /**
     * Create application cache manifest file
     *
     * @param logger the tree logger to record to
     * @param context the linker environment
     * @param publicSourcesSb public resources that generated by the compilation
     * @param staticResoucesSb static resources
     * @return
     * @throws UnableToCompleteException
     */
    private String createApplicationCache(TreeLogger logger, LinkerContext context,
            StringBuilder publicSourcesSb, StringBuilder staticResoucesSb)
                throws UnableToCompleteException {
        try {
            String manifest = IOUtils.toString(getClass().getResourceAsStream(getCacheManifestTemplate()));
  
            // replace placeholder with the real data
            StringBuilder sbData = new StringBuilder();
            sbData.append(new Date().getTime());
            sbData.append(".");
            sbData.append(Math.random());
  
            manifest = manifest.replace("$UNIQUEAPPID$", sbData.toString());
            manifest = manifest.replace("$STATICAPPFILES$", staticResoucesSb.toString());
            manifest = manifest.replace("$GENAPPFILES$", publicSourcesSb.toString());
  
            return manifest;
        } catch(IOException ex) {
            logger.log(TreeLogger.ERROR, "Cound not read cache manifest file", ex);
            throw new UnableToCompleteException();
        }
    }

    /**
     * You should include this file into <html manifest="" \>
     */
    protected String getCacheManifestTemplate() {
        return DEFAULT_MANIFEST_TEMPLATE;
    }

    /**
     * Obtains the extra files to include in the manifest.
     * Ensures the returned array is not null.
     */
    protected String[] getCacheExtraFiles() {
        String[] cacheExtraFiles = otherCachedFiles();
        return cacheExtraFiles == null ? new String[0] :
            Arrays.copyOf(cacheExtraFiles, cacheExtraFiles.length);
    }

    /**
     * Override this method to force the linker to also include more files in
     * the manifest.
     */
    protected String[] otherCachedFiles() {
        return null;
    }

    /**
     * Get array of configured external properties
     *
     * @param context the linker environment
     * @return external properties
     */
    protected String[] getPropertiesExtraFiles(LinkerContext context) {
        Set<ConfigurationProperty> properties = context.getConfigurationProperties();

        // if properties is empty - no external options, cache.manifest is empty!
        if (!properties.isEmpty()) {
            for (ConfigurationProperty property : properties) {
                if (property.getName().equalsIgnoreCase(CONFIGURATION_PROPERTY)) {
                    List<String> props = property.getValues();
                    return props.toArray(new String[0]);
                }
            }
        }

        return new String[0];
    }
}


* This source code was highlighted with Source Code Highlighter.
     Далее необходимо создать файл модуль компоновщика, где он будет зарегистрирован.
<module>
  <define-linker name="personalManifestLinker"
     class="com.dmitrynikol.spaceshooter.client.linker.PersonalLinker" />
</module>


* This source code was highlighted with Source Code Highlighter.
     Напишем компоновщик приложения наследуя главный PersonalLinker.
@Shardable
public class ApplicationCacheManifestLinker extends PersonalLinker {

    @Override
    protected String[] getCacheExtraFiles() {
        return new String[] { "SpaceShooter.html", "SpaceShooter.css" };
    }

    @Override
    protected String getCacheManifestTemplate() {
        return "spaceshooter.appcache.manifest";
    }
}


* This source code was highlighted with Source Code Highlighter.
     В переопределенных методах getCacheExtraFiles() и getCacheManifestTemplate() указываются кешируемые файлы и название файла шаблона манифеста. Если шаблон не был определен, то будет использоваться шаблон определенный по умолчанию в PersonalLinker. Написанный компоновщик следует добавить в главный модуль приложения <main_app_module>.gwt.xml:
<define-linker name="personalManifestLinker"
         class="com.dmitrynikol.spaceshooter.client.linker.ApplicationCacheManifestLinker" />
 <add-linker name="personalManifestLinker" />


* This source code was highlighted with Source Code Highlighter.
     Manifest файл приложения необходимо дописать в главный html файл, тем самым мы сообщим браузеру откуда считывать файл.
<html manifest="spaceshooter/spaceshootercache.manifest">

* This source code was highlighted with Source Code Highlighter.
     Также в web.xml необходимо добавить следующие строки:
<mime-mapping>
    <extension>personalManifestLinker</extension>
    <mime-type>text/cache-manifest</mime-type>
</mime-mapping>


* This source code was highlighted with Source Code Highlighter.
     Теперь linker приложения готов к использованию. После компилирования в war-нике можно будет увидеть сгенерированный spaceshootercache.manifest вместе с остальными файлами. При запуске приложения в браузере, который поддерживает Offline Web Applications стандарт, можно увидеть панель предлагающую хранить данные на компьютере пользователя для использования в автономном режиме.
     Readme и исходный код доступен на github.

вторник, 2 октября 2012 г.

GWT game development

     В этой статье я не буду рассказывать, как сделать игру на GWT, потому что это займет ряд статей. Я просто опишу, что у меня получилось и какие особенности игры. Исходный код есть в свободном доступе на github и dropbox.
     Игра называется Space Shooter - это аркадный шутер с элементами прокачки. Суть ее сводиться к тому, чтобы уничтожить как можно больше объектов и продержаться как можно дольше. Игрок управляет космическим кораблем и уничтожает постоянно приближающиеся астероиды, за это начисляются очки/баллы. Вначале игрок появляется в центре игрового пространства, с определенным количеством здоровья и нулевым опытом. Сверху на него произвольно приближаются астероиды с различной скоростью, но также есть возможность собирать бонусы, которые позволяют получить игроку временные преимущества. Количество этих объектов постоянно увеличивается в зависимости от набранных балов. Соприкосновение корабля с астероидами отнимает у игрока здоровье. Игра заканчивается если у игрока не осталось жизней, лучший результат будет записан в coookie и извлечен при старте игры и отображен в специальном info блоке.
     Итак, давайте рассмотрим детали 2D игры Space Shooter на GWT. Все действия будут происходить на HTML5 canvas элементе.
     В проекте есть пакет core, где содержаться базовые классы и интерфейсы. Это базовый абстрактный класс GameComponent, он является родительским для всех игровых элементов(Spaceship, Asteroid, Bullet, PowerupElement). И такие интерфейсы как: GameElement, Renderer, Updater. В которых содержаться методы для обновления объекта на канвасе, отрисовки, уничтожения и т.д.
     Все что связано с ресурсами для игры(css файлы, изображения, звуки и мелодии) содержится в пакете bundle. В интерфейсе SpaceShooterClientBundle подключены все необходимые и используемые ресурсы.
     Управление кораблем осуществляется через движение мыши или через клавиши: Up, Down, Left, Right. Стрельба из пушки происходит по нажатию левой клавиши мыши или через Shift. Тип пуль изменяется в зависимости от набранных балов. Также в игре по умолчанию возле каждого игрового объекта рисуется его координаты, включить или отключить это состояние можно с помощью Ctrl. Если же вы решили немножко отдохнуть от всех этих перестрелок или посмотреть, не напрягаясь на все это безумство на канвасе, можно включить паузу нажав на Esc. В состоянии паузы в правом верхнем углу будет отображен специальный лейбл.  
     Обработчики клавиатуры и мыши(GameKeyHandler, GameMouseHandler) содержаться в пакете - handlers. А в пакете math можно найти базовые классы для позиции объектов в игре(Position2D) и их размеров(Size2D).
     Также в проекте есть полезные классы ApplicationContext и ApplicationUtils, в которых содержатся константы, статические поля и очень много интересных методов.
     Типов астероидов в игре всего три: Coaly(углеродистый), Flinty(кремнёвый), Metal(металлический) и они содержаться в перечислении AsteroidType. У каждого астероида есть ускорение (AsteroidAcceleration). Их соответственно тоже три типа: Daydream(дрёма), Nightmare(кошмар) и Madness(безумство).
     Давайте разберемся в магической силе бонусов (power-ups).
     Бонусы, как и астероиды в рандомном порядке приближаются сверху игрового поля. Для игрока доступны следующие бонусы: Freeze(заморозка астероидов), Reflex_boost(замедляет движение всех астероидов на карте), Shield(игрок получает неуязвимость при помощи защитного поля, которое окружает корабль), Double_exp(за каждого уничтоженного астероида игрок получает 2 очка/балла), Medikit(+1 к здоровью игрока). Бонусы очень облегчают игру, но чем больше астероидов на карте, тем больше приходит понимание, что без бонусов никак не обойтись. К тому же бонусы добавляют динамичность и интересность к игровому процессу. Время бонусов длиться в течении 5 секунд после его соприкосновения с кораблем. Нельзя взять два одинаковых бонуса, пока не закончиться время действия бонуса. Но можно взять все бонусы сразу по одному виду. Время окончания действия бонусов будет отображаться пользователю на игровом поле.
     Эффект от бонусов никак не влияет на их появление и скорость движения, что очень приятно для игрока. Например случай когда использован бонус заморозки, все астероиды заморожены, а игрок спокойно подбирает бонусы и использует их по максимуму.
     Для бонусов и их генерирования в проекте есть перечисление Powerup и класс PowerupsGenerator.
     В главном классе SpaceShooter происходит все самое интересное. Включение/отключение главного таймера игры, инициализация основных элементов и обработчиков, регистрация используемых звуков, обновление игровых объектов и информации, проверка столкновений между кораблем/пулей и астероидом, кораблем и бонусом.
     При загрузке игры генерируется градиентный фон и произвольно рисуются множество маленьких звезд разных размеров. Для начала игры следует нажать на кнопку Play Game. Внутри игры в левом верхнем углу расположена info панель, которая содержит набранные баллы, лучший результат, количество жизней, общее время игры и количество кадров в секунду(FPS). Для расчетов fps в проекте предусмотрен класс FrameRate.
     В игре присутствует фоновая музыка и звуки при столкновении. Вся логика обработки звука находится в пакете sound. Где можно найти фабрику SoundManagerFactory, которая позволяет достучаться к менеджеру SoundManager, а он в свою очередь дает нам возможность работать со звуком через SoundPlayer, внутри которого и реализована логика воспроизведения. Вся магия звука возможна благодаря библиотеке gwt-voices.
     Игровой процесс игры Space Shooter во всей своей красе :)







воскресенье, 29 июля 2012 г.

Enumeration ListBox with GWT Editor framework

       Представим следующую ситуацию: вы разрабатываете GWT приложение и ваши сущности(domain entities - основные структурные строительные блоки любой системы) имеют поля в виде перечислений(Enum). Ваши Proxies/DTO также работают с полями в виде перечислений. Но когда данные приходят на клиентскую часть(UI), то виджет ListBox работает только со строковым(String) форматом и необходимо выполнить преобразования из enum в String, и наоборот из String в enum. Но было бы хорошо иметь такой виджет, который бы умел работать с enum как со строками. Также добавим виджету возможность связывания данных между бином и UI-полями. Это возможность доступна благодаря GWT Editor framework
     Для начала создадим список перечислений возможных значений статуса:
  1. public enum Status {
  2.     PROGRESS,
  3.     RESOLVED,
  4.     REOPENED,
  5.     CLOSED,
  6.     OPEN;
  7.     
  8.     @Override
  9.     public String toString() {
  10.         return name().toLowerCase();
  11.     }
  12. }
* This source code was highlighted with Source Code Highlighter.
     Создадим ListBox умеющий работать с перечислением как со строками. Для этого наследуем класс от ValueListBox, который реализует интрефейс IsEditor.
  1. public class EnumerationListBox<E extends Enum<E>> extends ValueListBox<E> {
  2.     public EnumerationListBox() {
  3.         super(new EnumerationRenderer<E>(), new EnumerationKeyProvider<E>());
  4.     }
  5.     
  6.     public EnumerationListBox(Class<E> clazz) {
  7.         this();
  8.         setAcceptableValues(Arrays.asList(clazz.getEnumConstants()));
  9.     }
  10. }
* This source code was highlighted with Source Code Highlighter.
     Также нам необходимо создать классы EnumerationRenderer(отвечает за отрисовку объекта определенного типа в строковой форме) и EnumerationKeyProvider(обеспечивает ключ для элемента списка).
  1. public class EnumerationRenderer<E extends Enum<E>> implements Renderer<E> {
  2.  
  3.     @Override
  4.     public String render(E object) {
  5.         return object == null ? "" : object.toString();
  6.     }
  7.     
  8.     @Override
  9.     public void render(E object, Appendable appendable) throws IOException {
  10.         appendable.append(render(object));
  11.     }
  12. }
* This source code was highlighted with Source Code Highlighter.
  1. public class EnumerationKeyProvider<E extends Enum<E>> implements ProvidesKey<E> {
  2.  
  3.     @Override
  4.     public Object getKey(E item) {
  5.         return item == null ? null : item.name() ;
  6.     }
  7. }
* This source code was highlighted with Source Code Highlighter.
     Теперь когда у нас есть enumeration список, создадим для него контейнер согласно Editor контракту.
  1. public class TaskEditor extends Composite implements Editor<TaskProxy> {
  2.     
  3.     @UiField(provided = true)
  4.     EnumerationListBox<Status> priority = new EnumerationListBox<Status>(Status.class);
  5.     
  6.     public TaskEditor() {
  7.         initWidget(priority);
  8.         
  9.         priority.addValueChangeHandler(new ValueChangeHandler<Status>() {
  10.             @Override
  11.             public void onValueChange(ValueChangeEvent<Status> event) {
  12.                 // and you can do whatever you want with selected event.getValue()
  13.             }
  14.         });
  15.     }
  16. }
* This source code was highlighted with Source Code Highlighter.
     Добавим сюда механизм RequestFactory, который позволяет реализовать уровень доступа к данным на клиенте и сервере. Это позволяет структурировать серверный код в data-ориентированном виде и обеспечивает более высокий уровень абстракции чем GWT-RPC. Первоначально он был создан для CRUD операций на сущности, но позже добавили возможность использования как универсальный механизм RPC.
     Создадим интерфейс TaskProxy для сущности Task и определим необходимые get/set методы. Все методы отображаются(mapped) по именованному соглашению, т.е. имена должны быть идентичными.
  1. @ProxyFor(value = Task.class)
  2. public interface TaskProxy extends EntityProxy {
  3.     
  4.     Status getPriority();
  5.     public void setPriority(Status priority);
  6. }
* This source code was highlighted with Source Code Highlighter.
  1. public class Task {
  2.     @NotNull private Status priority;
  3.     
  4.     public Status getPriority() {
  5.         return priority;
  6.     }
  7.     
  8.     public void setPriority(Status priority) {
  9.         this.priority = priority;
  10.     }
  11. }
* This source code was highlighted with Source Code Highlighter.
     Создание полного client/server-примера, где будут разные Entity, DAO, Proxy, Locator, RequestFactory, RequestContext, Manager-class это уже дело отдельной статьи. А на этом все.

воскресенье, 22 июля 2012 г.

Programmatically fire events in GWT

     Иногда необходимо программно сгенерировать событие на определенном элементе, например на ссылке, метке или кнопке. Вызовем событие клика на ссылке(firstAnchor) из другой ссылки(secondAnchor). Рассмотрим возможные способы как это сделать.
1-ый способ:
  1. firstAnchor.addClickHandler(new ClickHandler() {
  2.     @Override
  3.     public void onClick(ClickEvent event) {
  4.         secondAnchor.fireEvent(new GwtEvent<ClickHandler>(){
  5.             @Override
  6.             public GwtEvent.Type<ClickHandler> getAssociatedType() {
  7.                 return ClickEvent.getType();
  8.             }
  9.             @Override
  10.             protected void dispatch(ClickHandler handler) {
  11.                 handler.onClick(null);
  12.             }
  13.         });
  14.     }
  15. });
* This source code was highlighted with Source Code Highlighter.
2-ой способ:
  1. NativeEvent nativeClickEvent =
  2.     Document.get().createClickEvent(0, 0, 0, 0, 0, false, false, false, false);
  3. DomEvent.fireNativeEvent(nativeClickEvent, secondAnchor);
  4. // or we can create ENTER keydown event
  5. NativeEvent nativeKeyDownEvent =
  6.     Document.get().createKeyDownEvent(false, false, false, false, KeyCodes.KEY_ENTER);
  7. DomEvent.fireNativeEvent(nativeKeyDownEvent, secondAnchor);
* This source code was highlighted with Source Code Highlighter.
3) следующий способ работает, но это небольшой хак :) потому что com.google.gwt.event.dom.client.ClickEvent имеет protected конструктор.
  1. // it works but, it's a little bit hack
  2. secondAnchor.fireEvent(new ClickEvent(){});
* This source code was highlighted with Source Code Highlighter.
4) так как все браузеры реализуют метод click() на элементе, который генерирует событие нажатия на элементе, то можно воспользоваться вызовом через JSNI
  1. click(secondAnchor.getElement());
  2. ...
  3. public static native void click(Element elem) /*-{
  4.     elem.click();
  5. }-*/;
* This source code was highlighted with Source Code Highlighter.
5) или можно скастовать элемент Element в ButtonElement или InputElement и вызвать метод click()
secondAnchor.getElement().<ButtonElement>cast().click();

* This source code was highlighted with Source Code Highlighter.

понедельник, 11 июня 2012 г.

Creating and handling GWT сustom events

     События в GWT используют модель обработки аналогично многим другим фреймворкам. Интерфейс обработчика определяет один или несколько методов, которые виджет вызывает при соответствующем событии. Класс, желающий получить событие определенного типа, реализует соответствующий интерфейс обработчика и передает ссылку на себя виджету, чтобы подписаться на множество событий.
   События всегда отправляют для сообщения чего-то (например, изменение состояния). Давайте представим, что есть игра, где пользователь может ходить, как человек в лабиринте. Каждый раз, когда пользователь сталкивается со стеной, он должен быть проинформирован об этом, чтобы правильно отреагировать (например, стена может отрисовать себя как разрушенную). Это может быть сделано путем отправки событий столкновения, каждый раз когда происходит столкновение со стеной. Это событие посылается человеку и каждому объекту в системе заинтересованному в получении события, чтобы соответ. образом отреагировать. Объекты, которые хотят получать события должны зарегистрировать себя как заинтересованные в событиях. 
       Такое поведение событий свойственно любой системе и фреймворку, а не только в GWT. Для отправки и получения событий в каждой системе должны быть определенны следующие шаги:
1) что отправлять (как должны выглядеть события)
2) кто получает события (получатели)
3) кто отправляет события (отправители)
        А потом можно:
1) зарегистрировать получателей событий, которые хотят получать события
2) отправить события
      Давайте рассмотрим создание и использование собственных событий в GWT. В примере будет показана система, которая отвечает за проверку пожеланий и информирует пользователя, если есть новые пожелания. Предположим, что в системе есть, как минимум два компонента: компонент проверки, который отвечает за проверку нового пожелания и компонент отображения отвечающий за отображение пожелания. Проверяющий компонент отправляет события, когда приходят новые пожелания, а компонент отображения получает эти события. 
     Напишем класс собственного события.
/**
* Class that represent an information of new smile
*/
public class SmileReceivedEvent extends GwtEvent<SmileReceivedEventHandler> {

  // just type of handler
  public static Type<SmileReceivedEventHandler> TYPE =
      new Type<SmileReceivedEventHandler>();

  // let's assume that it will be a wish
  public final String smile;

  public SmileReceivedEvent(String smile) {
    this.smile = smile;
  }

  @Override
  public Type<SmileReceivedEventHandler> getAssociatedType() {
    return TYPE;
  }

  @Override
  protected void dispatch(SmileReceivedEventHandler handler) {
    handler.onSmileReceived(this);
  }

  public String getSmile() {
    return smile;
  }
}


* This source code was highlighted with Source Code Highlighter.
     Класс хранит информацию о пожелании (передается в конструкторе). Получатель события может получить его через метод getSmile(). Каждый класс представляющий собой GWT событие должен наследоваться от класса GwtEvent. Этот класс содержит два абстрактных метода, которые должны быть реализованы: getAssociatedType() и dispatch(). В каждом классе событий они, как правило, реализуются очень похожим образом.
     Любой тип события в GWT связан с интерфейсом представляющим приемника этого типа события. В примере интерфейс получателя события для SmileReceivedEvent наз. SmileReceivedEventHandler.
/**
* Interface that represents event receivers
*/
public interface SmileReceivedEventHandler extends EventHandler {
  public void onSmileReceived(SmileReceivedEvent event);
}


* This source code was highlighted with Source Code Highlighter.
     Каждый обработчик должен наследоваться от EventHandler интерфейса. Следует также определить метод, как минимум с один параметром - событие, который будет вызываться при возникновении события. Каждый получатель может реагировать на событие, через реализацию этого метода.
     Класс SmileShowing является компонентом, который получает событие и отображает пожелания.
/**
* Class that listening an events and reacts on them.
*/
public class SmileShowing extends Label implements SmileReceivedEventHandler {
  @Override
  public void onSmileReceived(SmileReceivedEvent event) {
    this.setText(event.getSmile());
  }
}


* This source code was highlighted with Source Code Highlighter.
     Класс SmileChecker отвечает за проверку пожеланий:
/**
* Class responsible for checking wishes
*/
public class SmileChecker implements HasHandlers {

  private HandlerManager handlerManager;

  public SmileChecker() {
    handlerManager = new HandlerManager(this);
  }

  /**
   * method responsible for sending events
   */
  @Override
  public void fireEvent(GwtEvent<?> event) {
    handlerManager.fireEvent(event);
  }

  /**
   * used by event receivers to register themselves as interested in receiving events.
   */
  public HandlerRegistration addSmileReceivedEventHandler(SmileReceivedEventHandler eventHandler) {
    return handlerManager.addHandler(SmileReceivedEvent.TYPE, eventHandler);
  }

  public void newSmileReceived() {
    fireEvent(new SmileReceivedEvent("This SMILE is just for you :-)"));
  }
}


* This source code was highlighted with Source Code Highlighter.
     Любой класс отправитель событий должен реализовать интерфейс HasHandlers. В этом классе HandlerManager отвечает за управление обработчиками событий. Как было сказано ранее, любой получатель событий, который хочет получать события должен зарегистрировать себя в качестве заинтересованного. Он позволяет регистрировать обработчики событий, а они могут отправлять конкретные события для каждого зарегистрированного обработчика. Когда создается HandlerManager, он принимает один аргумент в конструкторе. Каждое событие имеет источник происхождения и этот параметр будет использоваться в качестве источника для всех событий отправляемых через этот handlerManager.
     Давайте рассмотрим несколько иной способ создания собственных событий. Построим еще одну реализацию событий с использованием GWT EventBus из пакета com.google.web.bindery.event.shared. Пример, как построить собственное событие с использованием GWT 2.4:
/**
* Smile event. This event extends the Event from com.google.web.bindery.event.shared package.
*/
public class SmileReceivedEvent extends Event<SmileReceivedEvent.Handler> {
  /**
   * Implemented by methods that handle SmileReceivedEvent events.
   */
  public interface Handler {
    public void onSmileReceived(SmileReceivedEvent event);
  }

  private static final Type<SmileReceivedEvent.Handler> TYPE =
      new Type<SmileReceivedEvent.Handler>();

  private final String smile;

  public SmileReceivedEvent(String smile) {
    this.smile = smile;
  }

  @Override
  public Type<Handler> getAssociatedType() {
    return TYPE;
  }

  @Override
  protected void dispatch(Handler handler) {
    handler.onSmileReceived(this);
  }

  /**
   * Register a handler for SmileReceivedEvent events on the eventBus
   */
  public static HandlerRegistration register(EventBus eventBus, SmileReceivedEvent.Handler handler) {
    return eventBus.addHandler(TYPE, handler);
  }

  public String getSmile() {
    return smile;
  }
}


* This source code was highlighted with Source Code Highlighter.
     Чтобы зарегистрировать обработчик для этого события с eventBus, нужно вызвать статический метод HandlerRegistration#register(EventBus eventBus, SmileReceivedEvent.Handler handler).
     Класс SmileReceiver является компонентом, который получает событие и отображает пожелание.
/**
* Class that listening an events and reacts on them.
*/
public class SmileReceiver extends Label {
  public SmileReceiver(SimpleEventBus eventBus) {
    SmileReceivedEvent.register(eventBus, new SmileReceivedEvent.Handler() {
      @Override
      public void onSmileReceived(SmileReceivedEvent event) {
        SmileReceiver.this.setText(event.getSmile());
      }
    });
  }
}


* This source code was highlighted with Source Code Highlighter.
     Для инициирования события на eventBus следует вызвать метод fireEvent и передать в качестве аргумента событие.
eventBus.fireEvent(new SmileReceivedEvent("Smile today and everyday! ^__^"));

* This source code was highlighted with Source Code Highlighter.
     Вот как выглядит главный класс для запуска примера:
public class CustomGwtEvent implements EntryPoint {
  public void onModuleLoad() {
    SmileChecker checker = new SmileChecker();
    SmileShowing showingSmile = new SmileShowing();
    // define event receivers and register themselves in event senders
    checker.addSmileReceivedEventHandler(showingSmile);
    checker.newSmileReceived();

    SimpleEventBus eventBus = new SimpleEventBus();
    SmileReceiver receiverSmile = new SmileReceiver(eventBus);
    eventBus.fireEvent(new SmileReceivedEvent("Smile today and everyday! ^__^"));

    RootPanel.get().add(showingSmile);
    RootPanel.get().add(receiverSmile);
  }
}


* This source code was highlighted with Source Code Highlighter.
Eclipse проект можно скачать по следующей ссылке.

суббота, 9 июня 2012 г.

GWT carousel widget

     Рассмотрим создание виджета "карусель" с помощью JQuery и JCoverflip.
JCoverflip является плагином на JQuery, который позволяет организовать виджет типа карусель и отлично подойдет для презентаций различных материалов. Плагин достаточно стабилен и гибкий, позволяет настраивать цвета, шрифты, стили, скорость анимации, количество элементов и др. Взгляните сюда, чтобы увидеть карусель в действии. 
     Подключаем jcoverflip напрямую из папки, а jquery и jquery ui из серверов Google:
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.7.3/jquery-ui.js"></script>
<script type="text/javascript" language="javascript" src="jquery.jcoverflip.js"></script>


* This source code was highlighted with Source Code Highlighter.
данный способ является рекомендуемым Google и наиболее оптимален. Подробную информацию о списке доступных для подключения библиотек и их адресах на серверах можно получить на странице Google Libraries API. Также следует добавить блок стилей в хостовую стораницу Carousel.html, как в инструкции на сайте. Осталось добавить div с изображениями в тело страницы, над которыми будем делать манипуляции.
<div id="carouselWrapper">
 <p id="coverflip">
    <a href="img/prev.gif" rel="previous"><img alt="prev image" src="img/prev.gif" /> </a>            
    <a href="img/play.gif" rel="play"><img alt="play image" src="img/play.gif" /> </a>         
    <a href="img/pause.gif" rel="pause"><img alt="pause image" src="img/pause.gif"/> </a>
    <a href="img/stop.gif" rel="stop"><img alt="stop image" src="img/stop.gif"/> </a>
    <a href="img/next.gif" rel="next"><img alt="next image" src="img/next.gif" /> </a>
 </p>
</div>


* This source code was highlighted with Source Code Highlighter.
     При загрузке приложения следует вызвать JSNI метод showCarousel(), который вызывает jquery методы.
public class Carousel implements EntryPoint {
    public void onModuleLoad() {
        showCarousel();
    }
 
    /**
     * JSNI code that calls on the jquery methods
     * create div element with an anchor and span inside
     */
    public static native void showCarousel() /*-{
         // create the title element
         function createTitle(itemElem) {
             var title = $wnd.jQuery('<div class="title"></div>')
             .append(itemElem.find('img').attr('alt'))
             .append(' <a href="'+itemElem.attr('href')+'">(click here to view image)</a>')
             .append(' <span class="tags">'+itemElem.attr('rel')+'</span>');
             return title;
         }
        
         // delete the title element
         function destroyTitle(titleElem) {
             titleElem.remove();
         }
         
         $wnd.jQuery('#coverflip').jcoverflip({
             time: 800, //animation transition period
             titles: {
                create: createTitle,
              destroy: destroyTitle
            }
         });
    }-*/;
}


* This source code was highlighted with Source Code Highlighter.
     Из готовых GWT решений на просторах интернета есть еще такие варианты: gwt-yui-carouselspiral-carousel-gwt и gwt-carousel. Вот и все, gwt карусель выглядит вот так, анимация и поведение совпадает с демкой на сайте. Eclipse проект можно скачать по следующей ссылке.

понедельник, 27 февраля 2012 г.

GWT обертка для JQuery Slider виджета

   С помощью JavaScript мы можем сделать очень красивые и интересные вещи, например, динамическое зумирование изображений, создание сложных анимаций без flash, разработка мобильных приложений и т.д. На GWT тоже можно сделать, но не всегда получается легко. В GWT проекте мы можем легко и безопасно использовать JQuery через JSNI
   JQuery - javascript библиотека, использование которой делает разработку javascript кода намного быстрее, проще и приятнее. Библиотека помогает легко получать доступ к любому элементу DOM, обращаться к атрибутам и содержимому элементов DOM и манипулировать ими. 
   Рассмотрим создание GWT обертки для JQuery Slider виджета, который имеет больше возможностей и легче настраивается чем любой коробочный ползунок в GWT. Для работы с JQuery в хостовую страницу следует добавить след. строки:
  <link media="all" type="text/css"
    href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.6/themes/base/jquery-ui.css" rel="stylesheet">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"
    type="text/javascript" charset="utf-8"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.6/jquery-ui.min.js"
    type="text/javascript" charset="utf-8"></script>


* This source code was highlighted with Source Code Highlighter.
  Выбор правильного базового класса является важной частью создания полнофункционального GWT компонента. JQuery UI Slider DIV элемент прикрепляется к странице, по этому GWT Slider обертка будет наследоваться от Widget.
public class Slider extends Widget {

    public enum Option {
        DISABLED("disabled"),
        ANIMATE("animate"),
        MAX("max"),
        MIN("min"),
        ORIENTATION("orientation"),
        RANGE("range"),
        STEP("step"),
        VALUE("value"),
        VALUES("values");
 
        private String sliderName;
 
        private Option(String name) {
            sliderName = name;
        }
 
        public String getName() {
            return sliderName;
        }
    }

    private JSONObject defaultOptions;
    private List<SliderListener> listeners = new ArrayList<SliderListener>();
 
     /**
     * Slider with the specified ID
     * @param id - id of the element to create
     * @param options - JSONObject of any possible option, can be null for defaults
     */
    public Slider(String id, JSONObject options) {
        super();
        Element div = DOM.createDiv();
        this.setElement(div);
        div.setId(id);
 
        this.defaultOptions = options;
        if (defaultOptions == null) {
            defaultOptions = getOptions(0, 100, new int[]{0});
        }
    }

    /**
     * Create an options JSONObject. Use SliderOption for keys.
     * @param min - default minimum of the slider
     * @param max - default maximum of the slider
     * @param defaultValues - default points of each anchor
     * @return a JSONObject of Slider options
     */
    public static JSONObject getOptions(int min, int max, int[] defaultValues) {
        JSONObject options = new JSONObject();
        options.put(Option.MIN.getName(), new JSONNumber(min));
        options.put(Option.MAX.getName(), new JSONNumber(max));
        JSONArray arr = convertToJSONArray(defaultValues);
        options.put(Option.VALUES.getName(), arr);
        return options;
    }
}


* This source code was highlighted with Source Code Highlighter.
  Создаем DIV элемент и устанавливаем в качестве элемента в конструкторе. В перечислении Option хранятся все параметры, которые можно задать для ползунка. Javadoc комментарии для каждого варианта скопированы из документации JQuery UI Slider. Статический метод getOptions() позволяет создать опции через создание JSONObject. 
     Теперь давайте свяжем GWT с JQuery через JSNI. Метод onLoad() является хорошим местом для связки JQuery после того, как GWT часть будет загружена.
@Override
protected void onLoad() {
    createSlider(this, getElement().getId(), defaultOptions.getJavaScriptObject());
    super.onLoad();
}

private native void createSlider(Slider slider, String id, JavaScriptObject options) /*-{
    options.start = function(event, ui) {
        slider.@com.dmitrynikol.slider.client.widget.Slider::fireOnStartEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/core/client/JsArrayInteger;)(event, ui.values);
    };
    options.slide = function(event, ui) {
        return slider.@com.dmitrynikol.slider.client.widget.Slider::fireOnSlideEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/core/client/JsArrayInteger;)(event, ui.values);
    };
    options.change = function(event, ui) {
        var hasChange = event.originalEvent ? true : false;
        slider.@com.dmitrynikol.slider.client.widget.Slider::fireOnChangeEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/core/client/JsArrayInteger;Z)(event, ui.values, hasChange);
    };
    options.stop = function(event, ui) {
        slider.@com.dmitrynikol.slider.client.widget.Slider::fireOnStopEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/core/client/JsArrayInteger;)(event, ui.values);
    };
    
    $wnd.$("#" + id).slider(options);
}-*/;


* This source code was highlighted with Source Code Highlighter.
    В дополнение к опциям, установленным в конструкторе, метод createSlider() также отображает события ползунка на соответствующие fireOnXEvent() методы, которые передают JavaScript значения в Java. Это является основой обработки событий. 
     Класс SliderEvent отвечает за все события ползунка, а интерфейс SliderListener слушает ползунок. Каждое событие fire-метода уведомляет всех слушателей и передает native событие.
    /*
     * Fire event methods
     */

    private void fireOnStartEvent(Event event, JsArrayInteger values) {
        int[] vals = convertToIntArray(values);
        SliderEvent sliderEvent = new SliderEvent(event, this, vals);
 
        for (SliderListener listener : listeners) {
            listener.onStart(sliderEvent);
        }
    }

    private boolean fireOnSlideEvent(Event event, JsArrayInteger values) {
        int[] vals = convertToIntArray(values);
        SliderEvent sliderEvent = new SliderEvent(event, this, vals);
 
        for (SliderListener listener : listeners) {
            listener.onStart(sliderEvent);
        }
 
        boolean answer = true;
        for (SliderListener listener : listeners) {
            if (!listener.onSlide(sliderEvent)) {
                // if some of the listeners returns false - return false
                // but let them all do their thing
                answer = false;
            }
        }
 
        return answer;
    }

    private void fireOnChangeEvent(Event event, JsArrayInteger values, boolean hasOriginalEvent) {
        int[] vals = convertToIntArray(values);
        SliderEvent sliderEvent = new SliderEvent(event, this, vals, hasOriginalEvent);
 
        for (SliderListener listener : listeners) {
            listener.onChange(sliderEvent);
        }
    }

    private void fireOnStopEvent(Event event, JsArrayInteger values) {
        int[] vals = convertToIntArray(values);
        SliderEvent sliderEvent = new SliderEvent(event, this, vals);
 
        for (SliderListener listener : listeners) {
            listener.onStop(sliderEvent);
        }
    }


* This source code was highlighted with Source Code Highlighter.
     После инициализации мы можем изменить параметры ползунка с помощью методов-оберток. Каждый из этих методов вызывает соответствующий метод JSNI, чтобы получить или установить опцию.
    /*
     * Wrappers for JSNI methods.
     * Methods calls corresponding JSNI methods to get/set some options
     */

    public void setIntOption(Option option, int value) {
        setIntOption(getElement().getId(), option.getName(), value);
    }

    public int getIntOption(Option option) {
        return getIntOption(getElement().getId(), option.getName());
    }

    public void setBooleanOption(Option option, boolean value) {
        setBooleanOption(getElement().getId(), option.getName(), value);
    }

    public boolean getBooleanOption(Option option) {
        return getBooleanOption(getElement().getId(), option.getName());
    }

    public void setStringOption(Option option, String value) {
        setStringOption(getElement().getId(), option.getName(), value);
    }

    public boolean getStringOption(Option option) {
        return getBooleanOption(getElement().getId(), option.getName());
    }

    /*
     * JSNI methods 
     */

    private native void setIntOption(String id, String option, int value) /*-{
        $wnd.$("#" + id).slider("option", option, value);
    }-*/;

    private native int getIntOption(String id, String option) /*-{
        return $wnd.$("#" + id).slider("option", option);
    }-*/;

    private native void setBooleanOption(String id, String option, boolean value) /*-{
        $wnd.$("#" + id).slider("option", option, value);
    }-*/;

    private native boolean getBooleanOption(String id, String option) /*-{
        return $wnd.$("#" + id).slider("option", option);
    }-*/;

    private native void setStringOption(String id, String option, String value) /*-{
        $wnd.$("#" + id).slider("option", option, value);
    }-*/;

    private native boolean getStringOption(String id, String option) /*-{
        return $wnd.$("#" + id).slider("option", option);
    }-*/;

    private native void setValues(String id, JavaScriptObject values) /*-{
        $wnd.$("#" + id).slider("option", "values", values);
    }-*/;

    private native int getValue(String id, int index) /*-{
        $wnd.$("#" + id).slider("values", index);
    }-*/;


* This source code was highlighted with Source Code Highlighter.
     Slider виджет будет убирать после себя через вызов destroy на ползунке в методе onUnload(), который вызывается немедленно перед тем, как виджет будет отделен от документа в браузере.
    @Override
    protected void onUnload() {
        destroySlider(getElement().getId());
        super.onUnload();
    }
    private native void destroySlider(String id) /*-{
        $wnd.$("#" + id).slider("destroy");
    }-*/;


* This source code was highlighted with Source Code Highlighter.
     Главный стартовый класс WrapperJQuerySlider:
public class WrapperJQuerySlider implements EntryPoint, SliderListener {

    private Slider defaultSlider;
    private Slider stepSlider;
    private Slider rangeSlider;

    private Label defaultLabel;
    private Label stepLabel;
    private Label rangeLabel;

    public void onModuleLoad() {
  
        defaultLabel = new Label("Default value: 0");
        defaultSlider = new Slider("slider");
        defaultSlider.addListener(this);
        RootPanel.get().add(defaultLabel);
        RootPanel.get().add(defaultSlider);
  
        stepLabel = new Label("Step value: 100");
        JSONObject options = Slider.getOptions(20, 180, new int[]{100});
        options.put(Option.STEP.getName(), new JSONNumber(20));
        stepSlider = new Slider(Option.STEP.getName(), options);
        stepSlider.addListener(this);
        RootPanel.get().add(stepLabel);
        RootPanel.get().add(stepSlider);
  
        rangeLabel = new Label("Range values: $50 - $80");
        options = Slider.getOptions(10, 200, new int[]{50, 80});
        options.put(Option.RANGE.toString(), JSONBoolean.getInstance(true)); 
        rangeSlider = new Slider(Option.RANGE.getName(), options);
        rangeSlider.addListener(this);
        RootPanel.get().add(rangeLabel);
        RootPanel.get().add(rangeSlider);
    }

    @Override
    public boolean onSlide(SliderEvent event) {
        Slider slider = event.getSlider();
        if (slider == this.defaultSlider) {
            defaultLabel.setText("Default value: " + event.getValues()[0]);
        } else if (slider == stepSlider) {
            stepLabel.setText("Step value: " + event.getValues()[0]);
        } else if (slider == rangeSlider) {
            rangeLabel.setText("Range values: $" + event.getValues()[0] + " - $" + event.getValues()[1]);
        }
  
        return true;
    }

    @Override
    public void onStart(SliderEvent event) {}

    @Override
    public void onChange(SliderEvent event) {}

    @Override
    public void onStop(SliderEvent event) {}
}


* This source code was highlighted with Source Code Highlighter.
Вот и все, Eclipse проект можно скачать по следующей ссылке.