text:
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * Slide mode displays a single image and has a set of controls to navigate
 * between the images and to edit an image.
 *
 * @param {!HTMLElement} container Main container element.
 * @param {!HTMLElement} content Content container element.
 * @param {!HTMLElement} toolbar Toolbar element.
 * @param {!ImageEditor.Prompt} prompt Prompt.
 * @param {!ErrorBanner} errorBanner Error banner.
 * @param {!cr.ui.ArrayDataModel} dataModel Data model.
 * @param {!cr.ui.ListSelectionModel} selectionModel Selection model.
 * @param {!MetadataModel} metadataModel
 * @param {!ThumbnailModel} thumbnailModel
 * @param {!Object} context Context.
 * @param {!VolumeManagerWrapper} volumeManager Volume manager.
 * @param {function(function())} toggleMode Function to toggle the Gallery mode.
 * @param {function(string):string} displayStringFunction String formatting
 *     function.
 * @constructor
 * @struct
 * @suppress {checkStructDictInheritance}
 * @extends {cr.EventTarget}
 */
function SlideMode(container, content, toolbar, prompt, errorBanner, dataModel,
    selectionModel, metadataModel, thumbnailModel, context, volumeManager,
    toggleMode, displayStringFunction) {
  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.container_ = container;

  /**
   * @type {!Document}
   * @private
   * @const
   */
  this.document_ = assert(container.ownerDocument);

  /**
   * @type {!HTMLElement}
   * @const
   */
  this.content = content;

  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.toolbar_ = toolbar;

  /**
   * @type {!ImageEditor.Prompt}
   * @private
   * @const
   */
  this.prompt_ = prompt;

  /**
   * @type {!ErrorBanner}
   * @private
   * @const
   */
  this.errorBanner_ = errorBanner;

  /**
   * @type {!cr.ui.ArrayDataModel}
   * @private
   * @const
   */
  this.dataModel_ = dataModel;

  /**
   * @type {!cr.ui.ListSelectionModel}
   * @private
   * @const
   */
  this.selectionModel_ = selectionModel;

  /**
   * @type {!Object}
   * @private
   * @const
   */
  this.context_ = context;

  /**
   * @type {!VolumeManagerWrapper}
   * @private
   * @const
   */
  this.volumeManager_ = volumeManager;

  /**
   * @type {function(function())}
   * @private
   * @const
   */
  this.toggleMode_ = toggleMode;

  /**
   * @type {function(string):string}
   * @private
   * @const
   */
  this.displayStringFunction_ = displayStringFunction;

  /**
   * @type {function(this:SlideMode)}
   * @private
   * @const
   */
  this.onSelectionBound_ = this.onSelection_.bind(this);

  /**
   * @type {function(this:SlideMode,!Event)}
   * @private
   * @const
   */
  this.onSpliceBound_ = this.onSplice_.bind(this);

  /**
   * Unique numeric key, incremented per each load attempt used to discard
   * old attempts. This can happen especially when changing selection fast or
   * Internet connection is slow.
   *
   * @type {number}
   * @private
   */
  this.currentUniqueKey_ = 0;

  /**
   * @type {number}
   * @private
   */
  this.sequenceDirection_ = 0;

  /**
   * @type {number}
   * @private
   */
  this.sequenceLength_ = 0;

  /**
   * @type {Array<number>}
   * @private
   */
  this.savedSelection_ = null;

  /**
   * @type {Gallery.Item}
   * @private
   */
  this.displayedItem_ = null;

  /**
   * @type {?number}
   * @private
   */
  this.slideHint_ = null;

  /**
   * @type {boolean}
   * @private
   */
  this.active_ = false;

  /**
   * @type {boolean}
   * @private
   */
  this.leaveAfterSlideshow_ = false;

  /**
   * @type {boolean}
   * @private
   */
  this.fullscreenBeforeSlideshow_ = false;

  /**
   * @type {?number}
   * @private
   */
  this.slideShowTimeout_ = null;

  /**
   * @type {?number}
   * @private
   */
  this.spinnerTimer_ = null;

  window.addEventListener('resize', this.onResize_.bind(this));

  // ----------------------------------------------------------------
  // Initializes the UI.

  /**
   * Container for displayed image.
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.imageContainer_ = util.createChild(queryRequiredElement(
      this.document_, '.content'), 'image-container');
  this.imageContainer_.addEventListener('click', this.onClick_.bind(this));

  this.document_.addEventListener('click', this.onDocumentClick_.bind(this));

  /**
   * Overwrite options and info bubble.
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.options_ = util.createChild(queryRequiredElement(
      this.toolbar_, '.filename-spacer'), 'options');

  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.savedLabel_ = util.createChild(this.options_, 'saved');
  this.savedLabel_.textContent = this.displayStringFunction_('GALLERY_SAVED');

  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.overwriteOriginalBox_ = util.createChild(
      this.options_, 'overwrite-original');

  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.overwriteOriginal_ = util.createChild(
      this.overwriteOriginalBox_, '', 'input');
  this.overwriteOriginal_.type = 'checkbox';
  this.overwriteOriginal_.id = 'overwrite-checkbox';
  chrome.storage.local.get(SlideMode.OVERWRITE_KEY, function(values) {
    var value = values[SlideMode.OVERWRITE_KEY];
    // Out-of-the box default is 'true'
    this.overwriteOriginal_.checked =
        (value === 'false' || value === false) ? false : true;
  }.bind(this));
  this.overwriteOriginal_.addEventListener('click',
      this.onOverwriteOriginalClick_.bind(this));

  /**
   * @type {!HTMLElement}
   * @const
   */
  var overwriteLabel = util.createChild(
      this.overwriteOriginalBox_, '', 'label');
  overwriteLabel.textContent =
      this.displayStringFunction_('GALLERY_OVERWRITE_ORIGINAL');
  overwriteLabel.setAttribute('for', 'overwrite-checkbox');

  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.bubble_ = util.createChild(this.toolbar_, 'bubble');
  this.bubble_.hidden = true;

  /**
   * @type {!HTMLElement}
   * @const
   */
  var bubbleContent = util.createChild(this.bubble_);
  bubbleContent.innerHTML = this.displayStringFunction_(
      'GALLERY_OVERWRITE_BUBBLE');

  util.createChild(this.bubble_, 'pointer bottom', 'span');

  /**
   * @type {!HTMLElement}
   * @const
   */
  var bubbleClose = util.createChild(this.bubble_, 'close-x');
  bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this));

  /**
   * Ribbon and related controls.
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.arrowBox_ = util.createChild(this.container_, 'arrow-box');

  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.arrowLeft_ = util.createChild(
      this.arrowBox_, 'arrow left tool dimmable');
  this.arrowLeft_.addEventListener('click',
      this.advanceManually.bind(this, -1));
  util.createChild(this.arrowLeft_);

  util.createChild(this.arrowBox_, 'arrow-spacer');

  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.arrowRight_ = util.createChild(
      this.arrowBox_, 'arrow right tool dimmable');
  this.arrowRight_.addEventListener('click',
      this.advanceManually.bind(this, 1));
  util.createChild(this.arrowRight_);

  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.ribbonSpacer_ = queryRequiredElement(this.toolbar_, '.ribbon-spacer');

  /**
   * @type {!Ribbon}
   * @private
   * @const
   */
  this.ribbon_ = new Ribbon(
      this.document_, this.dataModel_, this.selectionModel_, thumbnailModel);
  this.ribbonSpacer_.appendChild(this.ribbon_);

  util.createChild(this.container_, 'spinner');

  /**
   * @type {!HTMLElement}
   * @const
   */
  var slideShowButton = queryRequiredElement(this.toolbar_, 'button.slideshow');
  slideShowButton.title = this.displayStringFunction_('GALLERY_SLIDESHOW');
  slideShowButton.addEventListener('click',
      this.startSlideshow.bind(this, SlideMode.SLIDESHOW_INTERVAL_FIRST));

  /**
   * @type {!HTMLElement}
   * @const
   */
  var slideShowToolbar = util.createChild(
      this.container_, 'tool slideshow-toolbar');
  util.createChild(slideShowToolbar, 'slideshow-play').
      addEventListener('click', this.toggleSlideshowPause_.bind(this));
  util.createChild(slideShowToolbar, 'slideshow-end').
      addEventListener('click', this.stopSlideshow_.bind(this));

  // Editor.
  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.editButton_ = queryRequiredElement(this.toolbar_, 'button.edit');
  this.editButton_.title = this.displayStringFunction_('GALLERY_EDIT');
  this.editButton_.disabled = true;  // Disabled by default.
  this.editButton_.addEventListener('click', this.toggleEditor.bind(this));

  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.printButton_ = queryRequiredElement(this.toolbar_, 'button.print');
  this.printButton_.title = this.displayStringFunction_('GALLERY_PRINT');
  this.printButton_.disabled = true;  // Disabled by default.
  this.printButton_.addEventListener('click', this.print_.bind(this));

  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.editBarSpacer_ = queryRequiredElement(this.toolbar_, '.edit-bar-spacer');

  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.editBarMain_ = util.createChild(this.editBarSpacer_, 'edit-main');

  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.editBarMode_ = util.createChild(this.container_, 'edit-modal');

  /**
   * @type {!HTMLElement}
   * @private
   * @const
   */
  this.editBarModeWrapper_ = util.createChild(
      this.editBarMode_, 'edit-modal-wrapper dimmable');
  this.editBarModeWrapper_.hidden = true;

  /**
   * Objects supporting image display and editing.
   * @type {!Viewport}
   * @private
   * @const
   */
  this.viewport_ = new Viewport();

  /**
   * @type {!ImageView}
   * @private
   * @const
   */
  this.imageView_ = new ImageView(
      this.imageContainer_,
      this.viewport_,
      metadataModel);

  /**
   * @type {!ImageEditor}
   * @private
   * @const
   */
  this.editor_ = new ImageEditor(
      this.viewport_,
      this.imageView_,
      this.prompt_,
      {
        root: this.container_,
        image: this.imageContainer_,
        toolbar: this.editBarMain_,
        mode: this.editBarModeWrapper_
      },
      SlideMode.EDITOR_MODES,
      this.displayStringFunction_,
      this.onToolsVisibilityChanged_.bind(this));

  /**
   * @type {!TouchHandler}
   * @private
   * @const
   */
  this.touchHandlers_ = new TouchHandler(this.imageContainer_, this);
}

