diff options
| author | Stephen Blott | 2015-01-22 11:39:21 +0000 | 
|---|---|---|
| committer | Stephen Blott | 2015-01-22 14:15:43 +0000 | 
| commit | d7b416747e5cf6971737a3f70243618419a1ac4b (patch) | |
| tree | 2e7aae49e1f345768ceb27eb2afe9ba677992f37 | |
| parent | dd9b2e9550a48c8e6e7a4a56b530ac8060279b12 (diff) | |
| download | vimium-d7b416747e5cf6971737a3f70243618419a1ac4b.tar.bz2 | |
Visual/edit modes: further development.
- Better abstraction.
- Add HUD message on yank.
- Require initial selection for visual mode.
- Try to start with a visible selection.
- Scroll the active end of the selection into view (with smooth
  scrolling, if enabled).
| -rw-r--r-- | content_scripts/mode_visual_edit.coffee | 153 | ||||
| -rw-r--r-- | content_scripts/scroller.coffee | 14 | ||||
| -rw-r--r-- | lib/keyboard_utils.coffee | 7 | 
3 files changed, 117 insertions, 57 deletions
| diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index cc0f0bf6..a5d8ff45 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -8,9 +8,9 @@ class SuppressPrintable extends Mode        if KeyboardUtils.isPrintable event          if event.type == "keydown"            DomUtils.suppressPropagation -          @stopBubblingAndTrue +          @stopBubblingAndFalse          else -          @suppressEvent +          false        else          @stopBubblingAndTrue @@ -25,7 +25,7 @@ class SuppressPrintable extends Mode      super options      @onExit => handlerStack.remove @suppressPrintableHandlerId -# This watches keyboard events and maintains @countPrefix as count and other keys are pressed. +# This watches keyboard events and maintains @countPrefix as number and other keys are pressed.  class MaintainCount extends SuppressPrintable    constructor: (options) ->      @countPrefix = "" @@ -47,63 +47,57 @@ class MaintainCount extends SuppressPrintable      count = if 0 < @countPrefix.length then parseInt @countPrefix else 1      func() for [0...count] -# This implements movement commands with count prefixes (using MaintainCount) for visual and edit modes. +forward = "forward" +backward = "backward" +character = "character" + +# This implements movement commands with count prefixes (using MaintainCount) for both visual mode and edit +# mode.  class Movement extends MaintainCount -  other: -    forward: "backward" -    backward: "forward" +  opposite: +    forward: backward +    backward: forward -  # Try to move one character in "direction".  Return 1, -1 or 0, indicating that the selection got bigger or +  # Try to move one character in "direction".  Return 1, -1 or 0, indicating that the selection got bigger, or    # smaller, or is unchanged. -  moveInDirection: (direction, selection = window.getSelection()) -> -    length = selection.toString().length -    selection.modify "extend", direction, "character" -    selection.toString().length - length +  moveInDirection: (direction) -> +    length = @selection.toString().length +    @selection.modify "extend", direction, character +    @selection.toString().length - length -  # Get the direction of the selection, either "forward" or "backward". +  # Get the direction of the selection, either forward or backward.    # FIXME(smblott).  There has to be a better way! -  getDirection: (selection = window.getSelection()) -> -    # Try to move the selection forward, then check whether it got bigger or smaller (then restore it). -    success = @moveInDirection "forward", selection -    if success -      @moveInDirection "backward", selection -      return if success < 0 then "backward" else "forward" - -    # If we can't move forward, we could be at the end of the document, so try moving backward instead. -    success = @moveInDirection "backward", selection -    if success -      @moveInDirection "forward", selection -      return if success < 0 then "forward" else "backward" - -    "none" +  getDirection: -> +    # Try to move the selection forward or backward, then check whether it got bigger or smaller (then restore +    # it). +    for type in [ forward, backward ] +      if success = @moveInDirection type +        @moveInDirection @opposite[type] +        return if 0 < success then type else @opposite[type]    nextCharacter: (direction) ->      if @moveInDirection direction -      text = window.getSelection().toString() -      @moveInDirection @other[direction] -      console.log text.charAt(if direction == "forward" then text.length - 1 else 0) -      text.charAt(if @getDirection() == "forward" then text.length - 1 else 0) +      text = @selection.toString() +      @moveInDirection @opposite[direction] +      text.charAt if @getDirection() == forward then text.length - 1 else 0    moveByWord: (direction) ->      # We go to the end of the next word, then come back to the start of it. -    movements = [ "#{direction} word", "#{@other[direction]} word" ] -    # If we're in the middle of a word, then we need to first skip over it. -    console.log @nextCharacter direction -    switch direction -      when "forward" -        movements.unshift "#{direction} word" unless /\s/.test @nextCharacter direction -      when "backward" -        movements.push "#{direction} word" unless /\s/.test @nextCharacter direction -    console.log movements +    movements = [ "#{direction} word", "#{@opposite[direction]} word" ] +    # If we're in the middle of a word, then we also need to skip over that one. +    movements.unshift "#{direction} word" unless /\s/.test @nextCharacter direction      @runMovements movements +  # Run a movement command.  Return true if the length of the selection changed, false otherwise.    runMovement: (movement) -> -    window.getSelection().modify @alterMethod, movement.split(" ")... +    length = @selection.toString().length +    @selection.modify @alterMethod, movement.split(" ")... +    @selection.toString().length != length    runMovements: (movements) ->      for movement in movements -      @runMovement movement +      break unless @runMovement movement    movements:      "l": "forward character" @@ -121,19 +115,19 @@ class Movement extends MaintainCount      "G": "forward documentboundary"      "g": "backward documentboundary" -    "w": -> @moveByWord "forward" -    "W": -> @moveByWord "backward" +    "w": -> @moveByWord forward +    "W": -> @moveByWord backward      "o": -> -      selection = window.getSelection() -      length = selection.toString().length -      switch @getDirection selection -        when "forward" -          selection.collapseToEnd() -          selection.modify "extend", "backward", "character" for [0...length] -        when "backward" -          selection.collapseToStart() -          selection.modify "extend", "forward", "character" for [0...length] +      # FIXME(smblott).  This is super slow if the selection is large. +      length = @selection.toString().length +      switch @getDirection() +        when forward +          @selection.collapseToEnd() +          @selection.modify "extend", backward, character for [0...length] +        when backward +          @selection.collapseToStart() +          @selection.modify "extend", forward, character for [0...length]    # TODO(smblott). What do we do if there is no initial selection?  Or multiple ranges?    constructor: (options) -> @@ -147,15 +141,45 @@ class Movement extends MaintainCount            unless event.metaKey or event.ctrlKey or event.altKey              keyChar = String.fromCharCode event.charCode              if @movements[keyChar] +              @selection = window.getSelection()                @runCountPrefixTimes =>                  switch typeof @movements[keyChar]                    when "string"                      @runMovement @movements[keyChar]                    when "function"                      @movements[keyChar].call @ +              # Try to scroll the leading end of the selection into view.  getLeadingElement() seems to work +              # most, but not all, of the time. +              leadingElement = @getLeadingElement @selection +              Scroller.scrollIntoView leadingElement if leadingElement + +  # Adapted from: http://roysharon.com/blog/37. +  # I have no idea how this works (smblott, 2015/1/22). +  getLeadingElement: (selection) -> +    r = t = selection.getRangeAt 0 +    if selection.type == "Range" +      r = t.cloneRange() +      r.collapse @getDirection() == backward +    t = r.startContainer +    t = t.childNodes[r.startOffset] if t.nodeType == 1 +    o = t +    o = o.previousSibling while o and o.nodeType != 1 +    t = o || t?.parentNode +    t  class VisualMode extends Movement    constructor: (options = {}) -> +    @selection = window.getSelection() +    type = @selection.type + +    if type == "None" +      HUD.showForDuration "An initial selection is required for visual mode.", 2500 +      return + +    # Try to start with a visible selection. +    if type == "Caret" or @selection.isCollapsed +      @moveInDirection(forward) or @moveInDirection backward +      defaults =        name: "visual"        badge: "V" @@ -168,15 +192,34 @@ class VisualMode extends Movement            unless event.metaKey or event.ctrlKey or event.altKey              switch String.fromCharCode event.charCode                when "y" +                text = window.getSelection().toString()                  chrome.runtime.sendMessage                    handler: "copyToClipboard" -                  data: window.getSelection().toString() +                  data: text                  @exit() -                # TODO(smblott). Suppress next keyup. +                handlerStack.push keyup: => false +                length = text.length +                suffix = if length == 1 then "" else "s" +                text = text[...12] + "..." if 15 < length +                HUD.showForDuration "Yanked #{length} character#{suffix}: \"#{text}\".", 2500      super extend defaults, options      @debug = true +    # FIXME(smblott). +    # onMouseUp = (event) => +    #   @alwaysContinueBubbling => +    #     if event.which == 1 +    #       window.removeEventListener onMouseUp +    #       new VisualMode @options +    # window.addEventListener "mouseup", onMouseUp, true + +  exit: -> +    super() +    unless @options.underEditMode +      if document.activeElement and DomUtils. isEditable document.activeElement +        document.activeElement.blur() +  class EditMode extends Movement    @activeElements = [] diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 6e2e1ffc..f31c4a6b 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -146,7 +146,7 @@ CoreScroller =    calibrationBoundary: 150 # Boundary between scrolls which are considered too slow, or too fast.    # Scroll element by a relative amount (a number) in some direction. -  scroll: (element, direction, amount) -> +  scroll: (element, direction, amount, continuous = true) ->      return unless amount      unless @settings.get "smoothScroll" @@ -202,6 +202,10 @@ CoreScroller =          # We're done.          checkVisibility element +    # If we've been asked not to be continuous, then we advance time, so the myKeyIsStillDown test always +    # fails. +    ++@time unless continuous +      # Launch animator.      requestAnimationFrame animate @@ -242,5 +246,13 @@ Scroller =      amount = getDimension(element,direction,pos) - element[scrollProperties[direction].axisName]      CoreScroller.scroll element, direction, amount +  scrollIntoView: (element) -> +    activatedElement ||= document.body and firstScrollableElement() +    rect = element.getBoundingClientRect() +    if rect.top < 0 +      CoreScroller.scroll activatedElement, "y", rect.top - 50, false +    else if window.innerHeight < rect.bottom +      CoreScroller.scroll activatedElement, "y", 50 + rect.bottom - window.innerHeight, false +  root = exports ? window  root.Scroller = Scroller diff --git a/lib/keyboard_utils.coffee b/lib/keyboard_utils.coffee index 30d99656..02c26610 100644 --- a/lib/keyboard_utils.coffee +++ b/lib/keyboard_utils.coffee @@ -59,7 +59,12 @@ KeyboardUtils =    # identify any of chrome's own keyboard shortcuts as printable.    isPrintable: (event) ->      return false if event.metaKey or event.ctrlKey or event.altKey -    @getKeyChar(event)?.length == 1 +    keyChar = +      if event.type == "keypress" +        String.fromCharCode event.charCode +      else +        @getKeyChar event +    keyChar.length == 1  KeyboardUtils.init() | 
