From c3338d3bf2cfdd1bcf11f676d94444df6b125690 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Fri, 6 May 2016 20:50:37 -0500 Subject: [PATCH] Implements custom context menu to flip the local video --- Makefile | 2 +- app.js | 1 + css/jquery.contextMenu.css | 206 ++++++++++++++++++++++++++ lang/main.json | 3 +- modules/UI/videolayout/LargeVideo.js | 27 +++- modules/UI/videolayout/LocalVideo.js | 64 +++++++- modules/UI/videolayout/SmallVideo.js | 3 - modules/UI/videolayout/VideoLayout.js | 22 +++ modules/settings/Settings.js | 19 +++ package.json | 4 + service/UI/UIEvents.js | 6 +- 11 files changed, 340 insertions(+), 17 deletions(-) create mode 100644 css/jquery.contextMenu.css diff --git a/Makefile b/Makefile index 617a110ca..766325053 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ BROWSERIFY = ./node_modules/.bin/browserify UGLIFYJS = ./node_modules/.bin/uglifyjs EXORCIST = ./node_modules/.bin/exorcist CLEANCSS = ./node_modules/.bin/cleancss -CSS_FILES = font.css toastr.css main.css videolayout_default.css font-awesome.css jquery-impromptu.css modaldialog.css notice.css popup_menu.css login_menu.css popover.css jitsi_popover.css contact_list.css chat.css welcome_page.css settingsmenu.css feedback.css +CSS_FILES = font.css toastr.css main.css videolayout_default.css font-awesome.css jquery-impromptu.css modaldialog.css notice.css popup_menu.css login_menu.css popover.css jitsi_popover.css contact_list.css chat.css welcome_page.css settingsmenu.css feedback.css jquery.contextMenu.css DEPLOY_DIR = libs BROWSERIFY_FLAGS = -d OUTPUT_DIR = . diff --git a/app.js b/app.js index e8a02a2ee..523e55781 100644 --- a/app.js +++ b/app.js @@ -3,6 +3,7 @@ import "babel-polyfill"; import "jquery"; +import "jquery-contextmenu"; import "jquery-ui"; import "strophe"; import "strophe-disco"; diff --git a/css/jquery.contextMenu.css b/css/jquery.contextMenu.css new file mode 100644 index 000000000..45fd987c6 --- /dev/null +++ b/css/jquery.contextMenu.css @@ -0,0 +1,206 @@ +@charset "UTF-8"; +/*! + * jQuery contextMenu - Plugin for simple contextMenu handling + * + * Version: v2.1.1 + * + * Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF) + * Web: http://swisnl.github.io/jQuery-contextMenu/ + * + * Copyright (c) 2011-2016 SWIS BV and contributors + * + * Licensed under + * MIT License http://www.opensource.org/licenses/mit-license + * + * Date: 2016-02-28T09:53:18.890Z + */ +@font-face { + font-family: "context-menu-icons"; + font-style: normal; + font-weight: normal; + + src: url("font/context-menu-icons.eot?2qmzf"); + src: url("font/context-menu-icons.eot?2qmzf#iefix") format("embedded-opentype"), url("font/context-menu-icons.woff2?2qmzf") format("woff2"), url("font/context-menu-icons.woff?2qmzf") format("woff"), url("font/context-menu-icons.ttf?2qmzf") format("truetype"); +} + +.context-menu-icon:before { + position: absolute; + top: 50%; + left: 0; + width: 28px; + font-family: "context-menu-icons"; + font-size: 16px; + font-style: normal; + font-weight: normal; + line-height: 1; + color: #2980b9; + text-align: center; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + -o-transform: translateY(-50%); + transform: translateY(-50%); + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.context-menu-icon-add:before { + content: ""; +} + +.context-menu-icon-copy:before { + content: ""; +} + +.context-menu-icon-cut:before { + content: ""; +} + +.context-menu-icon-delete:before { + content: ""; +} + +.context-menu-icon-edit:before { + content: ""; +} + +.context-menu-icon-paste:before { + content: ""; +} + +.context-menu-icon-quit:before { + content: ""; +} + +.context-menu-icon.context-menu-hover:before { + color: #fff; +} + +.context-menu-list { + position: absolute; + display: inline-block; + min-width: 180px; + max-width: 360px; + padding: 4px 0; + margin: 5px; + font-family: inherit; + font-size: inherit; + list-style-type: none; + background: #fff; + border: 1px solid #bebebe; + border-radius: 3px; + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, .5); + box-shadow: 0 2px 5px rgba(0, 0, 0, .5); +} + +.context-menu-item { + position: relative; + padding: 3px 28px; + color: #2f2f2f; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: #fff; +} + +.context-menu-separator { + padding: 0; + margin: 5px 0; + border-bottom: 1px solid #e6e6e6; +} + +.context-menu-item > label > input, +.context-menu-item > label > textarea { + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.context-menu-item.context-menu-hover { + color: #fff; + cursor: pointer; + background-color: #2980b9; +} + +.context-menu-item.context-menu-disabled { + color: #626262; + background-color: #fff; +} + +.context-menu-item.context-menu-disabled { + color: #626262; +} + +.context-menu-input.context-menu-hover, +.context-menu-item.context-menu-disabled.context-menu-hover { + cursor: default; + background-color: #eee; +} + +.context-menu-submenu:after { + position: absolute; + top: 50%; + right: 8px; + z-index: 1; + width: 0; + height: 0; + content: ''; + border-color: transparent transparent transparent #2f2f2f; + border-style: solid; + border-width: 4px 0 4px 4px; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + -o-transform: translateY(-50%); + transform: translateY(-50%); +} + +/** + * Inputs + */ +.context-menu-item.context-menu-input { + padding: 5px 10px; +} + +/* vertically align inside labels */ +.context-menu-input > label > * { + vertical-align: top; +} + +/* position checkboxes and radios as icons */ +.context-menu-input > label > input[type="checkbox"], +.context-menu-input > label > input[type="radio"] { + position: relative; + top: 3px; +} + +.context-menu-input > label, +.context-menu-input > label > input[type="text"], +.context-menu-input > label > textarea, +.context-menu-input > label > select { + display: block; + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.context-menu-input > label > textarea { + height: 100px; +} + +.context-menu-item > .context-menu-list { + top: 5px; + /* re-positioned by js */ + right: -5px; + display: none; +} + +.context-menu-item.context-menu-visible > .context-menu-list { + display: block; +} + +.context-menu-accesskey { + text-decoration: underline; +} diff --git a/lang/main.json b/lang/main.json index a7561290b..68137827f 100644 --- a/lang/main.json +++ b/lang/main.json @@ -99,7 +99,8 @@ "mute": "Participant is muted", "kick": "Kick out", "muted": "Muted", - "domute": "Mute" + "domute": "Mute", + "flip": "Flip" }, "connectionindicator": diff --git a/modules/UI/videolayout/LargeVideo.js b/modules/UI/videolayout/LargeVideo.js index 351a2b0f3..69a613867 100644 --- a/modules/UI/videolayout/LargeVideo.js +++ b/modules/UI/videolayout/LargeVideo.js @@ -168,6 +168,7 @@ class VideoContainer extends LargeContainer { super(); this.stream = null; this.videoType = null; + this.localFlipX = true; this.isVisible = false; @@ -284,13 +285,25 @@ class VideoContainer extends LargeContainer { } stream.attach(this.$video[0]); - - let flipX = stream.isLocal() && !this.isScreenSharing(); + let flipX = stream.isLocal() && this.localFlipX; this.$video.css({ transform: flipX ? 'scaleX(-1)' : 'none' }); } + /** + * Changes the flipX state of the local video. + * @param val {boolean} true if flipped. + */ + setLocalFlipX(val) { + this.localFlipX = val; + if(!this.$video || !this.stream || !this.stream.isLocal()) + return; + this.$video.css({ + transform: this.localFlipX ? 'scaleX(-1)' : 'none' + }); + } + /** * Check if current video stream is screen sharing. * @returns {boolean} @@ -453,7 +466,7 @@ export default class LargeVideoManager { } else { preUpdate = Promise.resolve(); } - + preUpdate.then(() => { let {id, stream, videoType, resolve} = this.newStreamData; this.newStreamData = null; @@ -651,4 +664,12 @@ export default class LargeVideoManager { } }); } + + /** + * Changes the flipX state of the local video. + * @param val {boolean} true if flipped. + */ + onLocalFlipXChange(val) { + this.videoContainer.setLocalFlipX(val); + } } diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js index 09b10184e..76964f44d 100644 --- a/modules/UI/videolayout/LocalVideo.js +++ b/modules/UI/videolayout/LocalVideo.js @@ -4,16 +4,15 @@ import UIUtil from "../util/UIUtil"; import UIEvents from "../../../service/UI/UIEvents"; import SmallVideo from "./SmallVideo"; -var LargeVideo = require("./LargeVideo"); - const RTCUIUtils = JitsiMeetJS.util.RTCUIHelper; const TrackEvents = JitsiMeetJS.events.track; function LocalVideo(VideoLayout, emitter) { this.videoSpanId = "localVideoContainer"; this.container = $("#localVideoContainer").get(0); + this.localVideoId = null; this.bindHoverHandler(); - this.flipX = true; + this._buildContextMenu(); this.isLocal = true; this.emitter = emitter; Object.defineProperty(this, 'id', { @@ -165,9 +164,8 @@ LocalVideo.prototype.changeVideo = function (stream) { localVideoContainerSelector.off('click'); localVideoContainerSelector.on('click', localVideoClick); - this.flipX = stream.videoType != "desktop"; let localVideo = document.createElement('video'); - localVideo.id = 'localVideo_' + stream.getId(); + localVideo.id = this.localVideoId = 'localVideo_' + stream.getId(); RTCUIUtils.setAutoPlay(localVideo, true); RTCUIUtils.setVolume(localVideo, 0); @@ -182,9 +180,9 @@ LocalVideo.prototype.changeVideo = function (stream) { // onclick has to be used with Temasys plugin localVideo.onclick = localVideoClick; - if (this.flipX) { - $(localVideo).addClass("flipVideoX"); - } + let isVideo = stream.videoType != "desktop"; + this._enableDisableContextMenu(isVideo); + this.setFlipX(isVideo? APP.settings.getLocalFlipX() : false); // Attach WebRTC stream localVideo = stream.attach(localVideo); @@ -222,4 +220,54 @@ LocalVideo.prototype.setVisible = function(visible) { } }; +/** + * Sets the flipX state of the video. + * @param val {boolean} true for flipped otherwise false; + */ +LocalVideo.prototype.setFlipX = function (val) { + this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val); + if(!this.localVideoId) + return; + if(val) { + this.selectVideoElement().addClass("flipVideoX"); + } else { + this.selectVideoElement().removeClass("flipVideoX"); + } +}; + +/** + * Builds the context menu for the local video. + */ +LocalVideo.prototype._buildContextMenu = function () { + $.contextMenu({ + selector: '#' + this.videoSpanId, + zIndex: 10000, + items: { + flip: { + name: "Flip", + callback: () => { + let val = !APP.settings.getLocalFlipX(); + this.setFlipX(val); + APP.settings.setLocalFlipX(val); + } + } + }, + events: { + show : function(options){ + options.items.flip.name = + APP.translation.translateString("videothumbnail.flip"); + } + } + }); +}; + +/** + * Enables or disables the context menu for the local video. + * @param enable {boolean} true for enable, false for disable + */ +LocalVideo.prototype._enableDisableContextMenu = function (enable) { + if($('#' + this.videoSpanId).contextMenu) + $('#' + this.videoSpanId).contextMenu(enable); +}; + export default LocalVideo; diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index d3acdaa35..a750e1156 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -165,9 +165,6 @@ SmallVideo.createStreamElement = function (stream) { console.log("(TIME) Render " + type + ":\t", now); }; - - element.oncontextmenu = function () { return false; }; - return element; }; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 3bb02ca9d..5f2a785b4 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -33,6 +33,11 @@ var eventEmitter = null; */ var pinnedId = null; +/** + * flipX state of the localVideo + */ +let localFlipX = null; + /** * On contact list item clicked. */ @@ -92,6 +97,11 @@ let largeVideo; var VideoLayout = { init (emitter) { eventEmitter = emitter; + eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED, function (val) { + localFlipX = val; + if(largeVideo) + largeVideo.onLocalFlipXChange(val); + }); localVideoThumbnail = new LocalVideo(VideoLayout, emitter); // sets default video type of local video localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE); @@ -105,6 +115,9 @@ var VideoLayout = { initLargeVideo (isSideBarVisible) { largeVideo = new LargeVideoManager(); + if(localFlipX) { + largeVideo.onLocalFlipXChange(localFlipX); + } largeVideo.updateContainerSize(isSideBarVisible); AudioLevels.init(); }, @@ -1084,6 +1097,15 @@ var VideoLayout = { videoResolutionLabel.css({display: "block"}); else if (!isResolutionHD && videoResolutionLabel.is(":visible")) videoResolutionLabel.css({display: "none"}); + }, + + /** + * Sets the flipX state of the local video. + * @param {boolean} true for flipped otherwise false; + */ + setLocalFlipX: function (val) { + this.localFlipX = val; + } }; diff --git a/modules/settings/Settings.js b/modules/settings/Settings.js index 8ba6a7d30..ba5b01ff1 100644 --- a/modules/settings/Settings.js +++ b/modules/settings/Settings.js @@ -6,6 +6,7 @@ let language = null; let cameraDeviceId = ''; let micDeviceId = ''; let welcomePageDisabled = false; +let localFlipX = null; function supportsLocalStorage() { try { @@ -31,6 +32,7 @@ if (supportsLocalStorage()) { } email = UIUtil.unescapeHtml(window.localStorage.email || ''); + localFlipX = JSON.parse(window.localStorage.localFlipX || true); displayName = UIUtil.unescapeHtml(window.localStorage.displayname || ''); language = window.localStorage.language; cameraDeviceId = window.localStorage.cameraDeviceId || ''; @@ -87,6 +89,23 @@ export default { window.localStorage.language = lang; }, + /** + * Sets new flipX state of local video and saves it to the local storage. + * @param {string} val flipX state of local video + */ + setLocalFlipX: function (val) { + localFlipX = val; + window.localStorage.localFlipX = val; + }, + + /** + * Returns flipX state of local video. + * @returns {string} flipX + */ + getLocalFlipX: function () { + return localFlipX; + }, + /** * Get device id of the camera which is currently in use. * Empty string stands for default device. diff --git a/package.json b/package.json index 0fd97c61a..e71995d25 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "jquery": "~2.1.1", "jQuery-Impromptu": "git+https://github.com/trentrichardson/jQuery-Impromptu.git#v6.0.0", "lib-jitsi-meet": "jitsi/lib-jitsi-meet", + "jquery-contextmenu": "*", "jquery-ui": "^1.10.5", "jssha": "1.5.0", "retry": "0.6.1", @@ -98,6 +99,9 @@ "jQuery-Impromptu": { "depends": "jquery:jQuery" }, + "jquery-contextmenu": { + "depends": "jquery:jQuery" + }, "autosize": { "depends": "jquery:jQuery" } diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index 5da292339..2040f2bf0 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -71,5 +71,9 @@ export default { * Notifies interested listeners that the follow-me feature is enabled or * disabled. */ - FOLLOW_ME_ENABLED: "UI.follow_me_enabled" + FOLLOW_ME_ENABLED: "UI.follow_me_enabled", + /** + * Notifies that flipX property of the local video is changed. + */ + LOCAL_FLIPX_CHANGED: "UI.local_flipx_changed" };