/**
 * List of available editor modes.
 * @type {!Array<ImageEditor.Mode>}
 * @const
 */
SlideMode.EDITOR_MODES = [
  new ImageEditor.Mode.InstantAutofix(),
  new ImageEditor.Mode.Crop(),
  new ImageEditor.Mode.Exposure(),
  new ImageEditor.Mode.OneClick(
      'rotate_left', 'GALLERY_ROTATE_LEFT', new Command.Rotate(-1)),
  new ImageEditor.Mode.OneClick(
      'rotate_right', 'GALLERY_ROTATE_RIGHT', new Command.Rotate(1))
];

/**
 * Map of the key identifier and offset delta.
 * @enum {!Array<number>})
 * @const
 */
SlideMode.KEY_OFFSET_MAP = {
  'Up': [0, 20],
  'Down': [0, -20],
  'Left': [20, 0],
  'Right': [-20, 0]
};

/**
 * SlideMode extends cr.EventTarget.
 */
SlideMode.prototype.__proto__ = cr.EventTarget.prototype;

/**
 * @return {string} Mode name.
 */
SlideMode.prototype.getName = function() { return 'slide'; };

/**
 * @return {string} Mode title.
 */
SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE'; };

/**
 * @return {!Viewport} Viewport.
 */
SlideMode.prototype.getViewport = function() { return this.viewport_; };

/**
 * Load items, display the selected item.
 * @param {ImageRect} zoomFromRect Rectangle for zoom effect.
 * @param {function()} displayCallback Called when the image is displayed.
 * @param {function()} loadCallback Called when the image is displayed.
 */
SlideMode.prototype.enter = function(
    zoomFromRect, displayCallback, loadCallback) {
  this.sequenceDirection_ = 0;
  this.sequenceLength_ = 0;

  // The latest |leave| call might have left the image animating. Remove it.
  this.unloadImage_();
  this.errorBanner_.clear();

  new Promise(function(fulfill) {
    // If the items are empty, just show the error message.
    if (this.getItemCount_() === 0) {
      this.displayedItem_ = null;
      this.errorBanner_.show('GALLERY_NO_IMAGES');
      fulfill();
      return;
    }

    // Remember the selection if it is empty or multiple. It will be restored
    // in |leave| if the user did not changing the selection manually.
    var currentSelection = this.selectionModel_.selectedIndexes;
    if (currentSelection.length === 1)
      this.savedSelection_ = null;
    else
      this.savedSelection_ = currentSelection;

    // Ensure valid single selection.
    // Note that the SlideMode object is not listening to selection change yet.
    this.select(Math.max(0, this.getSelectedIndex()));

    // Show the selected item ASAP, then complete the initialization
    // (loading the ribbon thumbnails can take some time).
    var selectedItem = this.getSelectedItem();
    this.displayedItem_ = selectedItem;

    // Load the image of the item.
    this.loadItem_(
        assert(selectedItem),
        zoomFromRect ?
            this.imageView_.createZoomEffect(zoomFromRect) :
            new ImageView.Effect.None(),
        displayCallback,
        function(loadType, delay) {
          fulfill(delay);
        });
  }.bind(this)).then(function(delay) {
    // Turn the mode active.
    this.active_ = true;
    ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
    this.ribbon_.enable();

    // Register handlers.
    this.selectionModel_.addEventListener('change', this.onSelectionBound_);
    this.dataModel_.addEventListener('splice', this.onSpliceBound_);
    this.touchHandlers_.enabled = true;

    // Wait 1000ms after the animation is done, then prefetch the next image.
    this.requestPrefetch(1, delay + 1000);

    // Call load callback.
    if (loadCallback)
      loadCallback();
  }.bind(this)).catch(function(error) {
    console.error(error.stack, error);
  });
};

/**
 * Leave the mode.
 * @param {ImageRect} zoomToRect Rectangle for zoom effect.
 * @param {function()} callback Called when the image is committed and
 *   the zoom-out animation has started.
 */
SlideMode.prototype.leave = function(zoomToRect, callback) {
  var commitDone = function() {
    this.stopEditing_();
    this.stopSlideshow_();
    ImageUtil.setAttribute(this.arrowBox_, 'active', false);
    this.selectionModel_.removeEventListener(
        'change', this.onSelectionBound_);
    this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
    this.ribbon_.disable();
    this.active_ = false;
    if (this.savedSelection_)
      this.selectionModel_.selectedIndexes = this.savedSelection_;
    this.unloadImage_(zoomToRect);
    callback();
  }.bind(this);

  this.viewport_.resetView();
  if (this.getItemCount_() === 0) {
    this.errorBanner_.clear();
    commitDone();
  } else {
    this.commitItem_(commitDone);
  }

  // Disable the slide-mode only buttons when leaving.
  this.editButton_.disabled = true;
  this.printButton_.disabled = true;

  // Disable touch operation.
  this.touchHandlers_.enabled = false;
};

/**
 * Execute an action when the editor is not busy.
 *
 * @param {function()} action Function to execute.
 */
SlideMode.prototype.executeWhenReady = function(action) {
  this.editor_.executeWhenReady(action);
};

/**
 * @return {boolean} True if the mode has active tools (that should not fade).
 */
SlideMode.prototype.hasActiveTool = function() {
  return this.isEditing();
};

/**
 * @return {number} Item count.
 * @private
 */
SlideMode.prototype.getItemCount_ = function() {
  return this.dataModel_.length;
};

/**
 * @param {number} index Index.
 * @return {Gallery.Item} Item.
 */
SlideMode.prototype.getItem = function(index) {
  var item =
      /** @type {(Gallery.Item|undefined)} */ (this.dataModel_.item(index));
  return item === undefined ? null : item;
};

/**
 * @return {number} Selected index.
 */
SlideMode.prototype.getSelectedIndex = function() {
  return this.selectionModel_.selectedIndex;
};

/**
 * @return {ImageRect} Screen rectangle of the selected image.
 */
SlideMode.prototype.getSelectedImageRect = function() {
  if (this.getSelectedIndex() < 0)
    return null;
  else
    return this.viewport_.getImageBoundsOnScreen();
};

/**
 * @return {Gallery.Item} Selected item.
 */
SlideMode.prototype.getSelectedItem = function() {
  return this.getItem(this.getSelectedIndex());
};

/**
 * Toggles the full screen mode.
 * @private
 */
SlideMode.prototype.toggleFullScreen_ = function() {
  util.toggleFullScreen(this.context_.appWindow,
                        !util.isFullScreen(this.context_.appWindow));
};

/**
 * Selection change handler.
 *
 * Commits the current image and displays the newly selected image.
 * @private
 */
SlideMode.prototype.onSelection_ = function() {
  if (this.selectionModel_.selectedIndexes.length === 0)
    return;  // Ignore temporary empty selection.

  // Forget the saved selection if the user changed the selection manually.
  if (!this.isSlideshowOn_())
    this.savedSelection_ = null;

  if (this.getSelectedItem() === this.displayedItem_)
    return;  // Do not reselect.

  this.commitItem_(this.loadSelectedItem_.bind(this));
};

/**
 * Handles changes in tools visibility, and if the header is dimmed, then
 * requests disabling the draggable app region.
 *
 * @private
 */
SlideMode.prototype.onToolsVisibilityChanged_ = function() {
  var headerDimmed = queryRequiredElement(this.document_, '.header')
      .hasAttribute('dimmed');
  this.context_.onAppRegionChanged(!headerDimmed);
};

/**
 * Change the selection.
 *
 * @param {number} index New selected index.
 * @param {number=} opt_slideHint Slide animation direction (-1|1).
 */
SlideMode.prototype.select = function(index, opt_slideHint) {
  this.slideHint_ = opt_slideHint || null;
  this.selectionModel_.selectedIndex = index;
  this.selectionModel_.leadIndex = index;
};

/**
 * Load the selected item.
 *
 * @private
 */
