Bug 449522 - Context menu for HTML5 <video> elements. r=gavin, ui-r=boriss
authorJustin Dolske <dolske@mozilla.com>
Wed, 22 Oct 2008 23:36:52 -0700
changeset 20775 50053981fd5d06b655b1965bc2e50a5fa168cb71
parent 20774 d8b4b7b0ec28463b2bcbbfd765fa61b9b17eab8a
child 20776 9f3f0b99cf37105b55007736db910af5060edb2b
push idunknown
push userunknown
push dateunknown
reviewersgavin, boriss
bugs449522
milestone1.9.1b2pre
Bug 449522 - Context menu for HTML5 <video> elements. r=gavin, ui-r=boriss
browser/base/content/browser-context.inc
browser/base/content/nsContextMenu.js
browser/base/content/test/Makefile.in
browser/base/content/test/ctxmenu-image.png
browser/base/content/test/subtst_contextmenu.html
browser/base/content/test/test_contextmenu.html
browser/locales/en-US/chrome/browser/browser.dtd
browser/themes/gnomestripe/browser/browser.css
toolkit/locales/en-US/chrome/global/contentAreaCommands.properties
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -69,52 +69,101 @@
                 label="&copyEmailCmd.label;"
                 accesskey="&copyEmailCmd.accesskey;"
                 oncommand="gContextMenu.copyEmail();"/>
       <menuitem id="context-copylink"
                 label="&copyLinkCmd.label;"
                 accesskey="&copyLinkCmd.accesskey;"
                 oncommand="goDoCommand('cmd_copyLink');"/>
       <menuseparator id="context-sep-copylink"/>
+      <menuitem id="context-media-play"
+                label="&mediaPlay.label;"
+                accesskey="&mediaPlay.accesskey;"
+                oncommand="gContextMenu.mediaCommand('play');"/>
+      <menuitem id="context-media-pause"
+                label="&mediaPause.label;"
+                accesskey="&mediaPause.accesskey;"
+                oncommand="gContextMenu.mediaCommand('pause');"/>
+      <menuitem id="context-media-mute"
+                label="&mediaMute.label;"
+                accesskey="&mediaMute.accesskey;"
+                oncommand="gContextMenu.mediaCommand('mute');"/>
+      <menuitem id="context-media-unmute"
+                label="&mediaUnmute.label;"
+                accesskey="&mediaUnmute.accesskey;"
+                oncommand="gContextMenu.mediaCommand('unmute');"/>
+      <menuitem id="context-media-showcontrols"
+                label="&mediaShowControls.label;"
+                accesskey="&mediaShowControls.accesskey;"
+                oncommand="gContextMenu.mediaCommand('showcontrols');"/>
+      <menuitem id="context-media-hidecontrols"
+                label="&mediaHideControls.label;"
+                accesskey="&mediaHideControls.accesskey;"
+                oncommand="gContextMenu.mediaCommand('hidecontrols');"/>
+      <menuseparator id="context-media-sep-commands"/>
       <menuitem id="context-showimage"
                 label="&showImageCmd.label;"
                 accesskey="&showImageCmd.accesskey;"
                 oncommand="gContextMenu.showImage();"/>
       <menuitem id="context-viewimage"
                 label="&viewImageCmd.label;"
                 accesskey="&viewImageCmd.accesskey;"
-                oncommand="gContextMenu.viewImage(event);"
+                oncommand="gContextMenu.viewMedia(event);"
                 onclick="checkForMiddleClick(this, event);"/>
 #ifdef CONTEXT_COPY_IMAGE_CONTENTS
       <menuitem id="context-copyimage-contents"
                 label="&copyImageContentsCmd.label;"
                 accesskey="&copyImageContentsCmd.accesskey;"
                 oncommand="goDoCommand('cmd_copyImageContents');"/>
 #endif
       <menuitem id="context-copyimage"
                 label="&copyImageCmd.label;"
                 accesskey="&copyImageCmd.accesskey;"
-                oncommand="goDoCommand('cmd_copyImageLocation');"/>
+                oncommand="gContextMenu.copyMediaLocation();"/>
+      <menuitem id="context-copyvideourl"
+                label="&copyVideoURLCmd.label;"
+                accesskey="&copyVideoURLCmd.accesskey;"
+                oncommand="gContextMenu.copyMediaLocation();"/>
+      <menuitem id="context-copyaudiourl"
+                label="&copyAudioURLCmd.label;"
+                accesskey="&copyAudioURLCmd.accesskey;"
+                oncommand="gContextMenu.copyMediaLocation();"/>
       <menuseparator id="context-sep-copyimage"/>
       <menuitem id="context-saveimage"
                 label="&saveImageCmd.label;"
                 accesskey="&saveImageCmd.accesskey;"
-                oncommand="gContextMenu.saveImage();"/>
+                oncommand="gContextMenu.saveMedia();"/>
       <menuitem id="context-sendimage"  
                 label="&sendImageCmd.label;" 
                 accesskey="&sendImageCmd.accesskey;" 
