diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html index 732f35244c3b09cc1d724c4d45fabc4e5f371d08..1cee0185f6857c851606ea9d67015362f3f6756f 100755 --- a/bigbluebutton-html5/client/main.html +++ b/bigbluebutton-html5/client/main.html @@ -42,6 +42,11 @@ display: none !important; } </style> + <script> + document.addEventListener('gesturestart', function (e) { + e.preventDefault(); +}); + </script> </head> <body style="background-color: #06172A"> <div id="app" role="document"></div> diff --git a/bigbluebutton-html5/imports/api/slides/server/methods.js b/bigbluebutton-html5/imports/api/slides/server/methods.js index 5d69c6254dea71a8ca690ed6bf6ff926f44a4132..87212015815073ad8b0e9a5dbb49fe1f5ad1b9b5 100644 --- a/bigbluebutton-html5/imports/api/slides/server/methods.js +++ b/bigbluebutton-html5/imports/api/slides/server/methods.js @@ -1,7 +1,9 @@ import { Meteor } from 'meteor/meteor'; import mapToAcl from '/imports/startup/mapToAcl'; import switchSlide from './methods/switchSlide'; +import zoomSlide from './methods/zoomSlide'; -Meteor.methods(mapToAcl(['methods.switchSlide'], { +Meteor.methods(mapToAcl(['methods.switchSlide', 'methods.zoomSlide'], { switchSlide, + zoomSlide, })); diff --git a/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js b/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js new file mode 100644 index 0000000000000000000000000000000000000000..c546a88af87a3abf577c76a23a9ee7111ab48091 --- /dev/null +++ b/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js @@ -0,0 +1,52 @@ +import Presentations from '/imports/api/presentations'; +import Slides from '/imports/api/slides'; +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import RedisPubSub from '/imports/startup/server/redis'; + +export default function switchSlide(credentials, slideNumber, podId, widthRatio, heightRatio, x, y) { + const REDIS_CONFIG = Meteor.settings.private.redis; + + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'ResizeAndMovePagePubMsg'; + + const { meetingId, requesterUserId, requesterToken } = credentials; + + check(meetingId, String); + check(requesterUserId, String); + check(requesterToken, String); + + const selector = { + meetingId, + podId, + current: true, + }; + const Presentation = Presentations.findOne(selector); + + if (!Presentation) { + throw new Meteor.Error('presentation-not-found', 'You need a presentation to be able to switch slides'); + } + + const Slide = Slides.findOne({ + meetingId, + podId, + presentationId: Presentation.id, + num: slideNumber, + }); + + if (!Slide) { + throw new Meteor.Error('slide-not-found', `Slide number ${slideNumber} not found in the current presentation`); + } + + const payload = { + podId, + presentationId: Presentation.id, + pageId: Slide.id, + xOffset: x, + yOffset: y, + widthRatio, + heightRatio, + }; + + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); +} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx index e94cbe9105ce059d6d55cbff2162ea44dcaad9ed..505a377415ec69913395b9dbef68b6cea7b33435 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx @@ -10,6 +10,8 @@ import PresentationOverlayContainer from './presentation-overlay/container'; import Slide from './slide/component'; import { styles } from './styles.scss'; +const HUNDRED_PERCENT = 100; +const MAX_PERCENT = 400; export default class PresentationArea extends Component { constructor() { @@ -19,9 +21,18 @@ export default class PresentationArea extends Component { presentationWidth: 0, presentationHeight: 0, showSlide: false, + zoom: 100, + touchZoom: false, + delta: { + x: 0, + y: 0, + }, }; this.getSvgRef = this.getSvgRef.bind(this); + this.zoomChanger = this.zoomChanger.bind(this); + this.touchUpdate = this.touchUpdate.bind(this); + this.pointUpdate = this.pointUpdate.bind(this); } componentDidMount() { @@ -53,7 +64,7 @@ export default class PresentationArea extends Component { // By default presentation sizes are equal to the sizes of the refPresentationArea // direct parent of the svg wrapper let { clientWidth, clientHeight } = refPresentationArea; - + // if a user is a presenter - this means there is a whiteboard toolbar on the right // and we have to get the width/height of the refWhiteboardArea // (inner hidden div with absolute position) @@ -70,6 +81,7 @@ export default class PresentationArea extends Component { getInitialPresentationSizes() { // determining the presentationWidth and presentationHeight (available space for the svg) // on the initial load + const presentationSizes = this.getPresentationSizesAvailable(); if (Object.keys(presentationSizes).length > 0) { // setting the state of the available space for the svg @@ -123,7 +135,30 @@ export default class PresentationArea extends Component { height: adjustedHeight, }; } - + zoomChanger(zoom) { + let newZoom = zoom; + const isDifferent = newZoom !== this.state.zoom; + + if (newZoom <= HUNDRED_PERCENT) { + newZoom = HUNDRED_PERCENT; + } else if (zoom >= MAX_PERCENT) { + newZoom = MAX_PERCENT; + } + if (isDifferent) this.setState({ zoom: newZoom }); + } + pointUpdate(pointX, pointY) { + this.setState({ + delta: { + x: pointX, + y: pointY, + }, + }); + } + touchUpdate(bool) { + this.setState({ + touchZoom: bool, + }); + } // renders the whole presentation area renderPresentationArea() { // sometimes tomcat publishes the slide url, but the actual file is not accessible (why?) @@ -229,10 +264,21 @@ export default class PresentationArea extends Component { return ( <PresentationOverlayContainer + podId={this.props.podId} + currentSlideNum={this.props.currentSlide.num} + slide={slideObj} whiteboardId={slideObj.id} slideWidth={width} slideHeight={height} + delta={this.state.delta} + viewBoxWidth={viewBoxWidth} + viewBoxHeight={viewBoxHeight} + zoom={this.state.zoom} + zoomChanger={this.zoomChanger} + adjustedSizes={adjustedSizes} getSvgRef={this.getSvgRef} + presentationSize={this.getPresentationSizesAvailable()} + touchZoom={this.state.touchZoom} > <WhiteboardOverlayContainer getSvgRef={this.getSvgRef} @@ -241,10 +287,14 @@ export default class PresentationArea extends Component { slideHeight={height} viewBoxX={x} viewBoxY={y} + pointChanger={this.pointUpdate} viewBoxWidth={viewBoxWidth} viewBoxHeight={viewBoxHeight} physicalSlideWidth={(adjustedSizes.width / slideObj.widthRatio) * 100} physicalSlideHeight={(adjustedSizes.height / slideObj.heightRatio) * 100} + zoom={this.state.zoom} + zoomChanger={this.zoomChanger} + touchUpdate={this.touchUpdate} /> </PresentationOverlayContainer> ); @@ -260,6 +310,8 @@ export default class PresentationArea extends Component { podId={this.props.podId} currentSlideNum={this.props.currentSlide.num} presentationId={this.props.currentSlide.presentationId} + zoom={this.state.zoom} + zoomChanger={this.zoomChanger} /> ); } diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx index 176cb950a8972c0cdafbefd9ce5e3e410a2d0458..f55c1c94b37d098b7b43679ecc9d0e3e6e4829d4 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/component.jsx @@ -1,7 +1,13 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; - +import SlideCalcUtil from '/imports/utils/slideCalcUtils'; +// After lots of trial and error on why synching doesn't work properly, I found I had to +// multiply the coordinates by 2. There's something I don't understand probably on the +// canvas coordinate system. (ralam feb 22, 2012) +const MYSTERY_NUM = 2; const CURSOR_INTERVAL = 16; +const HUNDRED_PERCENT = 100; +const MAX_PERCENT = 400; export default class PresentationOverlay extends Component { constructor(props) { @@ -17,6 +23,9 @@ export default class PresentationOverlay extends Component { // id of the setInterval() this.intervalId = 0; + this.state = { + zoom: props.zoom, + }; // Mobile Firefox has a bug where e.preventDefault on touchstart doesn't prevent // onmousedown from triggering right after. Thus we have to track it manually. @@ -35,20 +44,199 @@ export default class PresentationOverlay extends Component { this.mouseOutHandler = this.mouseOutHandler.bind(this); this.getTransformedSvgPoint = this.getTransformedSvgPoint.bind(this); this.svgCoordinateToPercentages = this.svgCoordinateToPercentages.bind(this); + this.mouseZoomHandler = this.mouseZoomHandler.bind(this); + this.zoomCalculation = this.zoomCalculation.bind(this); + this.doZoomCall = this.doZoomCall.bind(this); + this.tapHandler = this.tapHandler.bind(this); + this.zoomCall = this.zoomCall.bind(this); + + this.panZoom = this.panZoom.bind(this); + this.pinchZoom = this.pinchZoom.bind(this); + this.toolbarZoom = this.toolbarZoom.bind(this); + + this.isPortraitDoc = this.isPortraitDoc.bind(this); + this.doWidthBoundsDetection = this.doWidthBoundsDetection.bind(this); + this.doHeightBoundsDetection = this.doHeightBoundsDetection.bind(this); + this.calcViewedRegion = this.calcViewedRegion.bind(this); + this.onZoom = this.onZoom.bind(this); + + const { + viewBoxWidth, + viewBoxHeight, + slideWidth, + slideHeight, + slide, + } = props; + + this.fitToPage = false; + + this.viewportW = slideWidth; + this.viewportH = slideHeight; + + this.viewedRegionX = slide.xOffset; + this.viewedRegionY = slide.yOffset; + this.viewedRegionW = (viewBoxWidth / slideWidth) * 100; + this.viewedRegionH = (viewBoxHeight / slideHeight) * 100; + + this.pageOrigW = slideWidth; + this.pageOrigH = slideHeight; + + this.calcPageW = this.pageOrigW / (this.viewedRegionW / HUNDRED_PERCENT); + this.calcPageH = this.pageOrigH / (this.viewedRegionH / HUNDRED_PERCENT); + this.calcPageX = (this.viewedRegionX / HUNDRED_PERCENT) * this.calcPageW; + this.calcPageY = (this.viewedRegionY / HUNDRED_PERCENT) * this.calcPageH; + + this.tapedTwice = false; + this.touches = []; } - // transforms the coordinate from window coordinate system - // to the main svg coordinate system + + componentDidMount() { + const { + viewBoxWidth, + slideWidth, + zoomChanger, + } = this.props; + + const realZoom = (viewBoxWidth / slideWidth) * 100; + + const zoomPercentage = (Math.round((100 / realZoom) * 100)); + const roundedUpToFive = Math.round(zoomPercentage / 5) * 5; + zoomChanger(roundedUpToFive); + } + + componentDidUpdate(prevProps) { + const { + zoom, + delta, + touchZoom, + } = this.props; + const isDifferent = zoom !== this.state.zoom && !touchZoom; + const moveSLide = ((delta.x !== prevProps.delta.x) + || (delta.y !== prevProps.delta.y)) && !isDifferent; + const isTouchZoom = zoom !== this.state.zoom && touchZoom; + if (moveSLide) { + this.panZoom(); + } + + if (isTouchZoom) { + this.pinchZoom(); + } + + if (isDifferent) { + this.toolbarZoom(); + } + } + + onZoom(zoomValue, mouseX, mouseY) { + let absXcoordInPage = (Math.abs(this.calcPageX) * MYSTERY_NUM) + mouseX; + let absYcoordInPage = (Math.abs(this.calcPageY) * MYSTERY_NUM) + mouseY; + + const relXcoordInPage = absXcoordInPage / this.calcPageW; + const relYcoordInPage = absYcoordInPage / this.calcPageH; + + if (this.isPortraitDoc() && this.fitToPage) { + this.calcPageH = (this.viewportH * zoomValue) / HUNDRED_PERCENT; + this.calcPageW = (this.pageOrigW / this.pageOrigH) * this.calcPageH; + } else if (!this.isPortraitDoc() && this.fitToPage) { + this.calcPageW = (this.viewportW * zoomValue) / HUNDRED_PERCENT; + this.calcPageH = (this.viewportH * zoomValue) / HUNDRED_PERCENT; + } else { + this.calcPageW = (this.viewportW * zoomValue) / HUNDRED_PERCENT; + this.calcPageH = (this.calcPageW / this.pageOrigW) * this.pageOrigH; + } + + absXcoordInPage = relXcoordInPage * this.calcPageW; + absYcoordInPage = relYcoordInPage * this.calcPageH; + + this.calcPageX = -((absXcoordInPage - mouseX) / MYSTERY_NUM); + this.calcPageY = -((absYcoordInPage - mouseY) / MYSTERY_NUM); + + this.doWidthBoundsDetection(); + this.doHeightBoundsDetection(); + + this.calcViewedRegion(); + } + getTransformedSvgPoint(clientX, clientY) { const svgObject = this.props.getSvgRef(); const screenPoint = svgObject.createSVGPoint(); screenPoint.x = clientX; screenPoint.y = clientY; - // transform a screen point to svg point const CTM = svgObject.getScreenCTM(); + return screenPoint.matrixTransform(CTM.inverse()); } + panZoom() { + const { + delta, + } = this.props; + this.deltaX = delta.x; + this.deltaY = delta.y; + this.calcPageX += delta.x * -1; + this.calcPageY += delta.y * -1; + this.doHeightBoundsDetection(); + this.doWidthBoundsDetection(); + this.calcViewedRegion(); + this.zoomCall( + this.state.zoom, + this.viewedRegionW, + this.viewedRegionH, + this.viewedRegionX, + this.viewedRegionY, + ); + } + + pinchZoom() { + const { + zoom, + } = this.props; + const posX = this.touches[0].clientX; + const posY = this.touches[0].clientY; + this.doZoomCall(zoom, posX, posY); + } + + toolbarZoom() { + const { + getSvgRef, + zoom, + } = this.props; + const svgRect = getSvgRef().getBoundingClientRect(); + const svgCenterX = svgRect.left + (svgRect.width / 2); + const svgCenterY = svgRect.top + (svgRect.height / 2); + this.doZoomCall(zoom, svgCenterX, svgCenterY); + } + + isPortraitDoc() { + return this.pageOrigH > this.pageOrigW; + } + + doWidthBoundsDetection() { + const verifyPositionToBound = (this.calcPageW + (this.calcPageX * MYSTERY_NUM)); + if (this.calcPageX >= 0) { + this.calcPageX = 0; + } else if (verifyPositionToBound < this.viewportW) { + this.calcPageX = (this.viewportW - this.calcPageW) / MYSTERY_NUM; + } + } + + doHeightBoundsDetection() { + const verifyPositionToBound = (this.calcPageH + (this.calcPageY * MYSTERY_NUM)); + if (this.calcPageY >= 0) { + this.calcPageY = 0; + } else if (verifyPositionToBound < this.viewportH) { + this.calcPageY = (this.viewportH - this.calcPageH) / MYSTERY_NUM; + } + } + + calcViewedRegion() { + this.viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(this.viewportW, this.calcPageW); + this.viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(this.viewportH, this.calcPageH); + this.viewedRegionX = SlideCalcUtil.calcViewedRegionX(this.calcPageX, this.calcPageW); + this.viewedRegionY = SlideCalcUtil.calcViewedRegionY(this.calcPageY, this.calcPageH); + } + checkCursor() { // check if the cursor hasn't moved since last check if (this.lastSentClientX !== this.currentClientX @@ -56,6 +244,7 @@ export default class PresentationOverlay extends Component { const { currentClientX, currentClientY } = this; // retrieving a transformed coordinate let transformedSvgPoint = this.getTransformedSvgPoint(currentClientX, currentClientY); + // determining the cursor's coordinates as percentages from the slide's width/height transformedSvgPoint = this.svgCoordinateToPercentages(transformedSvgPoint); // updating last sent raw coordinates @@ -81,11 +270,93 @@ export default class PresentationOverlay extends Component { return point; } + zoomCalculation(zoom, mouseX, mouseY) { + const svgPosition = this.getTransformedSvgPoint(mouseX, mouseY); + this.onZoom(zoom, svgPosition.x, svgPosition.y); - handleTouchStart(event) { - // to prevent default behavior (scrolling) on devices (in Safari), when you draw a text box + return { + viewedRegionW: this.viewedRegionW, + viewedRegionH: this.viewedRegionH, + viewedRegionX: this.viewedRegionX, + viewedRegionY: this.viewedRegionY, + }; + } + + zoomCall(zoom, w, h, x, y) { + const { + zoomSlide, + podId, + currentSlideNum, + } = this.props; + zoomSlide(currentSlideNum, podId, w, h, x, y); + this.setState({ zoom }); + } + + doZoomCall(zoom, mouseX, mouseY) { + const zoomData = this.zoomCalculation(zoom, mouseX, mouseY); + + const { + viewedRegionW, + viewedRegionH, + viewedRegionX, + viewedRegionY, + } = zoomData; + + this.zoomCall(zoom, viewedRegionW, viewedRegionH, viewedRegionX, viewedRegionY); + this.props.zoomChanger(zoom); + } + + mouseZoomHandler(e) { + const { + zoom, + whiteboardId, + updateCursor, + } = this.props; + + let newZoom = zoom; + if (e.deltaY < 0) { + newZoom += 5; + } + if (e.deltaY > 0) { + newZoom -= 5; + } + if (newZoom <= HUNDRED_PERCENT) { + newZoom = HUNDRED_PERCENT; + } else if (newZoom >= MAX_PERCENT) { + newZoom = MAX_PERCENT; + } + + const mouseX = e.clientX; + const mouseY = e.clientY; + const svgPosition = this.getTransformedSvgPoint(mouseX, mouseY); + const svgPercentage = this.svgCoordinateToPercentages(svgPosition); + + this.doZoomCall(newZoom, mouseX, mouseY); + updateCursor({ + xPercent: svgPercentage.x, + yPercent: svgPercentage.y, + whiteboardId, + }); + } + tapHandler(event) { + if (event.touches.length === 2) return; + if (!this.tapedTwice) { + this.tapedTwice = true; + setTimeout(() => this.tapedTwice = false, 300); + return; + } event.preventDefault(); + const sizeDefault = this.state.zoom === HUNDRED_PERCENT; + + if (sizeDefault) { + this.doZoomCall(200, this.currentClientX, this.currentClientY); + return; + } + this.doZoomCall(HUNDRED_PERCENT, 0, 0); + } + handleTouchStart(event) { + // to prevent default behavior (scrolling) on devices (in Safari), when you draw a text box window.addEventListener('touchend', this.handleTouchEnd, { passive: false }); window.addEventListener('touchmove', this.handleTouchMove, { passive: false }); window.addEventListener('touchcancel', this.handleTouchCancel, true); @@ -95,9 +366,13 @@ export default class PresentationOverlay extends Component { const { clientX, clientY } = event.changedTouches[0]; this.currentClientX = clientX; this.currentClientY = clientY; + if (event.touches.length === 2) { + this.touches = [...event.touches]; + } const intervalId = setInterval(this.checkCursor, CURSOR_INTERVAL); this.intervalId = intervalId; + this.tapHandler(event); } handleTouchMove(event) { @@ -111,7 +386,7 @@ export default class PresentationOverlay extends Component { handleTouchEnd(event) { event.preventDefault(); - + // touch ended, removing the interval clearInterval(this.intervalId); this.intervalId = 0; @@ -153,7 +428,6 @@ export default class PresentationOverlay extends Component { if (this.touchStarted) { return; } - // for the case where you change settings in one of the lists (which are displayed on the slide) // the mouse starts pointing to the slide right away and mouseEnter doesn't fire // so we call it manually here @@ -201,6 +475,8 @@ export default class PresentationOverlay extends Component { onMouseOut={this.mouseOutHandler} onMouseEnter={this.mouseEnterHandler} onMouseMove={this.mouseMoveHandler} + onWheel={this.mouseZoomHandler} + onBlur={() => {}} style={{ width: '100%', height: '100%', touchAction: 'none' }} > {this.props.children} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/container.jsx index 89f5e39c6f79db9ed7cca302a0edb8b629de28ac..6665175b27f7fc90446e68195f5a6a973fdaea27 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-overlay/container.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withTracker } from 'meteor/react-meteor-data'; import PresentationOverlayService from './service'; +import PresentationToolbarService from '../presentation-toolbar/service'; import PresentationOverlay from './component'; const PresentationOverlayContainer = ({ children, ...rest }) => ( @@ -10,8 +11,12 @@ const PresentationOverlayContainer = ({ children, ...rest }) => ( </PresentationOverlay> ); -export default withTracker(() => ({ +export default withTracker(({ podId, currentSlideNum, slide }) => ({ + slide, + podId, + currentSlideNum, updateCursor: PresentationOverlayService.updateCursor, + zoomSlide: PresentationToolbarService.zoomSlide, }))(PresentationOverlayContainer); PresentationOverlayContainer.propTypes = { diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx index 170b194e34ada802b9880e62bf11f4c7a29f39f0..1cf9ff0d54f0a694464728aff7e4b65d81d4aaf7 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx @@ -1,9 +1,15 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import browser from 'browser-detect'; import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component'; import Button from '/imports/ui/components/button/component'; import { styles } from './styles.scss'; +import ZoomTool from './zoom-tool/component'; + +const STEP = 5; +const HUNDRED_PERCENT = 100; +const MAX_PERCENT = 400; const intlMessages = defineMessages({ previousSlideLabel: { @@ -125,6 +131,8 @@ class PresentationToolbar extends Component { this.state = { sliderValue: 100 }; this.handleValuesChange = this.handleValuesChange.bind(this); this.handleSkipToSlideChange = this.handleSkipToSlideChange.bind(this); + this.change = this.change.bind(this); + this.setInt = 0; } handleSkipToSlideChange(event) { @@ -133,7 +141,10 @@ class PresentationToolbar extends Component { } handleValuesChange(event) { - this.setState({ sliderValue: event.target.value }); + this.setState( + { sliderValue: event.target.value }, + () => this.handleZoom(this.state.sliderValue), + ); } fitToWidthClickHandler() { @@ -147,6 +158,11 @@ class PresentationToolbar extends Component { fitToScreenValue: 'not_implemented_yet', }); } + + change(value) { + this.props.zoomChanger(value); + } + renderSkipSlideOpts(numberOfSlides) { // Fill drop down menu with all the slides in presentation const { intl } = this.props; @@ -172,51 +188,71 @@ class PresentationToolbar extends Component { numberOfSlides, actions, intl, + zoom, } = this.props; + const BROWSER_RESULTS = browser(); + const isMobileBrowser = BROWSER_RESULTS.mobile || + BROWSER_RESULTS.os.includes('Android'); return ( <div id="presentationToolbarWrapper" className={styles.presentationToolbarWrapper}> {PresentationToolbar.renderAriaLabelsDescs()} - <Button - role="button" - aria-labelledby="prevSlideLabel" - aria-describedby="prevSlideDesc" - disabled={!(currentSlideNum > 1)} - color="default" - icon="left_arrow" - size="md" - onClick={actions.previousSlideHandler} - label={intl.formatMessage(intlMessages.previousSlideLabel)} - hideLabel - className={styles.prevSlide} - /> - <select - // <select> has an implicit role of listbox, no need to define role="listbox" explicitly - id="skipSlide" - aria-labelledby="skipSlideLabel" - aria-describedby="skipSlideDesc" - aria-live="polite" - aria-relevant="all" - value={currentSlideNum} - onChange={actions.skipToSlideHandler} - className={styles.skipSlideSelect} - > - {this.renderSkipSlideOpts(numberOfSlides)} - </select> - <Button - role="button" - aria-labelledby="nextSlideLabel" - aria-describedby="nextSlideDesc" - disabled={!(currentSlideNum < numberOfSlides)} - color="default" - icon="right_arrow" - size="md" - onClick={actions.nextSlideHandler} - label={intl.formatMessage(intlMessages.nextSlideLabel)} - hideLabel - className={styles.skipSlide} - /> - + { + <span className={styles.presentationControls}> + <Button + role="button" + aria-labelledby="prevSlideLabel" + aria-describedby="prevSlideDesc" + disabled={!(currentSlideNum > 1)} + color="default" + icon="left_arrow" + size="md" + onClick={actions.previousSlideHandler} + label={intl.formatMessage(intlMessages.previousSlideLabel)} + hideLabel + className={styles.prevSlide} + /> + <select + // <select> has an implicit role of listbox, no need to define role="listbox" explicitly + id="skipSlide" + aria-labelledby="skipSlideLabel" + aria-describedby="skipSlideDesc" + aria-live="polite" + aria-relevant="all" + value={currentSlideNum} + onChange={actions.skipToSlideHandler} + className={styles.skipSlideSelect} + > + {this.renderSkipSlideOpts(numberOfSlides)} + </select> + <Button + role="button" + aria-labelledby="nextSlideLabel" + aria-describedby="nextSlideDesc" + disabled={!(currentSlideNum < numberOfSlides)} + color="default" + icon="right_arrow" + size="md" + onClick={actions.nextSlideHandler} + label={intl.formatMessage(intlMessages.nextSlideLabel)} + hideLabel + className={styles.skipSlide} + /> + </span> + } + { + !isMobileBrowser ? + <span className={styles.zoomWrapper}> + <ZoomTool + value={zoom} + change={this.change} + minBound={HUNDRED_PERCENT} + maxBound={MAX_PERCENT} + step={STEP} + /> + </span> + : null + } {/* Fit to width button <Button role="button" @@ -243,29 +279,6 @@ class PresentationToolbar extends Component { label={'Fit to Screen'} hideLabel={true} /> */} - {/* Zoom slider - <div - className={classNames(styles.zoomWrapper, { [styles.zoomWrapperNoBorder]: true })} - > - <div className={styles.zoomMinMax}> 100% </div> - <input - role="slider" - aria-labelledby="zoomLabel" - aria-describedby="zoomDesc" - aria-valuemax="400" - aria-valuemin="100" - aria-valuenow={this.state.sliderValue} - value={this.state.sliderValue} - step="5" - type="range" - min="100" - max="400" - onChange={this.handleValuesChange} - onInput={this.handleValuesChange} - className={styles.zoomSlider} - /> - <div className={styles.zoomMinMax}> 400% </div> - </div> */} </div> ); } diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx index da6fd3d779d0d05c0aac31352d6768c75ffa6b6e..66767f2f5562d38d22ade1647794169e19b34e51 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx @@ -1,10 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withTracker } from 'meteor/react-meteor-data'; +import PresentationService from '/imports/ui/components/presentation/service'; import PresentationToolbarService from './service'; import PresentationToolbar from './component'; -import PresentationService from '/imports/ui/components/presentation/service'; const PresentationToolbarContainer = (props) => { const { @@ -12,15 +12,21 @@ const PresentationToolbarContainer = (props) => { userIsPresenter, numberOfSlides, actions, + zoom, + zoomChanger, } = props; if (userIsPresenter) { // Only show controls if user is presenter return ( <PresentationToolbar - currentSlideNum={currentSlideNum} - numberOfSlides={numberOfSlides} - actions={actions} + {...{ + currentSlideNum, + numberOfSlides, + actions, + zoom, + zoomChanger, + }} /> ); } @@ -38,6 +44,8 @@ export default withTracker((params) => { return { userIsPresenter: PresentationService.isPresenter(podId), numberOfSlides, + zoom: params.zoom, + zoomChanger: params.zoomChanger, actions: { nextSlideHandler: () => PresentationToolbarService.nextSlide(params.currentSlideNum, numberOfSlides, podId), @@ -45,6 +53,8 @@ export default withTracker((params) => { PresentationToolbarService.previousSlide(params.currentSlideNum, podId), skipToSlideHandler: requestedSlideNum => PresentationToolbarService.skipToSlide(requestedSlideNum, podId), + zoomSlideHandler: value => + PresentationToolbarService.zoomSlide(params.currentSlideNum, podId, value), }, }; })(PresentationToolbarContainer); @@ -52,6 +62,8 @@ export default withTracker((params) => { PresentationToolbarContainer.propTypes = { // Number of current slide being displayed currentSlideNum: PropTypes.number.isRequired, + zoom: PropTypes.number.isRequired, + zoomChanger: PropTypes.func.isRequired, // Is the user a presenter userIsPresenter: PropTypes.bool.isRequired, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js index 2b16bd03c50fdebd45884f41e9ddaa78a17b94ec..a9c98a1c1ea011ea430ce08fc74f1b263a258208 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js @@ -40,6 +40,11 @@ const nextSlide = (currentSlideNum, numberOfSlides, podId) => { } }; +const zoomSlide = (currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset) => { + makeCall('zoomSlide', currentSlideNum, podId, widthRatio, heightRatio, xOffset, yOffset); +}; + + const skipToSlide = (requestedSlideNum, podId) => { makeCall('switchSlide', requestedSlideNum, podId); }; @@ -49,4 +54,5 @@ export default { nextSlide, previousSlide, skipToSlide, + zoomSlide, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss index 63b7cadc87464cc58a02465a02e1429fd0f99b09..b5ba291587c5c9be3bc9c37b6863bc870271521e 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss @@ -6,18 +6,25 @@ $controls-background: $color-white !default; $toolbar-button-border-radius: 5px; .presentationToolbarWrapper, +.presentationControls, .zoomWrapper { order: 2; display: flex; flex-direction: row; align-items: center; + margin-left: .5rem; + margin-right: .5rem; + border-radius: $toolbar-button-border-radius; +} + +.presentationControls, +.zoomWrapper { + box-shadow: 0 0 10px -2px rgba(0, 0, 0, .25); } .presentationToolbarWrapper { position: absolute; bottom: .8rem; - box-shadow: 0 0 10px -2px rgba(0, 0, 0, .25); - border-radius: $toolbar-button-border-radius; align-self: center; justify-content: center; z-index: 1; @@ -27,6 +34,7 @@ $toolbar-button-border-radius: 5px; transform-origin: bottom; } + button, select, > div { @@ -36,7 +44,6 @@ $toolbar-button-border-radius: 5px; border-bottom: 0; border-left: 0; border-radius: 0; - box-shadow: none; height: 2.25rem; box-shadow: none !important; border: 0; @@ -61,10 +68,12 @@ $toolbar-button-border-radius: 5px; } .zoomWrapper { - border-radius: 0 5px 5px 0; + // border-radius: 0 5px 5px 0; justify-content: space-between; + // flex-direction: column; width: 11.5%; min-width: 175px; + background-color: $color-white; } .zoomWrapperNoBorder { @@ -95,3 +104,14 @@ $toolbar-button-border-radius: 5px; .zoomMinMax { font-weight: normal; } +.zoomPercentageDisplay { + width: 100%; + height: 2.25rem; + text-align: center; + color: black; + border-left: $border-size solid $color-gray-lighter !important; + border-right: $border-size solid $color-gray-lighter !important; + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0e976e08d004a7f378fde855070ecf1ec1d3b475 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/zoom-tool/component.jsx @@ -0,0 +1,206 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import Button from '/imports/ui/components/button/component'; +import { styles } from '../styles.scss'; + +const DELAY_MILLISECONDS = 200; +const STEP_TIME = 100; + +export default class ZoomTool extends Component { + static renderAriaLabelsDescs() { + return ( + <div hidden key="hidden-div"> + {/* Zoom in button aria */} + <div id="zoomInLabel"> + <FormattedMessage + id="app.presentation.presentationToolbar.zoomInLabel" + description="Aria label for when switching to previous slide" + defaultMessage="Previous slide" + /> + </div> + <div id="zoomInDesc"> + <FormattedMessage + id="app.presentation.presentationToolbar.zoomInDesc" + description="Aria description for when switching to previous slide" + defaultMessage="Change the presentation to the previous slide" + /> + </div> + {/* Zoom out button aria */} + <div id="zoomOutLabel"> + <FormattedMessage + id="app.presentation.presentationToolbar.zoomOutLabel" + description="Aria label for when switching to next slide" + defaultMessage="Next slide" + /> + </div> + <div id="zoomOutDesc"> + <FormattedMessage + id="app.presentation.presentationToolbar.zoomOutDesc" + description="Aria description for when switching to next slide" + defaultMessage="Change the presentation to the next slide" + /> + </div> + {/* Zoom indicator aria */} + <div id="zoomIndicator"> + <FormattedMessage + id="app.presentation.presentationToolbar.zoomIndicator" + description="Aria label for when switching to a specific slide" + defaultMessage="Skip slide" + /> + </div> + </div> + ); + } + constructor(props) { + super(props); + this.increment = this.increment.bind(this); + this.decrement = this.decrement.bind(this); + this.mouseDownHandler = this.mouseDownHandler.bind(this); + this.mouseUpHandler = this.mouseUpHandler.bind(this); + this.execInterval = this.execInterval.bind(this); + this.onChanger = this.onChanger.bind(this); + this.setInt = 0; + this.state = { + value: props.value, + mouseHolding: false, + }; + } + componentDidUpdate() { + const isDifferent = this.props.value !== this.state.value; + if (isDifferent) this.onChanger(this.props.value); + } + + onChanger(value) { + const { + maxBound, + minBound, + change, + } = this.props; + let newValue = value; + const isDifferent = newValue !== this.state.value; + + if (newValue <= minBound) { + newValue = minBound; + } else if (newValue >= maxBound) { + newValue = maxBound; + } + + const propsIsDifferente = this.props.value !== newValue; + if (isDifferent && propsIsDifferente) { + this.setState({ value: newValue }, () => { + change(newValue); + }); + } + if (isDifferent && !propsIsDifferente) this.setState({ value: newValue }); + } + + increment() { + const { + step, + } = this.props; + const increaseZoom = this.state.value + step; + this.onChanger(increaseZoom); + } + decrement() { + const { + step, + } = this.props; + const decreaseZoom = this.state.value - step; + this.onChanger(decreaseZoom); + } + + execInterval(inc) { + const exec = inc ? this.increment : this.decrement; + + const interval = () => { + clearInterval(this.setInt); + this.setInt = setInterval(exec, STEP_TIME); + }; + + setTimeout(() => { + if (this.state.mouseHolding) { + interval(); + } + }, DELAY_MILLISECONDS); + } + + mouseDownHandler(bool) { + this.setState({ + ...this.state, + mouseHolding: true, + }, () => { + this.execInterval(bool); + }); + } + + mouseUpHandler() { + this.setState({ + ...this.state, + mouseHolding: false, + }, () => clearInterval(this.setInt)); + } + + render() { + const { + value, + minBound, + maxBound, + } = this.props; + return ( + [ + ZoomTool.renderAriaLabelsDescs(), + (<Button + key="zoom-tool-1" + aria-labelledby="zoomInLabel" + aria-describedby="zoomInDesc" + role="button" + label="-" + icon="minus" + onClick={() => this.decrement()} + disabled={(value <= minBound)} + onMouseDown={() => this.mouseDownHandler(false)} + onMouseUp={this.mouseUpHandler} + onMouseLeave={this.mouseUpHandler} + className={styles.prevSlide} + hideLabel + />), + ( + <span + key="zoom-tool-2" + aria-labelledby="prevSlideLabel" + aria-describedby={this.state.value} + className={styles.zoomPercentageDisplay} + > + {`${this.state.value}%`} + </span> + ), + (<Button + key="zoom-tool-3" + aria-labelledby="zoomOutLabel" + aria-describedby="zoomOutDesc" + role="button" + label="+" + icon="plus" + onClick={() => this.increment()} + disabled={(value >= maxBound)} + onMouseDown={() => this.mouseDownHandler(true)} + onMouseUp={this.mouseUpHandler} + onMouseLeave={this.mouseUpHandler} + className={styles.skipSlide} + hideLabel + />), + ] + ); + } +} + +const propTypes = { + value: PropTypes.number.isRequired, + change: PropTypes.func.isRequired, + minBound: PropTypes.number.isRequired, + maxBound: PropTypes.number.isRequired, + step: PropTypes.number.isRequired, +}; + +ZoomTool.propTypes = propTypes; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pan-zoom-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pan-zoom-draw-listener/component.jsx index 6a618a25a41f6776bc2ff3ef1a9bde5be584f103..69fb85479480a87d56200a16b5811d0636c2aca1 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pan-zoom-draw-listener/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pan-zoom-draw-listener/component.jsx @@ -2,27 +2,182 @@ import React from 'react'; // import PropTypes from 'prop-types'; export default class PanZoomDrawListener extends React.Component { + static calculateDistance({ x: x1, y: y1 }, { x: x2, y: y2 }) { + return Math.sqrt(((x1 - x2) ** 2) + ((y1 - y2) ** 2)); + } + constructor(props) { super(props); - this.mouseDownHandler = this.mouseDownHandler.bind(this); this.mouseMoveHandler = this.mouseMoveHandler.bind(this); this.mouseUpHandler = this.mouseUpHandler.bind(this); + + this.touchMoveHandler = this.touchMoveHandler.bind(this); + this.touchStartHandler = this.touchStartHandler.bind(this); + this.touchEndHandler = this.touchEndHandler.bind(this); + + this.pinchStartHandler = this.pinchStartHandler.bind(this); + this.pinchMoveHandler = this.pinchMoveHandler.bind(this); + this.pinchEndHandler = this.pinchEndHandler.bind(this); + + this.panMoveHandler = this.panMoveHandler.bind(this); + this.updateTouchPoint = this.updateTouchPoint.bind(this); + this.currentMouseX = 0; + this.currentMouseY = 0; + this.mouseDeltaX = 0; + this.mouseDeltaY = 0; + this.initialTouches = []; + this.state = { + pressed: false, + }; + this.pinchGesture = false; + this.prevDiff = -1; + } + + + mouseDownHandler(event) { + const isLeftClick = event.button === 0; + if (isLeftClick) { + this.setState({ + pressed: true, + }); + } + } + + updateTouchPoint(event) { + if (event.touches.length <= 0) return; + const { clientX, clientY } = event.touches[0]; + this.currentMouseX = clientX; + this.currentMouseY = clientY; + } + + mouseMoveHandler(event) { + if (this.state.pressed) { + this.mouseDeltaX = this.currentMouseX - event.clientX; + this.mouseDeltaY = this.currentMouseY - event.clientY; + this.props.pointChanger(this.mouseDeltaX, this.mouseDeltaY); + } + this.currentMouseX = event.clientX; + this.currentMouseY = event.clientY; } - mouseDownHandler() { - this.dummyValue = ''; + touchStartHandler(event) { + this.updateTouchPoint(event); + const numberTouches = event.touches.length; + if (numberTouches === 2) { + this.pinchGesture = true; + this.pinchStartHandler(event); + } + if (numberTouches === 1) { + this.pinchGesture = false; + this.updateTouchPoint(event); + } } - mouseMoveHandler() { - this.dummyValue = ''; + touchMoveHandler(event) { + if (this.pinchGesture) { + this.pinchMoveHandler(event); + } + if (!this.pinchGesture) { + this.panMoveHandler(event); + } } - mouseUpHandler() { - this.dummyValue = ''; + touchEndHandler(event) { + if (this.pinchGesture) { + this.pinchEndHandler(event); + this.pinchGesture = false; + } else if (!this.pinchGesture) { + this.updateTouchPoint(event); + } + } + + panMoveHandler(event) { + if (this.pinchGesture) return; + const { clientX, clientY } = event.changedTouches[0]; + this.mouseDeltaX = this.currentMouseX - clientX; + this.mouseDeltaY = this.currentMouseY - clientY; + this.props.pointChanger(this.mouseDeltaX, this.mouseDeltaY); + this.currentMouseX = clientX; + this.currentMouseY = clientY; + } + + + pinchStartHandler(event) { + if (!this.pinchGesture) return; + this.initialTouches = [...event.touches]; + let inputs = []; + this.initialTouches.forEach((ev) => { + inputs.push({ + x: ev.clientX, + y: ev.clientY, + }); + }); + this.prevDiff = PanZoomDrawListener.calculateDistance(...inputs); + inputs = []; + this.updateTouchPoint(event); + this.props.touchUpdate(true); + } + + pinchMoveHandler(event) { + if (!this.pinchGesture) return; + if (event.touches.length < 2) return; + let inputs = []; + [...event.touches].forEach((ev) => { + inputs.push({ + x: ev.clientX, + y: ev.clientY, + }); + }); + const currDiff = PanZoomDrawListener.calculateDistance(...inputs); + inputs = []; + if (currDiff > 0) { + if (currDiff > this.prevDiff) { + this.props.zoomChanger(this.props.zoom + (100 * ((currDiff - this.prevDiff) / 100))); + } + if (currDiff < this.prevDiff) { + this.props.zoomChanger(this.props.zoom - (100 * ((this.prevDiff - currDiff) / 100))); + } + } + this.prevDiff = currDiff; + } + + pinchEndHandler(event) { + this.initialTouches = []; + this.updateTouchPoint(event); + this.pinchGesture = false; + this.props.touchUpdate(false); + } + + mouseUpHandler(event) { + const isLeftClick = event.button === 0; + + if (isLeftClick) { + this.setState({ + pressed: false, + }); + } } render() { - return (<div />); + const baseName = Meteor.settings.public.app.basename; + const pencilDrawStyle = { + width: '100%', + height: '100%', + touchAction: 'none', + zIndex: 2 ** 31 - 1, // maximun value of z-index to prevent other things from overlapping + cursor: this.state.pressed ? `url('${baseName}/resources/images/whiteboard-cursor/pan-closed.png') 4 8 , default` + : `url('${baseName}/resources/images/whiteboard-cursor/pan.png') 4 8, default`, + }; + return (<div + style={pencilDrawStyle} + onMouseDown={this.mouseDownHandler} + onMouseMove={this.mouseMoveHandler} + onMouseUp={this.mouseUpHandler} + onMouseLeave={this.mouseUpHandler} + onTouchStart={this.touchStartHandler} + onTouchMove={this.touchMoveHandler} + onTouchEnd={this.touchEndHandler} + />); } } diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/component.jsx index 0f9c584a2d456b2b879c7c6ce011164a38c6fb26..0c9a8fe51822a1ab8820c490aa944cdabafea693 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/component.jsx @@ -66,15 +66,15 @@ const runExceptInEdge = fn => (browser().name === 'edge' ? noop : fn); class WhiteboardToolbar extends Component { constructor() { super(); - + const isMobile = browser().mobile; this.state = { // a variable to control which list is currently open currentSubmenuOpen: '', // variables to keep current selected draw settings annotationSelected: { - icon: 'pen_tool', - value: 'pencil', + icon: isMobile ? 'hand' : 'pen_tool', + value: isMobile ? 'hand' : 'pencil', }, thicknessSelected: { value: 4 }, colorSelected: { value: '#000000' }, @@ -380,7 +380,7 @@ class WhiteboardToolbar extends Component { renderThicknessItem() { const { intl } = this.props; - const isDisabled = this.state.annotationSelected.value === 'pointer'; + const isDisabled = this.state.annotationSelected.value === 'hand'; return ( <ToolbarMenuItem disabled={isDisabled} @@ -453,7 +453,7 @@ class WhiteboardToolbar extends Component { renderColorItem() { const { intl } = this.props; - const isDisabled = this.state.annotationSelected.value === 'pointer'; + const isDisabled = this.state.annotationSelected.value === 'hand'; return ( <ToolbarMenuItem disabled={isDisabled} diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu/component.jsx index d46b3ef8e0c5917d1e22b6bc300de47c0ba9ce23..138079f833cd5e57082d9f2f014a98c53480c88f 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu/component.jsx @@ -7,9 +7,9 @@ import { styles } from '../styles'; import ToolbarSubmenuItem from '../toolbar-submenu-item/component'; const intlMessages = defineMessages({ - toolPointer: { - id: 'app.whiteboard.toolbar.tools.pointer', - description: 'Tool submenu pointer item', + toolHand: { + id: 'app.whiteboard.toolbar.tools.hand', + description: 'Tool submenu hand item', }, toolPencil: { id: 'app.whiteboard.toolbar.tools.pencil', diff --git a/bigbluebutton-html5/imports/utils/slideCalcUtils.js b/bigbluebutton-html5/imports/utils/slideCalcUtils.js new file mode 100644 index 0000000000000000000000000000000000000000..b2aee0aab47f0d0841f23d5cc829cfcb8caa2c7d --- /dev/null +++ b/bigbluebutton-html5/imports/utils/slideCalcUtils.js @@ -0,0 +1,63 @@ +const HUNDRED_PERCENT = 100; +const MYSTERY_NUM = 2; +export default class SlideCalcUtil { + // After lots of trial and error on why synching doesn't work properly, I found I had to + // multiply the coordinates by 2. There's something I don't understand probably on the + // canvas coordinate system. (ralam feb 22, 2012) + + /** + * Calculate the viewed region width + */ + static calcViewedRegionWidth(vpw, cpw) { + const width = (vpw / cpw) * HUNDRED_PERCENT; + if (width > HUNDRED_PERCENT) { + return HUNDRED_PERCENT; + } + return width; + } + + static calcViewedRegionHeight(vph, cph) { + const height = (vph / cph) * HUNDRED_PERCENT; + if (height > HUNDRED_PERCENT) { + return HUNDRED_PERCENT; + } + return height; + } + + static calcCalcPageSizeWidth(ftp, vpw, vrw) { + if (ftp) { + return (vpw / vrw) * HUNDRED_PERCENT; + } + return vpw; + } + + static calcCalcPageSizeHeight(ftp, vph, vrh, cpw, cph, opw, oph) { + if (ftp) { + return (vph / vrh) * HUNDRED_PERCENT; + } + return (cpw / opw) * oph; + } + + static calcViewedRegionX(cpx, cpw) { + return (cpx * HUNDRED_PERCENT) / cpw || 0; + } + + static calcViewedRegionY(cpy, cph) { + return (cpy * HUNDRED_PERCENT) / cph || 0; + } + + static calculateViewportX(vpw, pw) { + if (vpw == pw) { + return 0; + } + return (pw - vpw) / MYSTERY_NUM; + } + + static calculateViewportY(vph, ph) { + if (vph == ph) { + return 0; + } + return (ph - vph) / MYSTERY_NUM; + + } +} \ No newline at end of file diff --git a/bigbluebutton-html5/private/config/settings-development.json b/bigbluebutton-html5/private/config/settings-development.json index 710c0984c599a6fbce107bb4abea9a0a5ad864bd..f0f40fa656d4cb151247de4dcdf4b3411ab61ff2 100755 --- a/bigbluebutton-html5/private/config/settings-development.json +++ b/bigbluebutton-html5/private/config/settings-development.json @@ -150,6 +150,7 @@ "sendAnnotation", "removePresentation", "setPresentation", + "zoomSlide", "requestPresentationUploadToken" ] } @@ -353,8 +354,8 @@ "value": "pencil" }, { - "icon": "pointer", - "value": "pointer" + "icon": "hand", + "value": "hand" } ] } diff --git a/bigbluebutton-html5/private/config/settings-production.json b/bigbluebutton-html5/private/config/settings-production.json index 67a3c18088adc3076487836c5f9581fac78000df..40cde8bdf4b56dbc451210c8db2e78eba2d9f129 100755 --- a/bigbluebutton-html5/private/config/settings-production.json +++ b/bigbluebutton-html5/private/config/settings-production.json @@ -150,6 +150,7 @@ "sendAnnotation", "removePresentation", "setPresentation", + "zoomSlide", "requestPresentationUploadToken" ] } diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index f4a01c30d4a5840d9763a1269c6cdab8c71975a8..11707bbef9a6d3179d6b8db1014b265f9075f44a 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -57,6 +57,11 @@ "app.presentation.presentationToolbar.fitScreenDesc": "Display the whole slide", "app.presentation.presentationToolbar.zoomLabel": "Zoom", "app.presentation.presentationToolbar.zoomDesc": "Change the zoom level of the presentation", + "app.presentation.presentationToolbar.zoomInLabel": "Zoom in", + "app.presentation.presentationToolbar.zoomInDesc": "Zoom in the presentation", + "app.presentation.presentationToolbar.zoomOutLabel": "Zoom out", + "app.presentation.presentationToolbar.zoomOutDesc": "Zoom out of the presentation", + "app.presentation.presentationToolbar.zoomIndicator": "Show the zoom percentage", "app.presentation.presentationToolbar.goToSlide": "Slide {0}", "app.presentationUploder.title": "Presentation", "app.presentationUploder.message": "As a presenter in BigBlueButton, you have the ability of uploading any office document or PDF file. We recommend for the best results, to please upload a PDF file.", @@ -361,7 +366,7 @@ "app.meeting.endNotification.ok.label": "OK", "app.whiteboard.toolbar.tools": "Tools", - "app.whiteboard.toolbar.tools.pointer": "Pointer", + "app.whiteboard.toolbar.tools.hand": "Hand", "app.whiteboard.toolbar.tools.pencil": "Pencil", "app.whiteboard.toolbar.tools.rectangle": "Rectangle", "app.whiteboard.toolbar.tools.triangle": "Triangle", diff --git a/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan-closed.png b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan-closed.png new file mode 100644 index 0000000000000000000000000000000000000000..0122a1afc817b008d51a48a4726a2ec6c438c175 Binary files /dev/null and b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan-closed.png differ diff --git a/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan-closed.svg b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan-closed.svg new file mode 100644 index 0000000000000000000000000000000000000000..6c37077be908000f14139a6a4f5810444569c04c --- /dev/null +++ b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan-closed.svg @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" + id="svg2" inkscape:export-filename="/home/cwalton/Pictures/bbb-cursors/triangle.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90" inkscape:version="0.91 r13725" sodipodi:docname="triangle.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 32 32" + style="enable-background:new 0 0 32 32;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:none;stroke:#FF0000;stroke-miterlimit:10;} + .st1{fill:#FFFFFF;stroke:#3F3F3F;stroke-miterlimit:10;} +</style> +<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:bbox-nodes="true" inkscape:bbox-paths="true" inkscape:current-layer="layer1" inkscape:cx="25.698868" inkscape:cy="14.972468" inkscape:document-units="px" inkscape:guide-bbox="true" inkscape:object-nodes="true" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:snap-bbox="true" inkscape:snap-grids="true" inkscape:snap-nodes="false" inkscape:snap-others="true" inkscape:snap-smooth-nodes="true" inkscape:window-height="1009" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="0" inkscape:window-y="34" inkscape:zoom="16" pagecolor="#ffffff" showgrid="true" showguides="false" units="px"> + <inkscape:grid id="grid4203" type="xygrid"></inkscape:grid> + <sodipodi:guide id="guide4258" orientation="1,0" position="2,25"></sodipodi:guide> + <sodipodi:guide id="guide4260" orientation="0,1" position="8,30"></sodipodi:guide> + <sodipodi:guide id="guide4262" orientation="1,0" position="26,21"></sodipodi:guide> + <sodipodi:guide id="guide4264" orientation="0,1" position="9,6"></sodipodi:guide> + <sodipodi:guide id="guide4269" orientation="0,1" position="31,24"></sodipodi:guide> + <sodipodi:guide id="guide4271" orientation="1,0" position="8,21"></sodipodi:guide> +</sodipodi:namedview> +<path class="st0" d="M28.3,13.1"/> +<path class="st1" d="M9.9,11.8c-0.2-0.7,0-1.6,0.8-1.9c0.4-0.1,0.7-0.1,1,0.1c0.3,0.2,0.4,0.7,0.9,0.5c0.4-0.2,0.3-1.1,0.8-1.4 + c0.8-0.6,1.8,0,2,0.8c0,0.2,0,0.8,0.4,0.8c0.3,0,0.3-0.3,0.4-0.5c0.4-0.7,1.4-0.8,1.9-0.1c0.5,0.6,0,1.5,0.4,1.7 + c0.3,0.2,0.5-0.1,0.9-0.2c0.4-0.1,1.1,0,1.4,0.4c0.8,1-0.2,3.9-0.6,5c-0.7,2-1.6,4.1-3.6,5.1c-3.6,1.7-6.9-1.9-8-3.4 + c-0.3-0.5-0.7-1.3-1-1.8c-0.5-1.2,0.7-2.7,2-2.1c0.3,0.1,0.6,0.8,0.9,0.7c0.4-0.1,0.1-0.7,0.1-1C10.4,13.6,10.2,12.7,9.9,11.8z"/> +<path class="st0" d="M47.3,14.2"/> +</svg> diff --git a/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan.png b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan.png new file mode 100644 index 0000000000000000000000000000000000000000..5bff2d6cb58102d3e2c17344c1f34122c22b3db1 Binary files /dev/null and b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan.png differ diff --git a/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan.svg b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan.svg new file mode 100644 index 0000000000000000000000000000000000000000..c69f0479626567ae978dfcd54b21b96e36f41b3b --- /dev/null +++ b/bigbluebutton-html5/public/resources/images/whiteboard-cursor/pan.svg @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" + id="svg2" inkscape:export-filename="/home/cwalton/Pictures/bbb-cursors/triangle.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90" inkscape:version="0.91 r13725" sodipodi:docname="triangle.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 32 32" + style="enable-background:new 0 0 32 32;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#FFFFFF;stroke:#3F3F3F;stroke-miterlimit:10;} + .st1{fill:none;stroke:#FF0000;stroke-miterlimit:10;} +</style> +<path class="st0" d="M20,17.3l2.1-7.5c0,0,0.4-1.2-0.7-1.5c0,0-1.2-0.5-1.6,0.9L19,11.7c0,0-0.2,0.8-0.6,0.7c0,0-0.4,0-0.2-1l1-5.8 + c0,0,0.2-1-1-1.2c0,0-1.2-0.3-1.4,0.7l-0.8,5c0,0-0.1,0.9-0.4,0.9c0,0-0.3,0.1-0.3-0.9l-0.1-6.8c0,0,0.1-0.9-1.2-0.9 + c0,0-1.1,0.1-1.1,0.9l0,6.5c0,0,0.1,1-0.3,1c0,0-0.5,0.2-0.7-1L11.1,5c0,0-0.2-0.9-1.3-0.7c0,0-1,0.1-1,1.1l1.3,8.3 + c0,0,0.2,0.9-0.5,1.4c0,0-0.8,0.5-1.8-0.2l-2.2-1.9c0,0-1-0.8-2,0.5c0,0-0.7,0.8,0.4,1.9c0,0,6.3,6.9,9.8,6.9 + C13.9,22.5,18.5,23.4,20,17.3z"/> +</svg>