SlideMode.prototype.loadSelectedItem_ = function() {
  var slideHint = this.slideHint_;
  this.slideHint_ = null;

  if (this.getSelectedItem() === this.displayedItem_)
    return;  // Do not reselect.

  var index = this.getSelectedIndex();
  var displayedIndex = this.dataModel_.indexOf(this.displayedItem_);
  var step =
      slideHint || (displayedIndex > 0 ? index - displayedIndex : 1);

  if (Math.abs(step) != 1) {
    // Long leap, the sequence is broken, we have no good prefetch candidate.
    this.sequenceDirection_ = 0;
    this.sequenceLength_ = 0;
  } else if (this.sequenceDirection_ === step) {
    // Keeping going in sequence.
    this.sequenceLength_++;
  } else {
    // Reversed the direction. Reset the counter.
    this.sequenceDirection_ = step;
    this.sequenceLength_ = 1;
  }

  this.displayedItem_ = this.getSelectedItem();
  var selectedItem = assertInstanceof(this.getSelectedItem(), Gallery.Item);

  function shouldPrefetch(loadType, step, sequenceLength) {
    // Never prefetch when selecting out of sequence.
    if (Math.abs(step) != 1)
      return false;

    // Always prefetch if the previous load was from cache.
    if (loadType === ImageView.LoadType.CACHED_FULL)
      return true;

    // Prefetch if we have been going in the same direction for long enough.
    return sequenceLength >= 3;
  }

  this.currentUniqueKey_++;
  var selectedUniqueKey = this.currentUniqueKey_;

  // Discard, since another load has been invoked after this one.
  if (selectedUniqueKey != this.currentUniqueKey_)
    return;

  this.loadItem_(
      selectedItem,
      new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()),
      function() {} /* no displayCallback */,
      function(loadType, delay) {
        // Discard, since another load has been invoked after this one.
        if (selectedUniqueKey != this.currentUniqueKey_)
          return;
        if (shouldPrefetch(loadType, step, this.sequenceLength_))
          this.requestPrefetch(step, delay);
        if (this.isSlideshowPlaying_())
          this.scheduleNextSlide_();
      }.bind(this));
};

/**
 * Unload the current image.
 *
 * @param {ImageRect=} opt_zoomToRect Rectangle for zoom effect.
 * @private
 */
SlideMode.prototype.unloadImage_ = function(opt_zoomToRect) {
  this.imageView_.unload(opt_zoomToRect);
};

/**
 * Data model 'splice' event handler.
 * @param {!Event} event Event.
 * @this {SlideMode}
 * @private
 */
SlideMode.prototype.onSplice_ = function(event) {
  ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);

  // Splice invalidates saved indices, drop the saved selection.
  this.savedSelection_ = null;

  if (event.removed.length != 1)
    return;

  // Delay the selection to let the ribbon splice handler work first.
  setTimeout(function() {
    var displayedItemNotRemvoed = event.removed.every(function(item) {
      return item !== this.displayedItem_;
    }.bind(this));
    if (displayedItemNotRemvoed)
      return;
    var nextIndex;
    if (event.index < this.dataModel_.length) {
      // There is the next item, select it.
      // The next item is now at the same index as the removed one, so we need
      // to correct displayIndex_ so that loadSelectedItem_ does not think
      // we are re-selecting the same item (and does right-to-left slide-in
      // animation).
      nextIndex = event.index;
    } else if (this.dataModel_.length) {
      // Removed item is the rightmost, but there are more items.
      nextIndex = event.index - 1;  // Select the new last index.
    } else {
      // No items left. Unload the image, disable edit and print button, and
      // show the banner.
      this.commitItem_(function() {
        this.unloadImage_();
        this.printButton_.disabled = true;
        this.editButton_.disabled = true;
        this.errorBanner_.show('GALLERY_NO_IMAGES');
      }.bind(this));
      return;
    }
    // To force to dispatch a selection change event, clear selection before.
    this.selectionModel_.clear();
    this.select(nextIndex);
  }.bind(this), 0);
};

/**
 * @param {number} direction -1 for left, 1 for right.
 * @return {number} Next index in the given direction, with wrapping.
 * @private
 */
SlideMode.prototype.getNextSelectedIndex_ = function(direction) {
  function advance(index, limit) {
    index += (direction > 0 ? 1 : -1);
    if (index < 0)
      return limit - 1;
    if (index === limit)
      return 0;
    return index;
  }

  // If the saved selection is multiple the Slideshow should cycle through
  // the saved selection.
  if (this.isSlideshowOn_() &&
      this.savedSelection_ && this.savedSelection_.length > 1) {
    var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()),
        this.savedSelection_.length);
    return this.savedSelection_[pos];
  } else {
    return advance(this.getSelectedIndex(), this.getItemCount_());
  }
};

/**
 * Advance the selection based on the pressed key ID.
 * @param {string} keyID Key identifier.
 */
SlideMode.prototype.advanceWithKeyboard = function(keyID) {
  var prev = (keyID === 'Up' ||
              keyID === 'Left' ||
              keyID === 'MediaPreviousTrack');
  this.advanceManually(prev ? -1 : 1);
};

/**
 * Advance the selection as a result of a user action (as opposed to an
 * automatic change in the slideshow mode).
 * @param {number} direction -1 for left, 1 for right.
 */
SlideMode.prototype.advanceManually = function(direction) {
  if (this.isSlideshowPlaying_())
    this.pauseSlideshow_();
  cr.dispatchSimpleEvent(this, 'useraction');
  this.selectNext(direction);
};

/**
 * Select the next item.
 * @param {number} direction -1 for left, 1 for right.
 */
SlideMode.prototype.selectNext = function(direction) {
  this.select(this.getNextSelectedIndex_(direction), direction);
};

/**
 * Select the first item.
 */
SlideMode.prototype.selectFirst = function() {
  this.select(0);
};

/**
 * Select the last item.
 */
SlideMode.prototype.selectLast = function() {
  this.select(this.getItemCount_() - 1);
};

// Loading/unloading

/**
 * Load and display an item.
 *
 * @param {!Gallery.Item} item Item.
 * @param {!ImageView.Effect} effect Transition effect object.
 * @param {function()} displayCallback Called when the image is displayed
 *     (which can happen before the image load due to caching).
 * @param {function(number, number)} loadCallback Called when the image is fully
 *     loaded.
 * @private
 */
SlideMode.prototype.loadItem_ = function(
    item, effect, displayCallback, loadCallback) {
  this.showSpinner_(true);

  var loadDone = this.itemLoaded_.bind(this, item, loadCallback);

  var displayDone = function() {
    cr.dispatchSimpleEvent(this, 'image-displayed');
    displayCallback();
  }.bind(this);

  this.editor_.openSession(
      item,
      effect,
      this.saveCurrentImage_.bind(this, item),
      displayDone,
      loadDone);
};

/**
 * A callback function when the editor opens a editing session for an image.
 * @param {!Gallery.Item} item Gallery item.
 * @param {function(number, number)} loadCallback Called when the image is fully
 *     loaded.
 * @param {number} loadType Load type.
 * @param {number} delay Delay.
 * @param {*=} opt_error Error.
 * @private
 */
SlideMode.prototype.itemLoaded_ = function(
    item, loadCallback, loadType, delay, opt_error) {
  var entry = item.getEntry();

  this.showSpinner_(false);
  if (loadType === ImageView.LoadType.ERROR) {
    // if we have a specific error, then display it
    if (opt_error) {
      this.errorBanner_.show(/** @type {string} */ (opt_error));
    } else {
      // otherwise try to infer general error
      this.errorBanner_.show('GALLERY_IMAGE_ERROR');
    }
  } else if (loadType === ImageView.LoadType.OFFLINE) {
    this.errorBanner_.show('GALLERY_IMAGE_OFFLINE');
  }

  ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View'));

  var toMillions = function(number) {
    return Math.round(number / (1000 * 1000));
  };

  var metadata = item.getMetadataItem();
  if (metadata) {
    ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'),
        toMillions(metadata.size));
  }

  var canvas = this.imageView_.getCanvas();
  ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'),
      toMillions(canvas.width * canvas.height));

  var extIndex = entry.name.lastIndexOf('.');
  var ext = extIndex < 0 ? '' :
      entry.name.substr(extIndex + 1).toLowerCase();
  if (ext === 'jpeg') ext = 'jpg';
  ImageUtil.metrics.recordEnum(
      ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES);

  // Enable or disable buttons for editing and printing.
  if (opt_error) {
    this.editButton_.disabled = true;
    this.printButton_.disabled = true;
  } else {
    this.editButton_.disabled = false;
    this.printButton_.disabled = false;
  }

  // For once edited image, disallow the 'overwrite' setting change.
  ImageUtil.setAttribute(this.overwriteOriginalBox_, 'disabled',
      !this.getSelectedItem().isOriginal() ||
      FileType.isRaw(item.getEntry()) ||
      GalleryUtil.isOnMTPVolume(item.getEntry(), this.volumeManager_));

  var keys = {};
  keys[SlideMode.OVERWRITE_BUBBLE_KEY] = 0;
  keys[SlideMode.OVERWRITE_KEY] = true;
  chrome.storage.local.get(keys,
      function(values) {
        var times = values[SlideMode.OVERWRITE_BUBBLE_KEY];
        if (times < SlideMode.OVERWRITE_BUBBLE_MAX_TIMES) {
          this.bubble_.hidden = false;
          if (this.isEditing()) {
            var items = {};
            items[SlideMode.OVERWRITE_BUBBLE_KEY] = times + 1;
            chrome.storage.local.set(items);
          }
        }
        if (FileType.isRaw(item.getEntry()))
          this.overwriteOriginal_.checked = false;
        else
          this.overwriteOriginal_.checked = values[SlideMode.OVERWRITE_KEY];
      }.bind(this));

  loadCallback(loadType, delay);
};

/**
 * Commit changes to the current item and reset all messages/indicators.
 *
 * @param {function()} callback Callback.
 * @private
 */
SlideMode.prototype.commitItem_ = function(callback) {
  this.showSpinner_(false);
  this.errorBanner_.clear();
  this.editor_.getPrompt().hide();
  this.editor_.closeSession(callback);
};

/**
 * Request a prefetch for the next image.
 *
 * @param {number} direction -1 or 1.
 * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image
 *   loading from disrupting the animation that might be still in progress.
 */