-                oncommand="gContextMenu.sendImage();"/>
+                oncommand="gContextMenu.sendMedia();"/>
       <menuitem id="context-setDesktopBackground"
                 label="&setDesktopBackgroundCmd.label;"
                 accesskey="&setDesktopBackgroundCmd.accesskey;"
                 oncommand="gContextMenu.setDesktopBackground();"/>
       <menuitem id="context-blockimage"
                 type="checkbox"
                 accesskey="&blockImageCmd.accesskey;"
                 oncommand="gContextMenu.toggleImageBlocking(this.getAttribute('checked') == 'true');"/>
+      <menuitem id="context-savevideo"
+                label="&saveVideoCmd.label;"
+                accesskey="&saveVideoCmd.accesskey;"
+                oncommand="gContextMenu.saveMedia();"/>
+      <menuitem id="context-saveaudio"
+                label="&saveAudioCmd.label;"
+                accesskey="&saveAudioCmd.accesskey;"
+                oncommand="gContextMenu.saveMedia();"/>
+      <menuitem id="context-sendvideo"
+                label="&sendVideoCmd.label;"
+                accesskey="&sendVideoCmd.accesskey;"
+                oncommand="gContextMenu.sendMedia();"/>
+      <menuitem id="context-sendaudio"
+                label="&sendAudioCmd.label;"
+                accesskey="&sendAudioCmd.accesskey;"
+                oncommand="gContextMenu.sendMedia();"/>
       <menuitem id="context-back"
                 label="&backCmd.label;"
                 accesskey="&backCmd.accesskey;"
                 chromedir="&locale.dir;"
                 command="Browser:BackOrBackDuplicate"
                 onclick="checkForMiddleClick(this, event);"/>
       <menuitem id="context-forward"
                 label="&forwardCmd.label;"
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -37,16 +37,17 @@
 #   Jesse Ruderman <jruderman@gmail.com>
 #   Joe Hughes <joe@retrovirus.com>
 #   Pamela Greene <pamg.bugs@gmail.com>
 #   Michael Ventnor <ventnors_dogs234@yahoo.com.au>
 #   Simon Bünzli <zeniko@gmail.com>
 #   Gijs Kruitbosch <gijskruitbosch@gmail.com>
 #   Ehsan Akhgari <ehsan.akhgari@gmail.com>
 #   Dan Mosedale <dmose@mozilla.org>
+#   Justin Dolske <dolske@mozilla.com>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either the GNU General Public License Version 2 or later (the "GPL"), or
 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -63,16 +64,18 @@ function nsContextMenu(aXulMenu, aBrowse
   this.menu              = null;
   this.isFrameImage      = false;
   this.onTextInput       = false;
   this.onKeywordField    = false;
   this.onImage           = false;
   this.onLoadedImage     = false;
   this.onCompletedImage  = false;
   this.onCanvas          = false;
+  this.onVideo           = false;
+  this.onAudio           = false;
   this.onLink            = false;
   this.onMailtoLink      = false;
   this.onSaveableLink    = false;
   this.onMetaDataItem    = false;
   this.onMathML          = false;
   this.link              = false;
   this.linkURL           = "";
   this.linkURI           = null;
@@ -123,16 +126,17 @@ nsContextMenu.prototype = {
     this.initOpenItems();
     this.initNavigationItems();
     this.initViewItems();
     this.initMiscItems();
     this.initSpellingItems();
     this.initSaveItems();
     this.initClipboardItems();
     this.initMetadataItems();
+    this.initMediaPlayerItems();
   },
 
   initOpenItems: function CM_initOpenItems() {
     var isMailtoInternal = false;
     if (this.onMailtoLink) {
       var mailtoHandler = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
                           getService(Ci.nsIExternalProtocolService).
                           getProtocolHandlerInfo("mailto");
@@ -144,58 +148,67 @@ nsContextMenu.prototype = {
                      (this.inDirList && this.onLink);
     this.showItem("context-openlink", shouldShow);
     this.showItem("context-openlinkintab", shouldShow);
     this.showItem("context-sep-open", shouldShow);
   },
 
   initNavigationItems: function CM_initNavigationItems() {
     var shouldShow = !(this.isContentSelected || this.onLink || this.onImage ||
-                       this.onCanvas || this.onTextInput);
+                       this.onCanvas || this.onVideo || this.onAudio ||
+                       this.onTextInput);
     this.showItem("context-back", shouldShow);
     this.showItem("context-forward", shouldShow);
     this.showItem("context-reload", shouldShow);
     this.showItem("context-stop", shouldShow);
     this.showItem("context-sep-stop", shouldShow);
 
     // XXX: Stop is determined in browser.js; the canStop broadcaster is broken
     //this.setItemAttrFromNode( "context-stop", "disabled", "canStop" );
   },
 
   initSaveItems: function CM_initSaveItems() {
     var shouldShow = !(this.inDirList || this.onTextInput || this.onLink ||
-                       this.isContentSelected || this.onImage || this.onCanvas);
+                       this.isContentSelected || this.onImage ||
+                       this.onCanvas || this.onVideo || this.onAudio);
     this.showItem("context-savepage", shouldShow);
     this.showItem("context-sendpage", shouldShow);
 
     // Save+Send link depends on whether we're in a link.
     this.showItem("context-savelink", this.onSaveableLink);
     this.showItem("context-sendlink", this.onSaveableLink);
 
-    // Save image depends on whether we're on a loaded image, or a canvas.
+    // Save image depends on having loaded its content, video and audio don't.
     this.showItem("context-saveimage", this.onLoadedImage || this.onCanvas);
-    // We can send an image (even unloaded), but not a canvas:
+    this.showItem("context-savevideo", this.onVideo);
+    this.showItem("context-saveaudio", this.onAudio);
+    // Send media URL (but not for canvas, since it's a big data: URL)
     this.showItem("context-sendimage", this.onImage);
+    this.showItem("context-sendvideo", this.onVideo);
+    this.showItem("context-sendaudio", this.onAudio);
   },
 
   initViewItems: function CM_initViewItems() {
     // View source is always OK, unless in directory listing.
     this.showItem("context-viewpartialsource-selection",
                   this.isContentSelected);
     this.showItem("context-viewpartialsource-mathml",
                   this.onMathML && !this.isContentSelected);
 
     var shouldShow = !(this.inDirList || this.isContentSelected ||
-                       this.onImage || this.onLink || this.onTextInput);
+                       this.onImage || this.onCanvas ||
+                       this.onVideo || this.onAudio ||
+                       this.onLink || this.onTextInput);
     this.showItem("context-viewsource", shouldShow);
     this.showItem("context-viewinfo", shouldShow);
 
     this.showItem("context-sep-properties",
                   !(this.inDirList || this.isContentSelected ||
-                    this.onTextInput));
+                    this.onTextInput || this.onCanvas ||
+                    this.onVideo || this.onAudio));
 
     // Set as Desktop background depends on whether an image was clicked on,
     // and only works if we have a shell service.
     var haveSetDesktopBackground = false;
 #ifdef HAVE_SHELL_SERVICE
     // Only enable Set as Desktop Background if we can get the shell service.
     var shell = getShellService();
     if (shell)