SlideMode.prototype.requestPrefetch = function(direction, delay) {
  if (this.getItemCount_() <= 1) return;

  var index = this.getNextSelectedIndex_(direction);
  this.imageView_.prefetch(assert(this.getItem(index)), delay);
};

// Event handlers.

/**
 * Click handler for the image container.
 *
 * @param {!Event} event Mouse click event.
 * @private
 */
SlideMode.prototype.onClick_ = function(event) {
};

/**
 * Click handler for the entire document.
 * @param {!Event} event Mouse click event.
 * @private
 */
SlideMode.prototype.onDocumentClick_ = function(event) {
  // Events created in fakeMouseClick in test util don't pass this test.
  if (!window.IN_TEST)
    event = assertInstanceof(event, MouseEvent);

  var targetElement = assertInstanceof(event.target, HTMLElement);
  // Close the bubble if clicked outside of it and if it is visible.
  if (!this.bubble_.contains(targetElement) &&
      !this.editButton_.contains(targetElement) &&
      !this.arrowLeft_.contains(targetElement) &&
      !this.arrowRight_.contains(targetElement) &&
      !this.bubble_.hidden) {
    this.bubble_.hidden = true;
  }
};

/**
 * Keydown handler.
 *
 * @param {!Event} event Event.
 * @return {boolean} True if handled.
 */
SlideMode.prototype.onKeyDown = function(event) {
  var keyID = util.getKeyModifiers(event) + event.keyIdentifier;

  if (this.isSlideshowOn_()) {
    switch (keyID) {
      case 'U+001B':  // Escape exits the slideshow.
      case 'MediaStop':
        this.stopSlideshow_(event);
        break;

      case 'U+0020':  // Space pauses/resumes the slideshow.
      case 'MediaPlayPause':
        this.toggleSlideshowPause_();
        break;

      case 'Up':
      case 'Down':
      case 'Left':
      case 'Right':
      case 'MediaNextTrack':
      case 'MediaPreviousTrack':
        this.advanceWithKeyboard(keyID);
        break;
    }
    return true;  // Consume all keystrokes in the slideshow mode.
  }

  if (this.isEditing() && this.editor_.onKeyDown(event))
    return true;

  switch (keyID) {
    case 'Ctrl-U+0050':  // Ctrl+'p' prints the current image.
      if (!this.printButton_.disabled)
        this.print_();
      break;

    case 'U+0045':  // 'e' toggles the editor.
      if (!this.editButton_.disabled)
        this.toggleEditor(event);
      break;

    case 'U+001B':  // Escape
      if (this.isEditing()) {
        this.toggleEditor(event);
      } else if (this.viewport_.isZoomed()) {
        this.viewport_.resetView();
        this.touchHandlers_.stopOperation();
        this.imageView_.applyViewportChange();
      } else {
        return false;  // Not handled.
      }
      break;

    case 'Home':
      this.selectFirst();
      break;
    case 'End':
      this.selectLast();
      break;
    case 'Up':
    case 'Down':
    case 'Left':
    case 'Right':
      if (!this.isEditing() && this.viewport_.isZoomed()) {
        var delta = SlideMode.KEY_OFFSET_MAP[keyID];
        this.viewport_.setOffset(
            ~~(this.viewport_.getOffsetX() +
               delta[0] * this.viewport_.getZoom()),
            ~~(this.viewport_.getOffsetY() +
               delta[1] * this.viewport_.getZoom()));
        this.touchHandlers_.stopOperation();
        this.imageView_.applyViewportChange();
      } else {
        this.advanceWithKeyboard(keyID);
      }
      break;
    case 'MediaNextTrack':
    case 'MediaPreviousTrack':
      this.advanceWithKeyboard(keyID);
      break;

    case 'Ctrl-U+00BB':  // Ctrl+'=' zoom in.
      if (!this.isEditing()) {
        this.viewport_.zoomIn();
        this.touchHandlers_.stopOperation();
        this.imageView_.applyViewportChange();
      }
      break;

    case 'Ctrl-U+00BD':  // Ctrl+'-' zoom out.
      if (!this.isEditing()) {
        this.viewport_.zoomOut();
        this.touchHandlers_.stopOperation();
        this.imageView_.applyViewportChange();
      }
      break;

    case 'Ctrl-U+0030': // Ctrl+'0' zoom reset.
      if (!this.isEditing()) {
        this.viewport_.setZoom(1.0);
        this.touchHandlers_.stopOperation();
        this.imageView_.applyViewportChange();
      }
      break;
  }

  return true;
};

/**
 * Resize handler.
 * @private
 */
SlideMode.prototype.onResize_ = function() {
  this.viewport_.setScreenSize(
      this.container_.clientWidth, this.container_.clientHeight);
  this.touchHandlers_.stopOperation();
  this.editor_.getBuffer().draw();
};

/**
 * Update thumbnails.
 */
SlideMode.prototype.updateThumbnails = function() {
  this.ribbon_.reset();
  if (this.active_)
    this.ribbon_.redraw();
};

// Saving

/**
 * Save the current image to a file.
 *
 * @param {!Gallery.Item} item Item to save the image.
 * @param {function()} callback Callback.
 * @private
 */
SlideMode.prototype.saveCurrentImage_ = function(item, callback) {
  this.showSpinner_(true);

  var savedPromise = this.dataModel_.saveItem(
      this.volumeManager_,
      item,
      this.imageView_.getCanvas(),
      this.shouldOverwriteOriginal_());

  savedPromise.then(function() {
    this.showSpinner_(false);
    this.flashSavedLabel_();

    // Allow changing the 'Overwrite original' setting only if the user
    // used Undo to restore the original image AND it is not a copy.
    // Otherwise lock the setting in its current state.
    var mayChangeOverwrite = !this.editor_.canUndo() && item.isOriginal() &&
        !FileType.isRaw(item.getEntry());
    ImageUtil.setAttribute(
        this.overwriteOriginalBox_, 'disabled', !mayChangeOverwrite);

    // Record UMA for the first edit.
    if (this.imageView_.getContentRevision() === 1)
      ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit'));

    callback();
  }.bind(this)).catch(function(error) {
    console.error(error.stack || error);

    this.showSpinner_(false);
    this.errorBanner_.show('GALLERY_SAVE_FAILED');

    callback();
  }.bind(this));
};

/**
 * Flash 'Saved' label briefly to indicate that the image has been saved.
 * @private
 */
SlideMode.prototype.flashSavedLabel_ = function() {
  var setLabelHighlighted =
      ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted');
  setTimeout(setLabelHighlighted.bind(null, true), 0);
  setTimeout(setLabelHighlighted.bind(null, false), 300);
};

/**
 * Local storage key for the 'Overwrite original' setting.
 * @type {string}
 */
SlideMode.OVERWRITE_KEY = 'gallery-overwrite-original';

/**
 * Local storage key for the number of times that
 * the overwrite info bubble has been displayed.
 * @type {string}
 */
SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble';

/**
 * Max number that the overwrite info bubble is shown.
 * @type {number}
 */
SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5;

/**
 * @return {boolean} True if 'Overwrite original' is set.
 * @private
 */
SlideMode.prototype.shouldOverwriteOriginal_ = function() {
  return this.overwriteOriginal_.checked;
};

/**
 * 'Overwrite original' checkbox handler.
 * @param {!Event} event Event.
 * @private
 */
SlideMode.prototype.onOverwriteOriginalClick_ = function(event) {
  var items = {};
  items[SlideMode.OVERWRITE_KEY] = event.target.checked;
  chrome.storage.local.set(items);
};

/**
 * Overwrite info bubble close handler.
 * @private
 */
SlideMode.prototype.onCloseBubble_ = function() {
  this.bubble_.hidden = true;
  var items = {};
  items[SlideMode.OVERWRITE_BUBBLE_KEY] =
      SlideMode.OVERWRITE_BUBBLE_MAX_TIMES;
  chrome.storage.local.set(items);
};

// Slideshow

/**
 * Slideshow interval in ms.
 */
SlideMode.SLIDESHOW_INTERVAL = 5000;

/**
 * First slideshow interval in ms. It should be shorter so that the user
 * is not guessing whether the button worked.
 */
SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000;

/**
 * Empirically determined duration of the fullscreen toggle animation.
 */
SlideMode.FULLSCREEN_TOGGLE_DELAY = 500;

/**
 * @return {boolean} True if the slideshow is on.
 * @private
 */
SlideMode.prototype.isSlideshowOn_ = function() {
  return this.container_.hasAttribute('slideshow');
};

/**
 * Starts the slideshow.
 * @param {number=} opt_interval First interval in ms.
 * @param {Event=} opt_event Event.
 */
SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) {
  // Reset zoom.
  this.viewport_.resetView();
  this.imageView_.applyViewportChange();

  // Disable touch operation.
  this.touchHandlers_.enabled = false;

  // Set the attribute early to prevent the toolbar from flashing when
  // the slideshow is being started from the mosaic view.
  this.container_.setAttribute('slideshow', 'playing');

  if (this.active_) {
    this.stopEditing_();
  } else {
    // We are in the Mosaic mode. Toggle the mode but remember to return.
    this.leaveAfterSlideshow_ = true;

    // Wait until the zoom animation from the mosaic mode is done.
    var startSlideshowAfterTransition = function() {
      setTimeout(function() {
        this.startSlideshow.call(this, SlideMode.SLIDESHOW_INTERVAL, opt_event);
      }.bind(this), ImageView.MODE_TRANSITION_DURATION);
    }.bind(this);
    this.toggleMode_(startSlideshowAfterTransition);
    return;
  }

  if (opt_event)  // Caused by user action, notify the Gallery.
    cr.dispatchSimpleEvent(this, 'useraction');

  this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow);
  if (!this.fullscreenBeforeSlideshow_) {
    this.toggleFullScreen_();
    opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) +
        SlideMode.FULLSCREEN_TOGGLE_DELAY;
  }

  this.resumeSlideshow_(opt_interval);
};

/**
 * Stops the slideshow.
 * @param {Event=} opt_event Event.
 * @private
 */
SlideMode.prototype.stopSlideshow_ = function(opt_event) {
  if (!this.isSlideshowOn_())
    return;

  if (opt_event)  // Caused by user action, notify the Gallery.
    cr.dispatchSimpleEvent(this, 'useraction');

  this.pauseSlideshow_();
  this.container_.removeAttribute('slideshow');

  // Do not restore fullscreen if we exited fullscreen while in slideshow.
  var fullscreen = util.isFullScreen(this.context_.appWindow);
  var toggleModeDelay = 0;
  if (!this.fullscreenBeforeSlideshow_ && fullscreen) {
    this.toggleFullScreen_();
    toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY;
  }
  if (this.leaveAfterSlideshow_) {
    this.leaveAfterSlideshow_ = false;
    setTimeout(this.toggleMode_.bind(this), toggleModeDelay);
  }

  // Re-enable touch operation.
  this.touchHandlers_.enabled = true;
};

/**
 * @return {boolean} True if the slideshow is playing (not paused).
 * @private
 */
SlideMode.prototype.isSlideshowPlaying_ = function() {
  return this.container_.getAttribute('slideshow') === 'playing';
};

/**
 * Pauses/resumes the slideshow.
 * @private
 */
SlideMode.prototype.toggleSlideshowPause_ = function() {
  cr.dispatchSimpleEvent(this, 'useraction');  // Show the tools.
  if (this.isSlideshowPlaying_()) {
    this.pauseSlideshow_();
  } else {
    this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST);
  }
};

/**
 * @param {number=} opt_interval Slideshow interval in ms.
 * @private
 */
SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) {
  console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state');

  if (this.slideShowTimeout_)
    clearTimeout(this.slideShowTimeout_);

  this.slideShowTimeout_ = setTimeout(function() {
    this.slideShowTimeout_ = null;
    this.selectNext(1);
  }.bind(this), opt_interval || SlideMode.SLIDESHOW_INTERVAL);
};

/**
 * Resumes the slideshow.
 * @param {number=} opt_interval Slideshow interval in ms.
 * @private
 */
SlideMode.prototype.resumeSlideshow_ = function(opt_interval) {
  this.container_.setAttribute('slideshow', 'playing');
  this.scheduleNextSlide_(opt_interval);
};

/**
 * Pauses the slideshow.
 * @private
 */
SlideMode.prototype.pauseSlideshow_ = function() {
  this.container_.setAttribute('slideshow', 'paused');
  if (this.slideShowTimeout_) {
    clearTimeout(this.slideShowTimeout_);
    this.slideShowTimeout_ = null;
  }
};

/**
 * @return {boolean} True if the editor is active.
 */
SlideMode.prototype.isEditing = function() {
  return this.container_.hasAttribute('editing');
};

/**
 * Stops editing.
 * @private
 */
SlideMode.prototype.stopEditing_ = function() {
  if (this.isEditing())
    this.toggleEditor();
};

/**
 * Activate/deactivate editor.
 * @param {Event=} opt_event Event.
 */
SlideMode.prototype.toggleEditor = function(opt_event) {
  if (opt_event)  // Caused by user action, notify the Gallery.
    cr.dispatchSimpleEvent(this, 'useraction');

  if (!this.active_) {
    this.toggleMode_(this.toggleEditor.bind(this));
    return;
  }

  this.stopSlideshow_();

  ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing());

  if (this.isEditing()) { // isEditing has just been flipped to a new value.
    // Reset zoom.
    this.viewport_.resetView();
    this.imageView_.applyViewportChange();
    if (this.context_.readonlyDirName) {
      this.editor_.getPrompt().showAt(
          'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName);
    }
    this.touchHandlers_.enabled = false;
  } else {
    this.editor_.getPrompt().hide();
    this.editor_.leaveModeGently();
    this.touchHandlers_.enabled = true;
  }
};

/**
 * Prints the current item.
 * @private
 */
SlideMode.prototype.print_ = function() {
  cr.dispatchSimpleEvent(this, 'useraction');
  window.print();
};

/**
 * Shows/hides the busy spinner.
 *
 * @param {boolean} on True if show, false if hide.
 * @private
 */
SlideMode.prototype.showSpinner_ = function(on) {
  if (this.spinnerTimer_) {
    clearTimeout(this.spinnerTimer_);
    this.spinnerTimer_ = null;
  }

  if (on) {
    this.spinnerTimer_ = setTimeout(function() {
      this.spinnerTimer_ = null;
      ImageUtil.setAttribute(this.container_, 'spinner', true);
    }.bind(this), 1000);
  } else {
    ImageUtil.setAttribute(this.container_, 'spinner', false);
  }
};

/**
 * Apply the change of viewport.
 */
SlideMode.prototype.applyViewportChange = function() {
  this.imageView_.applyViewportChange();
};

/**
 * Touch handlers of the slide mode.
 * @param {!Element} targetElement Event source.
 * @param {!SlideMode} slideMode Slide mode to be operated by the handler.
 * @struct
 * @constructor
 */
function TouchHandler(targetElement, slideMode) {
  /**
   * Event source.
   * @type {!Element}
   * @private
   * @const
   */
  this.targetElement_ = targetElement;

  /**
   * Target of touch operations.
   * @type {!SlideMode}
   * @private
   * @const
   */
  this.slideMode_ = slideMode;

  /**
   * Flag to enable/disable touch operation.
   * @type {boolean}
   * @private
   */
  this.enabled_ = true;

  /**
   * Whether it is in a touch operation that is started from targetElement or
   * not.
   * @type {boolean}
   * @private
   */
  this.touchStarted_ = false;

  /**
   * The swipe action that should happen only once in an operation is already
   * done or not.
   * @type {boolean}
   * @private
   */
  this.done_ = false;

  /**
   * Event on beginning of the current gesture.
   * The variable is updated when the number of touch finger changed.
   * @type {TouchEvent}
   * @private
   */
  this.gestureStartEvent_ = null;

  /**
   * Rotation value on beginning of the current gesture.
   * @type {number}
   * @private
   */
  this.gestureStartRotation_ = 0;

  /**
   * Last touch event.
   * @type {TouchEvent}
   * @private
   */
  this.lastEvent_ = null;

  /**
   * Zoom value just after last touch event.
   * @type {number}
   * @private
   */
  this.lastZoom_ = 1.0;

  targetElement.addEventListener('touchstart', this.onTouchStart_.bind(this));
  var onTouchEventBound = this.onTouchEvent_.bind(this);
  targetElement.ownerDocument.addEventListener('touchmove', onTouchEventBound);
  targetElement.ownerDocument.addEventListener('touchend', onTouchEventBound);

  targetElement.addEventListener('mousewheel', this.onMouseWheel_.bind(this));
}

/**
 * If the user touched the image and moved the finger more than SWIPE_THRESHOLD
 * horizontally it's considered as a swipe gesture (change the current image).
 * @type {number}
 * @const
 */
TouchHandler.SWIPE_THRESHOLD = 100;

/**
 * Rotation threshold in degrees.
 * @type {number}
 * @const
 */
TouchHandler.ROTATION_THRESHOLD = 25;

/**
 * Obtains distance between fingers.
 * @param {!TouchEvent} event Touch event. It should include more than two
 *     touches.
 * @return {number} Distance between touch[0] and touch[1].
 */
TouchHandler.getDistance = function(event) {
  var touch1 = event.touches[0];
  var touch2 = event.touches[1];
  var dx = touch1.clientX - touch2.clientX;
  var dy = touch1.clientY - touch2.clientY;
  return Math.sqrt(dx * dx + dy * dy);
};

/**
 * Obtains the degrees of the pinch twist angle.
 * @param {!TouchEvent} event1 Start touch event. It should include more than
 *     two touches.
 * @param {!TouchEvent} event2 Current touch event. It should include more than
 *     two touches.
 * @return {number} Degrees of the pinch twist angle.
 */
TouchHandler.getTwistAngle = function(event1, event2) {
  var dx1 = event1.touches[1].clientX - event1.touches[0].clientX;
  var dy1 = event1.touches[1].clientY - event1.touches[0].clientY;
  var dx2 = event2.touches[1].clientX - event2.touches[0].clientX;
  var dy2 = event2.touches[1].clientY - event2.touches[0].clientY;
  var innerProduct = dx1 * dx2 + dy1 * dy2;  // |v1| * |v2| * cos(t) = x / r
  var outerProduct = dx1 * dy2 - dy1 * dx2;  // |v1| * |v2| * sin(t) = y / r
  return Math.atan2(outerProduct, innerProduct) * 180 / Math.PI;  // atan(y / x)
};

TouchHandler.prototype = /** @struct */ {
  /**
   * @param {boolean} flag New value.
   */
  set enabled(flag) {
    this.enabled_ = flag;
    if (!this.enabled_)
      this.stopOperation();
  }
};

/**
 * Stops the current touch operation.
 */
TouchHandler.prototype.stopOperation = function() {
  this.touchStarted_ = false;
  this.done_ = false;
  this.gestureStartEvent_ = null;
  this.lastEvent_ = null;
  this.lastZoom_ = 1.0;
};