@@ -222,18 +235,18 @@ nsContextMenu.prototype = {
     this.showItem("context-sep-viewbgimage", shouldShow);
     document.getElementById("context-viewbgimage")
             .disabled = !this.hasBGImage;
   },
 
   initMiscItems: function CM_initMiscItems() {
     // Use "Bookmark This Link" if on a link.
     this.showItem("context-bookmarkpage",
-                  !(this.isContentSelected || this.onTextInput ||
-                    this.onLink || this.onImage));
+                  !(this.isContentSelected || this.onTextInput || this.onLink ||
+                    this.onImage || this.onVideo || this.onAudio));
     this.showItem("context-bookmarklink", this.onLink && !this.onMailtoLink);
     this.showItem("context-searchselect", this.isTextSelected);
     this.showItem("context-keywordfield",
                   this.onTextInput && this.onKeywordField);
     this.showItem("frame", this.inFrame);
     this.showItem("frame-sep", this.inFrame);
 
     // Hide menu entries for images, show otherwise
@@ -334,64 +347,82 @@ nsContextMenu.prototype = {
     this.showItem("context-undo", this.onTextInput);
     this.showItem("context-sep-undo", this.onTextInput);
     this.showItem("context-cut", this.onTextInput);
     this.showItem("context-copy",
                   this.isContentSelected || this.onTextInput);
     this.showItem("context-paste", this.onTextInput);
     this.showItem("context-delete", this.onTextInput);
     this.showItem("context-sep-paste", this.onTextInput);
-    this.showItem("context-selectall",
-                  !(this.onLink || this.onImage) || this.isDesignMode);
+    this.showItem("context-selectall", !(this.onLink || this.onImage ||
+                  this.onVideo || this.onAudio) || this.isDesignMode);
     this.showItem("context-sep-selectall", this.isContentSelected );
 
     // XXX dr
     // ------
     // nsDocumentViewer.cpp has code to determine whether we're
     // on a link or an image. we really ought to be using that...
 
     // Copy email link depends on whether we're on an email link.
     this.showItem("context-copyemail", this.onMailtoLink);
 
     // Copy link location depends on whether we're on a non-mailto link.
     this.showItem("context-copylink", this.onLink && !this.onMailtoLink);
-    this.showItem("context-sep-copylink", this.onLink && this.onImage);
+    this.showItem("context-sep-copylink", this.onLink &&
+                  (this.onImage || this.onVideo || this.onAudio));
 
 #ifdef CONTEXT_COPY_IMAGE_CONTENTS
     // Copy image contents depends on whether we're on an image.
     this.showItem("context-copyimage-contents", this.onImage);
 #endif
     // Copy image location depends on whether we're on an image.
     this.showItem("context-copyimage", this.onImage);
-    this.showItem("context-sep-copyimage", this.onImage);
+    this.showItem("context-copyvideourl", this.onVideo);
+    this.showItem("context-copyaudiourl", this.onAudio);
+    this.showItem("context-sep-copyimage", this.onImage ||
+                  this.onVideo || this.onAudio);
   },
 
   initMetadataItems: function() {
     // Show if user clicked on something which has metadata.
     this.showItem("context-metadata", this.onMetaDataItem);
   },
 