/**
 * Handles touch start events.
 * @param {!Event} event Touch event.
 * @private
 */
TouchHandler.prototype.onTouchStart_ = function(event) {
  event = assertInstanceof(event, TouchEvent);
  if (this.enabled_ && event.touches.length === 1)
    this.touchStarted_ = true;
};

/**
 * Handles touch move and touch end events.
 * @param {!Event} event Touch event.
 * @private
 */
TouchHandler.prototype.onTouchEvent_ = function(event) {
  event = assertInstanceof(event, TouchEvent);
  // Check if the current touch operation started from the target element or
  // not.
  if (!this.touchStarted_)
    return;

  // Check if the current touch operation ends with the event.
  if (event.touches.length === 0) {
    this.stopOperation();
    return;
  }

  // Check if a new gesture started or not.
  var viewport = this.slideMode_.getViewport();
  if (!this.lastEvent_ ||
      this.lastEvent_.touches.length !== event.touches.length) {
    if (event.touches.length === 2 ||
        event.touches.length === 1) {
      this.gestureStartEvent_ = event;
      this.gestureStartRotation_ = viewport.getRotation();
      this.lastEvent_ = event;
      this.lastZoom_ = viewport.getZoom();
    } else {
      this.gestureStartEvent_ = null;
      this.gestureStartRotation_ = 0;
      this.lastEvent_ = null;
      this.lastZoom_ = 1.0;
    }
    return;
  }

  // Handle the gesture movement.
  switch (event.touches.length) {
    case 1:
      if (viewport.isZoomed()) {
        // Scrolling an image by swipe.
        var dx = event.touches[0].screenX - this.lastEvent_.touches[0].screenX;
        var dy = event.touches[0].screenY - this.lastEvent_.touches[0].screenY;
        viewport.setOffset(
            viewport.getOffsetX() + dx, viewport.getOffsetY() + dy);
        this.slideMode_.applyViewportChange();
      } else {
        // Traversing images by swipe.
        if (this.done_)
          break;
        var dx =
            event.touches[0].clientX -
            this.gestureStartEvent_.touches[0].clientX;
        if (dx > TouchHandler.SWIPE_THRESHOLD) {
          this.slideMode_.advanceManually(-1);
          this.done_ = true;
        } else if (dx < -TouchHandler.SWIPE_THRESHOLD) {
          this.slideMode_.advanceManually(1);
          this.done_ = true;
        }
      }
      break;

    case 2:
      // Pinch zoom.
      var distance1 = TouchHandler.getDistance(this.lastEvent_);
      var distance2 = TouchHandler.getDistance(event);
      if (distance1 === 0)
        break;
      var zoom = distance2 / distance1 * this.lastZoom_;
      viewport.setZoom(zoom);

      // Pinch rotation.
      assert(this.gestureStartEvent_);
      var angle = TouchHandler.getTwistAngle(this.gestureStartEvent_, event);
      if (angle > TouchHandler.ROTATION_THRESHOLD)
        viewport.setRotation(this.gestureStartRotation_ + 1);
      else if (angle < -TouchHandler.ROTATION_THRESHOLD)
        viewport.setRotation(this.gestureStartRotation_ - 1);
      else
        viewport.setRotation(this.gestureStartRotation_);
      this.slideMode_.applyViewportChange();
      break;
  }

  // Update the last event.
  this.lastEvent_ = event;
  this.lastZoom_ = viewport.getZoom();
};

/**
 * Handles mouse wheel events.
 * @param {!Event} event Wheel event.
 * @private
 */
TouchHandler.prototype.onMouseWheel_ = function(event) {
  var event = assertInstanceof(event, MouseEvent);
  var viewport = this.slideMode_.getViewport();
  if (!this.enabled_ || !viewport.isZoomed())
    return;
  this.stopOperation();
  viewport.setOffset(
      viewport.getOffsetX() + event.wheelDeltaX,
      viewport.getOffsetY() + event.wheelDeltaY);
  this.slideMode_.applyViewportChange();
};


text:
<!DOCTYPE html>

    <meta charset="utf-8">
    <title>Example: Attribute Based Speed Dating</title>
    <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=PT+Sans:400,700,400italic,700italic">
    <link rel="stylesheet" href="../../build/cssgrids/cssgrids-min.css">
    <link rel="stylesheet" href="../assets/css/main.css">
    <link rel="stylesheet" href="../assets/vendor/prettify/prettify-min.css">
    <link rel="shortcut icon" type="image/png" href="../assets/favicon.png">

Example: Attribute Based Speed Dating

        This example builds on the "Basic Configuration" example,
        showing how you can use attribute to model objects in your application.

        As with the basic example, it is geared towards users who want to create their own classes from
        scratch and add attribute support. In most cases you should consider extending the
        Base class when you need attribute support, instead
        of augmenting Attribute directly. Base does the work described
        in this example for you, in addition to making it easier for users to extend you class.

Speed Dating With Attributes

        <button type="button" class="hi">Hi I'm John</button>
        <button type="button" class="taken" disabled="disabled">I like Jane</button>

        <button type="button" disabled="disabled" class="hi">Hey, I'm Jane</button>
        <button type="button" class="upgrade" disabled="disabled">No way!, I save whales too!</button>
        <button type="button" class="taken" disabled="disabled">I like John</button>

Setting Up a SpeedDater Class

In this example, we'll create a custom SpeedDater class, and show how you can use attributes to manage the state for a Speed Dater.
In the "Attribute Event Based Speed Dating" example we'll modify this example to leverage attribute change events.

Creating A YUI Instance

As with the other attribute examples, we'll setup our own instance of the YUI object and request the modules we require using the code pattern shown below:

<script type="text/javascript">

    // Create our local YUI instance, to avoid
    // modifying the global YUI object

    YUI({...}).use("attribute", "node", ... function(Y) {

        // Example code is written inside this function,
        // which gets passed our own YUI instance, Y, populated
        // with the modules we asked for (e.g. Y.Attribute, Y.Node etc.)

    });
</script>

Defining The SpeedDater Class

The first step in the example is to create the constructor function for our new class, to which we want to add attribute support. In our example, this class is called SpeedDater.
We then augment SpeedDater with Y.Attribute, so that it receives all of Attribute's methods, in addition to any it may defined itself:

// Setup custom class which we want to add attribute support to
function SpeedDater(cfg) {
    ...
}

// Augment custom class with Attribute
Y.augment(SpeedDater, Y.Attribute);

Adding Attributes

We can now set up any attributes we need for SpeedDater using Attribute's addAttrs() method.

For this example we add 3 attributes - name, personality, and available.

We provide an default initial value for personality and available, but don't have anything for name.

As mentioned in the basic example, the same object literal we use to provide the initial value for the attribute can also be used to configure attribute properties such as readOnly or
writeOnce, and to define getter, setter and validator methods for the attribute. For name, we configure it to be writeOnce,
meaning that it's value can be set once by the user, but not modified after that single set.

The default set of attributes which SpeedDater will support is passed to addAttrs to set up the attributes for each instance during construction.

As mentioned previously, if you expect your class to be extended, Base provides a more convenient way for you to define the same attribute configuration statically for your class, so that it can be modified by extended classes.
Base will take care of isolating the static configuration, so that it isn't modified across instances.

The complete definition for SpeedDater is shown below:

// Setup custom class which we want to
// add managed attribute support to

function SpeedDater(cfg) {

    // When constructed, setup the initial attributes for the
    // instance, by calling the addAttrs method.

    var attrs = {
        // Add 3 attributes: name, personality, available
        name : {
            writeOnce:true
        },

        personality : {
            value:50
        },

        available : {
            value:true
        }
    };

    this.addAttrs(attrs, cfg);
}

SpeedDater.prototype.applyNameTag = function(where) {
    // Method used to render the visual representation of a
    // SpeedDater object's state (in this case as a name tag)
};

SpeedDater.prototype.updateNameTag = function() {
    // Method used to update the rendered state of SpeedDater in the DOM.
}

// Template to use form the markup
SpeedDater.NAMETAG = "<div class="sd-nametag"><div class="sd-hd">Hello!</div>... </div>";

// Augment custom class with Attribute
Y.augment(SpeedDater, Y.Attribute);

The addAttrs() method, in addition to the default attribute configuration, also accepts an object literal (associative array) of name/value pairs which can be
used to over-ride the default initial values of the attributes. This is useful for classes which wish to allow the user the set the value of attributes as part of object
construction, as shown by the use of the cfg argument above.

Using Attributes

Now that we have SpeedDater defined with the set of attributes it supports, we can create multiple instances of SpeedDater defining the initial
attribute state for each instance through the constructor. We can also update the instance's attribute state after construction, using the get and
set methods defined by Attribute.

We create a first instance, john, setting up the intial state using Attribute's constructor configuration object support:

// Set both name and personality during construction
john = new SpeedDater({
    name: "John",
    personality: 76.43
});

For the second instance that we create, jane, we set the value of the personality attribute, after construction:

// Set name during construction
jane = new SpeedDater({
    name: "Jane"
});

// Set personality after construction. The initial value for personality
// in this case, will be the value defined when the attribute was added
// using addAttrs (above)
jane.set("personality", 82);

We render the current attribute state of each instance to the DOM, using the applyNameTag() method defined on SpeedDater's prototype:

// Render the sticker with john's state information to the DOM
john.applyNameTag("#john .shirt");

// Render the sticker with jane's state information to the DOM
jane.applySicker("#jane .shirt");

Although not directly related to working with Attributes, it's worth taking a look at the applyNameTag() and updateNameTag() implementations, since they establish
a commonly used pattern.

The applyNameTag() method handles rendering the initial visual representation for a speed dater object's state (in this case a name tag). It uses tokens in an HTML "template" string, which it replaces with the values
of attributes using the Y.Lang.sub() utility method:

// A template for the markup representing the SpeedDater object..
SpeedDater.NAMETAG = '<div class="sd-nametag"> \
                        <div class="sd-hd">Hello!</div> \
                        <div class="sd-bd">I\'m <span class="sd-name">{name}</span> \
                        and my PersonalityQuotientIndex is \
                        <span class="sd-personality">{personality}</span> \
                        </div> \
                        <div class="sd-ft sd-availability">{available}</div> \
                     </div>';

// A rendering method, used to create the initial markup for the SpeedDater.
SpeedDater.prototype.applyNameTag = function(where) {

    // This example uses an HTML template string containing placeholder
    // tokens (SpeedDater.NAMETAG above), and Y.Lang.sub() to replace the
    // tokens with the current attribute values.

    var tokens = {
        // Get attribute values and map them to the tokens in the HTML template string
        name: this.get("name"),
        available: (this.get("available")) ? "I'm still looking " : "Sorry, I'm taken",
        personality: this.get("personality")
    };

    // Create a new Node element, from the token substituted string...
    this.nameTag = Y.Node.create(Y.Lang.sub(SpeedDater.NAMETAG, tokens));

    // ... and append it to the DOM
    Y.one(where).appendChild(this.nameTag);
};

The updateNameTag() method handles updating this visual representation with the current state, when requested by the user

// An update method, used to refresh the rendered content, after
// an attribute value is changed.
SpeedDater.prototype.updateNameTag = function() {

    // Get current attribute values...
    var taken = (this.get("available")) ? "I'm still looking " : "Sorry, I'm taken";
    var name = this.get("name");
    var personality = this.get("personality");

    // Find the corresponding element, and replace the innerHTML with the new value
    this.nameTag.one(".sd-name").set("innerHTML", name);
    this.nameTag.one(".sd-availability").set("innerHTML", taken);

    var personalityEl = this.nameTag.one(".sd-personality");
    personalityEl.set("innerHTML", personality);

    if (personality > 90) {
        personalityEl.addClass("sd-max");
    }
}

Each instance's state can be now be updated using Attribute's set method, and the subsequent call to SpeedDater's updateNameTag() method will update the visual representation (the rendered DOM) of the object:

Y.on("click", function() {

    john.set("available", false);
    john.updateNameTag();

}, "#john .taken");

Y.on("click", function() {

    jane.set("available", false);
    jane.updateNameTag();

}, "#jane .taken");

Y.on("click", function() {

    jane.set("personality", 98);
    jane.updateNameTag();

}, "#jane .upgrade");

In the "Attribute Event Based Speed Dating" example, we'll see how we can use Attribute change events to eliminate the need for users to call updateNameTag() each time they set an attribute, and have the two instances communicate with one another.

Complete Example Source

<div id="speeddate">

    <h1>Speed Dating With Attributes</h1>

    <div id="john">
        <button type="button" class="hi">Hi I'm John</button>
        <button type="button" class="taken" disabled="disabled">I like Jane</button>
        <div class="shirt"></div>
    </div>

    <div id="jane">
        <button type="button" disabled="disabled" class="hi">Hey, I'm Jane</button>
        <button type="button" class="upgrade" disabled="disabled">No way!, I save whales too!</button>
        <button type="button" class="taken" disabled="disabled">I like John</button>
        <div class="shirt"></div>
    </div>
</div>

<script type="text/javascript">

// Get a new instance of YUI and
// load it with the required set of modules

YUI().use("node", "attribute", function(Y) {

    // Setup custom class which we want to
    // add managed attribute support to

    function SpeedDater(cfg) {

        // When constructed, setup the initial attributes for the
        // instance, by calling the addAttrs method.

        var attrs = {
            // Add 3 attributes: name, personality, available
            name : {
                writeOnce:true
            },

            personality : {
                value:50
            },

            available : {
                value:true
            }
        };

        this.addAttrs(attrs, cfg);
    }

    // Setup static property to hold attribute config

    SpeedDater.NAMETAG = '<div class="sd-nametag"><div class="sd-hd">Hello!</div><div class="sd-bd">I\'m <span class="sd-name">{name}</span> and my PersonalityQuotientIndex is <span class="sd-personality">{personality}</span></div><div class="sd-ft sd-availability">{available}</div></div>';

    SpeedDater.prototype.applyNameTag = function(where) {

        var tokens = {
            name: this.get("name"),
            available: (this.get("available")) ? "I'm still looking" : "Sorry, I'm taken",
            personality: this.get("personality")
        };

        var str = Y.Lang.sub(SpeedDater.NAMETAG, tokens);
        this.nameTag = Y.Node.create(str);
        Y.one(where).appendChild(this.nameTag);
    };

    SpeedDater.prototype.updateNameTag = function() {

        var taken = (this.get("available")) ? "I'm still looking" : "Sorry, I'm taken";
        var name = this.get("name");
        var personality = this.get("personality");

        this.nameTag.one(".sd-name").set("innerHTML", name);
        this.nameTag.one(".sd-availability").set("innerHTML", taken);

        var personalityEl = this.nameTag.one(".sd-personality");
        personalityEl.set("innerHTML", personality);

        if (personality > 90) {
            personalityEl.addClass("sd-max");
        }
    }

    // Augment custom class with Attribute
    Y.augment(SpeedDater, Y.Attribute);

    var john, jane;

    Y.on("click", function() {

        if (!john) {

            // Set both name and personality during construction
            john = new SpeedDater({
                name: "John",
                personality: 76.43
            });
            john.applyNameTag("#john .shirt");

            Y.one("#jane .hi").set("disabled", false);
        }

    }, "#john .hi");

    Y.on("click", function() {

        if (!jane) {

            // Set name during construction
            jane = new SpeedDater({
                name: "Jane"
            });

            // Set personality after construction
            jane.set("personality", 82);

            jane.applyNameTag("#jane .shirt");

            Y.all("#jane button").set("disabled", false);
            Y.all("#john button").set("disabled", false);
        }

    }, "#jane .hi");

    Y.on("click", function() {

        john.set("available", false);
        john.updateNameTag();

    }, "#john .taken");

    Y.on("click", function() {

        jane.set("available", false);
        jane.updateNameTag();

    }, "#jane .taken");

    Y.on("click", function() {

        jane.set("personality", 98);
        jane.updateNameTag();

    }, "#jane .upgrade");

 });
</script>

Examples

                                            Basic Attribute Configuration

                                            Read-Only and Write-Once Attributes

                                            Attribute Change Events

                                            Attribute Based Speed Dating

                                            Attribute Event Based Speed Dating

                                            Attribute Getters, Setters and Validators

redpajama_set_name: RedPajamaGithub


text:
<?php

namespace Oro\Bundle\TagBundle\Tests\Unit\Form\Type;

use Oro\Bundle\TagBundle\Form\Type\TagType;

class TagTypeTest extends \PHPUnit_Framework_TestCase
{

    /**
     * @var TagType
     */
    protected $type;

    protected function setUp()
    {
        $this->type = new TagType();
    }

    public function tearDown()
    {
        unset($this->type);
    }

    public function testSetDefaultOptions()
    {
        $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface');
        $resolver->expects($this->once())
            ->method('setDefaults')
            ->with($this->isType('array'));

        $this->type->setDefaultOptions($resolver);
    }

    public function testGetName()
    {
        $this->assertEquals('oro_tag_tag', $this->type->getName());
    }

    public function testBuildForm()
    {
        $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder')
            ->disableOriginalConstructor()
            ->getMock();
        $builder->expects($this->once())
            ->method('add')
            ->will($this->returnSelf());

        $this->type->buildForm($builder, array());
    }
}


text:
/**
 * Interchange module.
 * @module foundation.interchange
 * @requires foundation.util.mediaQuery
 * @requires foundation.util.timerAndImageLoader
 */