+  initMediaPlayerItems: function() {
+    var onMedia = (this.onVideo || this.onAudio);
+    // Several mutually exclusive items... play/pause, mute/unmute, show/hide
+    this.showItem("context-media-play",  onMedia && this.target.paused);
+    this.showItem("context-media-pause", onMedia && !this.target.paused);
+    this.showItem("context-media-mute",   onMedia && !this.target.muted);
+    this.showItem("context-media-unmute", onMedia && this.target.muted);
+    this.showItem("context-media-showcontrols", onMedia && !this.target.controls)
+    this.showItem("context-media-hidecontrols", onMedia && this.target.controls)
+    this.showItem("context-media-sep-commands",  onMedia);
+  },
+
   // Set various context menu attributes based on the state of the world.
   setTarget: function (aNode, aRangeParent, aRangeOffset) {
     const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
     if (aNode.namespaceURI == xulNS ||
         this.isTargetAFormControl(aNode)) {
       this.shouldDisplay = false;
     }
 
     // Initialize contextual info.
     this.onImage           = false;
     this.onLoadedImage     = false;
     this.onCompletedImage  = false;
     this.onStandaloneImage = false;
     this.onCanvas          = false;
+    this.onVideo           = false;
+    this.onAudio           = false;
     this.onMetaDataItem    = false;
     this.onTextInput       = false;
     this.onKeywordField    = false;
-    this.imageURL          = "";
+    this.mediaURL          = "";
     this.onLink            = false;
     this.linkURL           = "";
     this.linkURI           = null;
     this.linkProtocol      = "";
     this.onMathML          = false;
     this.inFrame           = false;
     this.hasBGImage        = false;
     this.bgImageURL        = "";
@@ -420,23 +451,31 @@ nsContextMenu.prototype = {
                 
         var request =
           this.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
         if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
           this.onLoadedImage = true;
         if (request && (request.imageStatus & request.STATUS_LOAD_COMPLETE))
           this.onCompletedImage = true;
 
-        this.imageURL = this.target.currentURI.spec;
+        this.mediaURL = this.target.currentURI.spec;
         if (this.target.ownerDocument instanceof ImageDocument)
           this.onStandaloneImage = true;
       }
       else if (this.target instanceof HTMLCanvasElement) {
         this.onCanvas = true;
       }
+      else if (this.target instanceof HTMLVideoElement) {
+        this.onVideo = true;
+        this.mediaURL = this.target.src;
+      }
+      else if (this.target instanceof HTMLAudioElement) {
+        this.onAudio = true;
+        this.mediaURL = this.target.src;
+      }
       else if (this.target instanceof HTMLInputElement ) {
         this.onTextInput = this.isTargetATextBox(this.target);
         // allow spellchecking UI on all writable text boxes except passwords
         if (this.onTextInput && ! this.target.readOnly &&
             this.target.type != "password") {
           this.possibleSpellChecking = true;
           InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
           InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
@@ -733,32 +772,32 @@ nsContextMenu.prototype = {
     BrowserPageInfo(this.target.ownerDocument.defaultView.top.document);
   },
 
   viewFrameInfo: function() {
     BrowserPageInfo(this.target.ownerDocument);
   },
 
   showImage: function(e) {
-    urlSecurityCheck(this.imageURL,
+    urlSecurityCheck(this.mediaURL,
                      this.browser.contentPrincipal,
                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
 
     if (this.target instanceof Ci.nsIImageLoadingContent)
       this.target.forceReload();
   },
 
-  // Change current window to the URL of the image.
-  viewImage: function(e) {
+  // Change current window to the URL of the image, video, or audio.
+  viewMedia: function(e) {
     var viewURL;
 
     if (this.onCanvas)
       viewURL = this.target.toDataURL();
     else {
-      viewURL = this.imageURL;
+      viewURL = this.mediaURL;
       urlSecurityCheck(viewURL,
                        this.browser.contentPrincipal,
                        Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     }
 
     var doc = this.target.ownerDocument;
     openUILink(viewURL, e, null, null, null, null, doc.documentURIObject );
   },
@@ -954,33 +993,51 @@ nsContextMenu.prototype = {
     channel.asyncOpen(new saveAsListener(), null);
   },
 
   sendLink: function() {
     // we don't know the title of the link so pass in an empty string
     MailIntegration.sendMessage( this.linkURL, "" );
   },
 
-  // Save URL of clicked-on image.
-  saveImage: function() {
+  // Backwards-compatability wrapper
+  saveImage : function() {
+    if (this.onCanvas || this.onImage)
+        this.saveMedia();
+  },
+
+  // Save URL of the clicked upon image, video, or audio.
+  saveMedia: function() {
     var doc =  this.target.ownerDocument;
     if (this.onCanvas) {
       // Bypass cache, since it's a data: URL.
       saveImageURL(this.target.toDataURL(), "canvas.png", "SaveImageTitle",
                    true, false, doc.documentURIObject);
     }
-    else {
-      urlSecurityCheck(this.imageURL, doc.nodePrincipal);
-      saveImageURL(this.imageURL, null, "SaveImageTitle", false,
+    else if (this.onImage) {
+      urlSecurityCheck(this.mediaURL, doc.nodePrincipal);
+      saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
                    false, doc.documentURIObject);
     }
+    else if (this.onVideo || this.onAudio) {
+      urlSecurityCheck(this.mediaURL, doc.nodePrincipal);
+      var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
+      saveURL(this.mediaURL, null, dialogTitle, false,
+              false, doc.documentURIObject);
+    }
   },
 
-  sendImage: function() {
-    MailIntegration.sendMessage(this.imageURL, "");
+  // Backwards-compatability wrapper
+  sendImage : function() {
+    if (this.onCanvas || this.onImage)
+        this.sendMedia();
+  },
+
+  sendMedia: function() {
+    MailIntegration.sendMessage(this.mediaURL, "");
   },
 
   toggleImageBlocking: function(aBlock) {
     var permissionmanager = Cc["@mozilla.org/permissionmanager;1"].
                             getService(Ci.nsIPermissionManager);
 
     var uri = this.target.QueryInterface(Ci.nsIImageLoadingContent).currentURI;
 
@@ -1341,10 +1398,41 @@ nsContextMenu.prototype = {
   },
 
   printFrame: function CM_printFrame() {
     PrintUtils.print(this.target.ownerDocument.defaultView);
   },
 
   switchPageDirection: function CM_switchPageDirection() {
     SwitchDocumentDirection(this.browser.contentWindow);
+  },
+
+  mediaCommand : function CM_mediaCommand(command) {
+    var media = this.target;
+
+    switch (command) {
+      case "play":
+        media.play();
+        break;
+      case "pause":
+        media.pause();
+        break;
+      case "mute":
+        media.muted = true;
+        break;
+      case "unmute":
+        media.muted = false;
+        break;
+      case "hidecontrols":
+        media.removeAttribute("controls");
+        break;
+      case "showcontrols":
+        media.setAttribute("controls", "true");
+        break;
+    }
+  },
+
+  copyMediaLocation : function () {
+    var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+                    getService(Ci.nsIClipboardHelper);
+    clipboard.copyString(this.mediaURL);
   }
 };
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -42,16 +42,19 @@ relativesrcdir  = browser/base/content/t
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES =	test_feed_discovery.html \
 		feed_discovery.html \
 		test_bug395533.html \
 		bug395533-data.txt \
+		test_contextmenu.html \
+		subtst_contextmenu.html \
+		ctxmenu-image.png \
 		$(NULL)
 
 # browser_bug423833.js disabled temporarily since it's unreliable: bug 428712
 # browser_sanitize-download-history.js disabled temporarily since it's unreliable: bug 432425
 _BROWSER_FILES = browser_bug321000.js \
                  browser_bug405137.js \
                  browser_bug409481.js \
                  browser_bug413915.js \
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4c3be508477eb19cd08ddf4d0f568a06a4ec7a45
GIT binary patch
literal 5401
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
IX|7cLfA!KSp8x;=
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/subtst_contextmenu.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Subtest for browser context menu</title>
+</head>
+<body>
+Browser context menu subtest.
+
+<div id="test-text">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div>
+<a id="test-link" href="http://mozilla.com">Click the monkey!</a>
+<a id="test-mailto" href="mailto:codemonkey@mozilla.com">Mail the monkey!</a><br>
+<input id="test-input"><br>
+<img id="test-image" src="ctxmenu-image.png">
+<canvas id="test-canvas" width="100" height="100" style="background-color: blue"></canvas>
+<video id="test-video"   width="100" height="100" style="background-color: orange"></video>
+<iframe id="test-iframe" width="98"  height="98" style="border: 1px solid black"></iframe>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/test_contextmenu.html
@@ -0,0 +1,347 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Tests for browser context menu</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>  
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Browser context menu tests.
+<p id="display"></p>
+
+<div id="content">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: multiple login autocomplete. **/
+
+netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+function openContextMenuFor(element) {
+    // Context menu should be closed before we open it again.
+    is(contextMenu.state, "closed", "checking if popup is closed");
+
+    // XXX this doesn't work:
+    // var eventDetails = { type : "contextmenu", button : 2 }
+    // synthesizeMouse(element, 50, 90, eventDetails);
+    //
+    // It triggers the popup, but then we fail in nsContextMenu when
+    // initializing: ine 565: this.target.ownerDocument is null
+    // |this.target| was assigned there from |document.popupNode|, but it's a
+    // HTMLDocument instead of the node we supposedly fired the event at.
+    // I think the event's |target| is never being set, and we're hitting the
+    // fallback case in nsXULPopupListener::PreLaunchPopup.
+    //
+    // Also interesting is that just firing a mousedown+mouseup doesn't seem
+    // to do anything. Not clear why we'd specifically have to fire a
+    // contextmenu event instead of just a right-click.
+
+    // This seems to work, as long as we explicitly set the popupNode first.
+    // For frames, the popupNode needs to be set to inside the frame.
+    if (element.localName == "IFRAME")
+        chromeWin.document.popupNode = element.contentDocument.body;
+    else
+        chromeWin.document.popupNode = element;
+    contextMenu.openPopup(element, "overlap", 5, 5, true, false);
+}
+
+function closeContextMenu() {
+    contextMenu.hidePopup();
+}
+
+function getVisibleMenuItems(aMenu) {
+    var items = [];
+    var accessKeys = {};
+    for (var i = 0; i < aMenu.childNodes.length; i++) {
+        var item = aMenu.childNodes[i];
+        if (item.hidden)
+            continue;
+
+        var key = item.accessKey;
+        if (key)
+            key = key.toLowerCase();
+
+        if (item.nodeName == "menuitem") {
+            ok(item.id, "child menuitem #" + i + " has an ID");
+            ok(key, "menuitem has an access key");
+            if (accessKeys[key])
+                ok(false, "menuitem " + item.id + " has same accesskey as " + accessKeys[key]);
+            else
+                accessKeys[key] = item.id
+            items.push(item.id);
+        } else if (item.nodeName == "menuseparator") {
+            ok(true, "--- seperator id is " + item.id);
+            items.push("---");
+        } else if (item.nodeName == "menu") {
+            ok(item.id, "child menu #" + i + " has an ID");
+            ok(key, "menu has an access key");
+            if (accessKeys[key])
+                ok(false, "menu " + item.id + " has same accesskey as " + accessKeys[key]);
+            else
+                accessKeys[key] = item.id
+            items.push(item.id);
+            // Add a dummy item to that the indexes in checkMenu are the same
+            // for expectedItems and actualItems.
+            items.push([]);
+        } else {
+            ok(false, "child #" + i + " of menu ID " + aMenu.id +
+                      " has an unknown type (" + item.nodeName + ")");
+        }
+    }
+    return items;
+}
+
+function checkContextMenu(expectedItems) {
+    is(contextMenu.state, "open", "checking if popup is open");
+    checkMenu(contextMenu, expectedItems);
+}
+
+/*
+ * checkMenu - checks to see if the specified <menupopup> contains the
+ * expected items, as specified by an array of element IDs. To check the
+ * contents of a submenu, include a nested array after the expected <menu> ID.
+ * For example: ["foo, "submenu", ["sub1", "sub2"], "bar"]
+ * 
+ */
+function checkMenu(menu, expectedItems) {
+    var actualItems = getVisibleMenuItems(menu);
+    //ok(false, "Items are: " + actualItems);
+    for (var i = 0; i < expectedItems.length; i++) {
+        if (expectedItems[i] instanceof Array) {
+            ok(true, "Checking submenu...");
+            var menuID = expectedItems[i - 1]; // The last item was the menu ID.
+            var submenu = menu.getElementsByAttribute("id", menuID)[0];
+            ok(submenu && submenu.nodeName == "menu", "got expected submenu element");
+            checkMenu(submenu.menupopup, expectedItems[i]);
+        } else {
+            is(actualItems[i], expectedItems[i],
+               "checking item #" + i + " (" + expectedItems[i] + ")");
+        }
+    }
+    // Could find unexpected extra items at the end...
+    is(actualItems.length, expectedItems.length, "checking expected number of menu entries");
+}
+
+/*
+ * runTest
+ *
+ * Called by a popupshowing event handler. Each test checks for expected menu
+ * contents, closes the popup, and finally triggers the popup on a new element
+ * (thus kicking off another cycle).
+ *
+ */
+function runTest(testNum) {
+  // Seems we need to enable this again, or sendKeyEvent() complaints.
+  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+  ok(true, "Starting test #" + testNum);
+
+  switch(testNum) {
+    case 1:
+        // Invoke context menu for next test.
+        openContextMenuFor(text);
+        break;
+
+    case 2:
+        // Context menu for plain text
+        checkContextMenu(["context-back",
+                          "context-forward",
+                          "context-reload",
+                          "context-stop",
+                          "---",
+                          "context-bookmarkpage",
+                          "context-savepage",
+                          "context-sendpage",
+                          "---",
+                          "context-viewbgimage",
+                          "context-selectall",
+                          "---",
+                          "context-viewsource",
+                          "context-viewinfo"]);
+        closeContextMenu()
+        openContextMenuFor(link); // Invoke context menu for next test.
+        break;
+
+    case 3:
+        // Context menu for text link
+        checkContextMenu(["context-openlink",
+                          "context-openlinkintab",
+                          "---",
+                          "context-bookmarklink",
+                          "context-savelink",
+                          "context-sendlink",
+                          "context-copylink",
+                          "---",
+                          "context-metadata"]);
+        closeContextMenu()
+        openContextMenuFor(mailto); // Invoke context menu for next test.
+        break;
+
+    case 4:
+        // Context menu for text mailto-link
+        checkContextMenu(["context-copyemail",
+                          "---",
+                          "context-metadata"]);
+        closeContextMenu()
+        openContextMenuFor(input); // Invoke context menu for next test.
+        break;
+
+    case 5:
+        // Context menu for text input field
+        checkContextMenu(["context-undo",
+                          "---",
+                          "context-cut",
+                          "context-copy",
+                          "context-paste",
+                          "context-delete",
+                          "---",
+                          "context-selectall",
+                          "---",
+                          "spell-check-enabled"]);
+        closeContextMenu()
+        openContextMenuFor(img); // Invoke context menu for next test.
+        break;
+
+    case 6:
+        // Context menu for an image
+        checkContextMenu(["context-viewimage",
+                          "context-copyimage-contents",
+                          "context-copyimage",
+                          "---",
+                          "context-saveimage",
+                          "context-sendimage",
+                          "context-setDesktopBackground",
+                          "context-blockimage",
+                          "---",
+                          "context-metadata"]);
+        closeContextMenu();
+        openContextMenuFor(canvas); // Invoke context menu for next test.
+        break;
+
+    case 7:
+        // Context menu for a canvas
+        checkContextMenu(["context-viewimage",
+                          "context-saveimage",
+                          "context-bookmarkpage",
+                          "context-selectall"]);
+        closeContextMenu();
+        openContextMenuFor(video); // Invoke context menu for next test.
+        break;
+
+    case 8:
+        // Context menu for a video
+        checkContextMenu(["context-media-play",
+                          "context-media-mute",
+                          "context-media-showcontrols",
+                          "---",
+                          "context-copyvideourl",
+                          "---",
+                          "context-savevideo",
+                          "context-sendvideo"]);
+        closeContextMenu();
+        openContextMenuFor(iframe); // Invoke context menu for next test.
+        break;
+
+    case 9:
+        // Context menu for an iframe
+        checkContextMenu(["context-back",
+                          "context-forward",
+                          "context-reload",
+                          "context-stop",
+                          "---",
+                          "context-bookmarkpage",
+                          "context-savepage",
+                          "context-sendpage",
+                          "---",
+                          "context-viewbgimage",
+                          "context-selectall",
+                          "---",
+                          "frame",
+                              ["context-showonlythisframe",
+                               "context-openframe",
+                               "context-openframeintab",
+                               "---",
+                               "context-reloadframe",
+                               "---",
+                               "context-bookmarkframe",
+                               "context-saveframe",
+                               "---",
+                               "context-printframe",
+                               "---",
+                               "context-viewframesource",
+                               "context-viewframeinfo"],
+                          "---",
+                          "context-viewsource",
+                          "context-viewinfo"]);
+        closeContextMenu();
+
+        subwindow.close();
+        SimpleTest.finish();
+        return;
+
+    /*
+     * Other things that would be nice to test:
+     *  - selected text
+     *  - spelling / misspelled word (in text input?)
+     *  - check state of disabled items
+     *  - test execution of menu items (maybe as a separate test?)
+     */
+
+    default:
+        ok(false, "Unexpected invocataion of test #" + testNum);
+        subwindow.close();
+        SimpleTest.finish();
+        return;
+  }
+
+}
+
+
+var testNum = 1;
+var subwindow, chromeWin, contextMenu;
+var text, link, mailto, input, img, canvas, video, iframe;
+
+function startTest() {
+    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+    chromeWin = subwindow
+                    .QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIWebNavigation)
+                    .QueryInterface(Ci.nsIDocShellTreeItem)
+                    .rootTreeItem
+                    .QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDOMWindow)
+                    .QueryInterface(Ci.nsIDOMChromeWindow);
+    contextMenu = chromeWin.document.getElementById("contentAreaContextMenu");
+    ok(contextMenu, "Got context menu XUL");
+
+    text   = subwindow.document.getElementById("test-text");
+    link   = subwindow.document.getElementById("test-link");
+    mailto = subwindow.document.getElementById("test-mailto");
+    input  = subwindow.document.getElementById("test-input");
+    img    = subwindow.document.getElementById("test-image");
+    canvas = subwindow.document.getElementById("test-canvas");
+    video  = subwindow.document.getElementById("test-video");
+    iframe = subwindow.document.getElementById("test-iframe");
+
+    contextMenu.addEventListener("popupshown", function() { runTest(++testNum); }, false);
+    runTest(1);
+}
+
+// We open this in a separate window, because the Mochitests run inside a frame.
+// The frame causes an extra menu item, and prevents running the test
+// standalone (ie, clicking the test name in the Mochitest window) to see
+// success/failure messages.
+var subwindow = window.open("./subtst_contextmenu.html", "contextmenu-subtext", "width=600,height=700");
+subwindow.onload = startTest;
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -259,34 +259,61 @@
 <!ENTITY printFrameCmd.label          "Print Frame…">
 <!ENTITY printFrameCmd.accesskey      "P">
 <!ENTITY saveLinkCmd.label            "Save Link As…">
 <!ENTITY saveLinkCmd.accesskey        "k">
 <!ENTITY sendLinkCmd.label            "Send Link…"> 
 <!ENTITY sendLinkCmd.accesskey        "d">
 <!ENTITY saveImageCmd.label           "Save Image As…">
 <!ENTITY saveImageCmd.accesskey       "v">
+<!ENTITY saveVideoCmd.label           "Save Video As…">
+<!ENTITY saveVideoCmd.accesskey       "v">
+<!ENTITY saveAudioCmd.label           "Save Audio As…">
+<!ENTITY saveAudioCmd.accesskey       "v">
 <!ENTITY sendImageCmd.label           "Send Image…">
 <!ENTITY sendImageCmd.accesskey       "n">
+<!ENTITY sendVideoCmd.label           "Send Video…">
+<!ENTITY sendVideoCmd.accesskey       "n">
+<!ENTITY sendAudioCmd.label           "Send Audio…">
+<!ENTITY sendAudioCmd.accesskey       "n">
 <!ENTITY copyLinkCmd.label            "Copy Link Location">
 <!ENTITY copyLinkCmd.accesskey        "a">
 <!ENTITY copyLinkTextCmd.label        "Copy Link Text">
 <!ENTITY copyLinkTextCmd.accesskey    "x">
 <!ENTITY copyImageCmd.label           "Copy Image Location">
 <!ENTITY copyImageCmd.accesskey       "o">
 <!ENTITY copyImageContentsCmd.label   "Copy Image">
 <!ENTITY copyImageContentsCmd.accesskey  "y"> 
-<!ENTITY blockImageCmd.accesskey       "B">
+<!ENTITY copyVideoURLCmd.label        "Copy Video Location">
+<!ENTITY copyVideoURLCmd.accesskey    "o">
+<!ENTITY copyAudioURLCmd.label        "Copy Audio Location">
+<!ENTITY copyAudioURLCmd.accesskey    "o">
+<!ENTITY blockImageCmd.accesskey      "B">
 <!ENTITY metadataCmd.label            "Properties">
 <!ENTITY metadataCmd.accesskey        "P">
 <!ENTITY copyEmailCmd.label           "Copy Email Address">
 <!ENTITY copyEmailCmd.accesskey       "E">
 <!ENTITY thisFrameMenu.label              "This Frame">
 <!ENTITY thisFrameMenu.accesskey          "h">
 
+<!-- Media (video/audio) controls -->
+<!ENTITY mediaPlay.label             "Play">
+<!ENTITY mediaPlay.accesskey         "P">
+<!ENTITY mediaPause.label            "Pause">
+<!ENTITY mediaPause.accesskey        "P">
+<!ENTITY mediaMute.label             "Mute">
+<!ENTITY mediaMute.accesskey         "M">
+<!ENTITY mediaUnmute.label           "Unmute">
+<!ENTITY mediaUnmute.accesskey       "m">
+<!ENTITY mediaShowControls.label     "Show Controls">
+<!ENTITY mediaShowControls.accesskey "C">
+<!ENTITY mediaHideControls.label     "Hide Controls">
+<!ENTITY mediaHideControls.accesskey "C">
+
+
 <!-- LOCALIZATION NOTE :
 fullZoomEnlargeCmd.commandkey3, fullZoomReduceCmd.commandkey2 and
 fullZoomResetCmd.commandkey2 are alternative acceleration keys for zoom.
 If shift key is needed with your locale popular keyboard for them,
 you can use these alternative items. Otherwise, their values should be empty.  -->
 
 <!ENTITY fullZoomEnlargeCmd.label       "Zoom In">
 <!ENTITY fullZoomEnlargeCmd.accesskey   "I">
--- a/browser/themes/gnomestripe/browser/browser.css
+++ b/browser/themes/gnomestripe/browser/browser.css
@@ -255,19 +255,29 @@ menuitem[command="cmd_newNavigatorTab"],
 menuitem[command="Browser:OpenFile"] {
   list-style-image: url("moz-icon://stock/gtk-open?size=menu");
 }
 
 #menu_close {
   list-style-image: url("moz-icon://stock/gtk-close?size=menu");
 }
 
+#context-media-play {
+  list-style-image: url("moz-icon://stock/gtk-media-play?size=menu");
+}
+
+#context-media-pause {
+  list-style-image: url("moz-icon://stock/gtk-media-pause?size=menu");
+}
+
 menuitem[command="Browser:SavePage"],
 #context-savelink,
 #context-saveimage,
+#context-savevideo,
+#context-saveaudio,
 #context-savepage,
 #context-saveframe {
   list-style-image: url("moz-icon://stock/gtk-save-as?size=menu");
 }
 
 menuitem[command="cmd_printPreview"] {
   list-style-image: url("moz-icon://stock/gtk-print-preview?size=menu");
 }
@@ -307,16 +317,18 @@ menuitem[command="cmd_cut"],
 menuitem[command="cmd_cut"][disabled],
 #context-cut[disabled] {
   list-style-image: url("moz-icon://stock/gtk-cut?size=menu&state=disabled");
 }
 
 menuitem[command="cmd_copy"],
 #context-copy,
 #context-copyimage,
+#context-copyvideourl,
+#context-copyaudiourl,
 #context-copylink,
 #context-copyemail {
   list-style-image: url("moz-icon://stock/gtk-copy?size=menu");
 }
 
 menuitem[command="cmd_copy"][disabled],
 #context-copy[disabled] {
   list-style-image: url("moz-icon://stock/gtk-copy?size=menu&state=disabled");
--- a/toolkit/locales/en-US/chrome/global/contentAreaCommands.properties
+++ b/toolkit/locales/en-US/chrome/global/contentAreaCommands.properties
@@ -1,11 +1,13 @@
 # context menu strings
 
 SaveImageTitle=Save Image
+SaveVideoTitle=Save Video
+SaveAudioTitle=Save Audio
 SaveLinkTitle=Save As
 DefaultSaveFileName=index
 WebPageCompleteFilter=Web Page, complete
 WebPageHTMLOnlyFilter=Web Page, HTML only
 WebPageXHTMLOnlyFilter=Web Page, XHTML only
 WebPageSVGOnlyFilter=Web Page, SVG only
 WebPageXMLOnlyFilter=Web Page, XML only