!function(Foundation, $) {
  'use strict';

  /**
   * Creates a new instance of Interchange.
   * @class
   * @fires Interchange#init
   * @param {Object} element - jQuery object to add the trigger to.
   * @param {Object} options - Overrides to the default plugin settings.
   */
  function Interchange(element, options) {
    this.$element = element;
    this.options = $.extend({}, Interchange.defaults, options);
    this.rules = [];
    this.currentPath = '';

    this._init();
    this._events();

    Foundation.registerPlugin(this, 'Interchange');
  }

  /**
   * Default settings for plugin
   */
  Interchange.defaults = {
    /**
     * Rules to be applied to Interchange elements. Set with the `data-interchange` array notation.
     * @option
     */
    rules: null
  };

  Interchange.SPECIAL_QUERIES = {
    'landscape': 'screen and (orientation: landscape)',
    'portrait': 'screen and (orientation: portrait)',
    'retina': 'only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx)'
  };

  /**
   * Initializes the Interchange plugin and calls functions to get interchange functioning on load.
   * @function
   * @private
   */
  Interchange.prototype._init = function() {
    this._addBreakpoints();
    this._generateRules();
    this._reflow();
  };

  /**
   * Initializes events for Interchange.
   * @function
   * @private
   */
  Interchange.prototype._events = function() {
    $(window).on('resize.fndtn.interchange', Foundation.util.throttle(this._reflow.bind(this), 50));
  };

  /**
   * Calls necessary functions to update Interchange upon DOM change
   * @function
   * @private
   */
  Interchange.prototype._reflow = function() {
    var match;

    // Iterate through each rule, but only save the last match
    for (var i in this.rules) {
      var rule = this.rules[i];

      if (window.matchMedia(rule.query).matches) {
        match = rule;
      }
    }

    if (match) {
      this.replace(match.path);
    }
  };

  /**
   * Gets the Foundation breakpoints and adds them to the Interchange.SPECIAL_QUERIES object.
   * @function
   * @private
   */
  Interchange.prototype._addBreakpoints = function() {
    for (var i in Foundation.MediaQuery.queries) {
      var query = Foundation.MediaQuery.queries[i];
      Interchange.SPECIAL_QUERIES[query.name] = query.value;
    }
  };

  /**
   * Checks the Interchange element for the provided media query + content pairings
   * @function
   * @private
   * @param {Object} element - jQuery object that is an Interchange instance
   * @returns {Array} scenarios - Array of objects that have 'mq' and 'path' keys with corresponding keys
   */
  Interchange.prototype._generateRules = function() {
    var rulesList = [];
    var rules;

    if (this.options.rules) {
      rules = this.options.rules;
    }
    else {
      rules = this.$element.data('interchange').match(/\[.*?\]/g);
    }

    for (var i in rules) {
      var rule = rules[i].slice(1, -1).split(', ');
      var path = rule.slice(0, -1).join('');
      var query = rule[rule.length - 1];

      if (Interchange.SPECIAL_QUERIES[query]) {
        query = Interchange.SPECIAL_QUERIES[query];
      }

      rulesList.push({
        path: path,
        query: query
      });
    }

    this.rules = rulesList;
  };

  /**
   * Update the `src` property of an image, or change the HTML of a container, to the specified path.
   * @function
   * @param {String} path - Path to the image or HTML partial.
   * @fires Interchange#replaced
   */
  Interchange.prototype.replace = function(path) {
    if (this.currentPath === path) return;

    var _this = this;

    // Replacing images
    if (this.$element[0].nodeName === 'IMG') {
      this.$element.attr('src', path).load(function() {
        _this.$element.trigger('replaced.zf.interchange');
        _this.currentPath = path;
      });
    }
    // Replacing background images
    else if (path.match(/\.(gif|jpg|jpeg|tiff|png)([?#].*)?/i)) {
      this.$element.css({ 'background-image': 'url('+path+')' });
    }
    // Replacing HTML
    else {
      $.get(path, function(response) {
        _this.$element.html(response);
        $(response).foundation();
        _this.$element.trigger('replaced.zf.interchange');
        _this.currentPath = path;
      });
    }
  };
  /**
   * Destroys an instance of interchange.
   * @function
   */
  Interchange.prototype.destroy = function(){
    //TODO this.
  };
  Foundation.plugin(Interchange, 'Interchange');

  // Exports for AMD/Browserify
  if (typeof module !== 'undefined' && typeof module.exports !== 'undefined')
    module.exports = Interchange;
  if (typeof define === 'function')
    define(['foundation'], function() {
      return Interchange;
    });

}(Foundation, jQuery);


text:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

using WpfCommonds;
using FelicaMail;

namespace FelicaCashingSystemV2.Views
{
    class MailTemplate
    {
        public MailTemplateType Id { get; set; }
        public string Name { get; set; }
    }

    enum MailTemplateType
    {
        PaymentRequest,
        Message
    }

    class AdministeringMailViewModel : AdministeringUserViewModel
    {
        public AdministeringMailViewModel()
        {
            this.Templates.Add(new MailTemplate
            {
                Id = MailTemplateType.PaymentRequest,
                Name = "支払い要求"
            });

            this.Templates.Add(new MailTemplate
            {
                Id = MailTemplateType.Message,
                Name = "任意のメッセージ"
            });

            this.SendCommand = new DelegateCommand(this.Send);
        }

        private ObservableCollection<MailTemplate> templates = new ObservableCollection<MailTemplate>();
        public ObservableCollection<MailTemplate> Templates
        {
            get { return this.templates; }
            set
            {
                this.templates = value;
                this.OnPropertyChanged("Templates");
            }
        }

        private MailTemplateType templateId = MailTemplateType.PaymentRequest;
        public MailTemplateType TemplateId
        {
            get {
                return this.templateId;
            }
            set
            {
                this.templateId = value;
                this.OnPropertyChanged("TemplateId");
                this.OnPropertyChanged("IsMessageRequired");
            }
        }

        public bool IsMessageRequired
        {
            get
            {
                return this.TemplateId == MailTemplateType.Message;
            }
        }

        private string message = "";
        public string Message
        {
            get { return this.message; }
            set
            {
                this.message = value;
                this.OnPropertyChanged("Message");
            }
        }

        private string errorMessage = "";
        public string ErrorMessage
        {
            get { return this.errorMessage; }
            set
            {
                this.errorMessage = value;
                this.OnPropertyChanged("ErrorMessage");
            }
        }

        public ICommand SendCommand { set; get; }
        private void Send()
        {
            Debug.WriteLine("Send mail start");

            this.ErrorMessage = string.Empty;

            var email = this.AdministeringUser.Email;
            var name = this.AdministeringUser.Name;
            var adminName = App.Current.User.Name;

            switch (this.TemplateId)
            {
                case MailTemplateType.Message:
                    App.Current.Mailer.SendMessage(
                        email,
                        new MessageArgs
                        {
                            Name = name,
                            AdminName = adminName,
                            Message = this.Message,
                        });
                    break;

                case MailTemplateType.PaymentRequest:
                    var money = this.AdministeringUser.Money;

                    if (money >= 0)
                    {
                        this.ErrorMessage = "利用者の金額がマイナスではありません。";
                        return;
                    }

                    App.Current.Mailer.SendPaymentRequest(
                        email,
                        new PaymentRequestArgs
                        {
                            Name = name,
                            AdminName = adminName,
                            Money = money.ToCommaString(),
                        });
                    break;
            }

            this.ShowMessageBox("ユーザーにメールを送信しました。", "メール送信に成功");
            Debug.WriteLine("Send mail succeeded");
        }
    }
}


text:
<?xml version="1.0" encoding="UTF-8"?>
<ldswebml type="image" uri="/media-library/images/temples/billings-montana#billings-temple-lds-932700" xml:lang="eng" locale="eng" status="publish" xmlns:its="http://www.w3.org/2005/11/its">
  <search-meta>
      <uri-title its:translate="no">billings-temple-lds-932700</uri-title>
      <source>billings-temple-lds-932700</source>
      <publication-type its:translate="no">image</publication-type>
      <media-type its:translate="no">image</media-type>
      <publication value="ldsorg">LDS.org</publication>
      <publication-date value="2012-12-14">2012-12-14</publication-date>
      <title/>
      <description/>
      <collections>
          <collection its:translate="no">global-search</collection>
          <collection its:translate="no">media</collection>
          <collection its:translate="no">images</collection>
      </collections>
      <subjects>
          <subject>Billings Montana Temple</subject>
          <subject>_x000D_
Temples</subject>
      </subjects>
  </search-meta>
  <no-search>
      <path>http://media.ldscdn.org/images/media-library/temples/billings-montana/billings-temple-lds-932700-gallery.jpg</path>
      <images>
          http://media.ldscdn.org/images/media-library/temples/billings-montana/billings-temple-lds-932700-thumbnail.jpg
      </images>
          <download-urls>
              <download-url>
                  Mobile
                  <size>192 KB</size>
                  <path>http://media.ldscdn.org/images/media-library/temples/billings-montana/billings-temple-lds-932700-mobile.jpg</path>
              </download-url>
              <download-url>
                  Tablet
                  <size>273 KB</size>
                  <path>http://media.ldscdn.org/images/media-library/temples/billings-montana/billings-temple-lds-932700-tablet.jpg</path>
              </download-url>
              <download-url>
                  Print
                  <size>623 KB</size>
                  <path>http://media.ldscdn.org/images/media-library/temples/billings-montana/billings-temple-lds-932700-print.jpg</path>
              </download-url>
              <download-url>
                  Wallpaper
                  <size>1,013 KB</size>
                  <path>http://media.ldscdn.org/images/media-library/temples/billings-montana/billings-temple-lds-932700-wallpaper.jpg</path>
              </download-url>
          </download-urls>
  </no-search>
</ldswebml>


text:
Game engine toolkit for Go.

**Status**: Alpha. (No stability guarantee)

Some good stuff in here, but fairly specific to my own projects at the moment.
It may generalize more over time. Largely factored out of
[shazow/linerage3d](https://github.com/shazow/linerage3d).

Some things that already work well:

- Cameras (Quat and Euler based, with lerping)
- Shader managing (with reloading support)
- Scene managing (basic for now, need to flesh it out into a tree-based thing)
- Control key binding

## Goals

- Multiple rendering backends
  - [x] Support golang.com/x/mobile backend
  - [ ] Support additional backends (glfw)

- More non-rendering things, maybe?
- More docs, examples.

## License

MIT


text:
package com.thaiopensource.xml.sax;

import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

/**
 * A factory for XMLReaders.  Thread-safety is determined by each particular
 * implementation of this interface.
 *
 * @author James Clark
 */
public interface XMLReaderCreator {
  /**
   * Creates a new XMLReader.
   *
   * @return a new XMLReader; never null
   * @throws org.xml.sax.SAXException If an XMLReader cannot be created for any reason
   */
  XMLReader createXMLReader() throws SAXException;
}