/* * ShowController.js * Keynote HTML Player * * Responsibility: Tungwei Cheng * Copyright (c) 2009-2016 Apple Inc. All rights reserved. */ var kShowControllerState_Stopped = "Stopped"; var kShowControllerState_Starting = "Starting"; var kShowControllerState_DownloadingScript = "DownloadingScipt"; var kShowControllerState_SettingUpScene = "SettingUpScene"; var kShowControllerState_IdleAtFinalState = "IdleAtFinalState"; var kShowControllerState_IdleAtInitialState = "IdleAtInitialState"; var kShowControllerState_WaitingToJump = "WaitingToJump"; var kShowControllerState_ReadyToJump = "ReadyToJump"; var kShowControllerState_WaitingToDisplay = "WaitingToDisplay"; var kShowControllerState_ReadyToDisplay = "ReadyToDisplay"; var kShowControllerState_WaitingToPlay = "WaitingToPlay"; var kShowControllerState_ReadyToPlay = "ReadyToPlay"; var kShowControllerState_Playing = "Playing"; // Events: // ------- var kKeyDownEvent = "keydown"; var kSlideIndexDidChangeEvent = "ShowController:SlideIndexDidChangeEvent"; var ShowController = Class.create({ initialize: function() { // extract delegate from url or create a default delegate this.delegate = extractDelegateFromUrlParameter(); this.delegate.showDidLoad(); this.showUrl = "../"; // These must be created before the OrientationController as they // subscribe to its event this.displayManager = new DisplayManager(); this.scriptManager = new ScriptManager(this.showUrl); this.textureManager = new TextureManager(this.showUrl); this.stageManager = new StageManager(this.textureManager, this.scriptManager); this.touchController = new TouchController(); this.animationManager = new AnimationManager(); this.orientationController = new OrientationController(); this.activeHyperlinks = new Array(); this.movieHyperlinks = new Array(); // initialize default values this.script = null; this.currentSceneIndex = -1; this.nextSceneIndex = -1; this.currentSlideIndex = -1; this.previousSlideIndex = -1; this.currentSoundTrackIndex = 0; this.transformOriginValue = ""; this.accumulatingDigits = false; this.digitAccumulator = 0; this.firstSlide = true; this.lastSlideViewedIndex = -1; this.accountID = ""; this.guid = ""; this.locale = "EN"; this.isNavigationBarVisible = false; this.isFullscreen = false; this.volume = 3.0; this.muted = false; this.soundTrackPlayer = null; this.sceneIndexOfPrebuiltAnimations = -1; // store queued user action this.queuedUserAction = null; // events document.observe(kScriptDidDownloadEvent, this.handleScriptDidDownloadEvent.bind(this)); document.observe(kScriptDidNotDownloadEvent, this.handleScriptDidNotDownloadEvent.bind(this)); document.observe(kStageIsReadyEvent, this.handleStageIsReadyEvent.bind(this)); document.observe(kStageSizeDidChangeEvent, this.handleStageSizeDidChangeEvent.bind(this)); // swipe and keydown events document.observe(kKeyDownEvent, this.handleKeyDownEvent.bind(this)); document.observe(kSwipeEvent, this.handleSwipeEvent.bind(this)); // mouse event Event.observe(this.displayManager.body, "click", this.handleClickEvent.bind(this)); // fullscreen change event document.observe(kFullscreenChangeEventName, this.handleFullscreenChangeEvent.bind(this)); // windows resize event Event.observe(window, "resize", this.handleWindowResizeEvent.bind(this)); // Can't use event observer for tap events // - this would cause the handler to be on a seperate event loop // invocation // - this would prevent us from doing this like opening new tabs (popup // blocker logic kicks in) this.touchController.registerTapEventCallback(this.handleTapEvent.bind(this)); // Initialize state to Stopped this.changeState(kShowControllerState_Stopped); // movie cache to be used across different event timeline within one slide this.movieCache = null; // audio cache this.audioCache = null; // KPF playback controller this.playbackController = new KPFPlaybackController({}, this.stageManager.stage); // Navigator this.navigatorController = new NavigatorController(document.getElementById("slideshowNavigator")); // Slide number feedback this.slideNumberController = new SlideNumberController(document.getElementById("slideNumberControl")); // Slide number display this.slideNumberDisplay = new SlideNumberDisplay(document.getElementById("slideNumberDisplay")); // Help Placard this.helpPlacard = new HelpPlacardController(document.getElementById("helpPlacard")); // indicate if the show has recording this.isRecording = false; // a boolean to indicate if the recording is started this.isRecordingStarted = false; // IE9 does not support CSS animations if (isIE && browserVersion < 10) { this.animationSupported = false; } else { this.animationSupported = true; } // disable mouse right click context menu document.observe("contextmenu", this.handleContextMenuEvent.bind(this)); }, startShow: function() { this.changeState(kShowControllerState_DownloadingScript); this.scriptManager.downloadScript(this.delegate); }, exitShow: function(endNow) { clearTimeout(this.exitTimeout); if (endNow) { this.delegate.showExited(); } else { this.exitTimeout = setTimeout((function(){ this.delegate.showExited(); }).bind(this), 750); } }, promptUserToTryAgain: function(message) { var tryAgain = false; tryAgain = confirm(message); return tryAgain; }, handleScriptDidDownloadEvent: function(event) { switch (this.state) { case kShowControllerState_DownloadingScript: var script = this.script = event.memo.script; var showMode = script.showMode; if (showMode == kShowModeHyperlinksOnly) { this.displayManager.setHyperlinksOnlyMode(); } this.changeState(kShowControllerState_Starting); // checking to see if a restarting scene index was specified in url... var sceneIndex; var restartingSceneIndex = parseInt(getUrlParameter("restartingSceneIndex")); // also look for a fragment identifier which can also indicate scene index var currentUrl = document.URL.split("?"); var fragments = currentUrl[0].split("#"); if (fragments[1]) { restartingSceneIndex = parseInt(fragments[1]); } if (restartingSceneIndex) { // found a restarting scene index, using that...restartingSceneIndex sceneIndex = restartingSceneIndex; } else { // checking to see if a starting slide number was specified in url... var startingSlide = getUrlParameter("currentSlide"); var startingSlideNumber; if (startingSlide) { startingSlideNumber = parseInt(startingSlide); } else { // nope, not there, use 1... startingSlideNumber = 1; } sceneIndex = this.scriptManager.sceneIndexFromSlideIndex(startingSlideNumber - 1); } // if this show has recording, then we start the show in recording mode if (script.recording) { if (script.recording.eventTracks[0].type === "navigation") { this.narrationManager = new NarrationManager(script.recording); sceneIndex = this.narrationManager.sceneIndexFromNavigationEvent(this.narrationManager.navigationEvents[0]); this.isRecording = true; this.jumpToScene(sceneIndex, false); break; } } if (sceneIndex > script.lastSceneIndex) { break; } if (showMode === kShowModeAutoplay) { this.jumpToScene(sceneIndex, true); } else { var event = script.events[sceneIndex]; var automaticPlay = event.automaticPlay == 1 || event.automaticPlay == true; this.jumpToScene(sceneIndex, automaticPlay); } break; default: debugMessage(kDebugShowController_HandleScriptDidDownloadEvent, "- hmmm we seem to have arrived here from an unpredicted state"); break; } }, handleScriptDidNotDownloadEvent: function(event) { debugMessage(kDebugShowController_HandleScriptDidNotDownloadEvent); var tryAgain = this.promptUserToTryAgain(kUnableToReachiWorkTryAgain); if (tryAgain) { this.scriptManager.downloadScript(); } else { this.displayManager.clearLaunchMode(); this.displayManager.hideWaitingIndicator(); } }, handleStageIsReadyEvent: function(event) { if (this.isFullscreen) { setTimeout((function() { this.displayManager.stageArea.style.opacity = 1; }).bind(this), 50) } else { setTimeout((function() { this.displayManager.stageArea.style.opacity = 1; }).bind(this), 500) } this.positionSlideNumberControl(); this.positionSlideNumberDisplay(); this.positionHelpPlacard(); }, positionSlideNumberControl: function() { var left = (this.displayManager.usableDisplayWidth - this.slideNumberController.width) / 2; var top = this.displayManager.stageAreaTop + this.displayManager.stageAreaHeight - (this.slideNumberController.height + 16); this.slideNumberController.setPosition(left, top); }, positionSlideNumberDisplay: function() { var left = (this.displayManager.usableDisplayWidth - this.slideNumberDisplay.width) / 2; var top = this.displayManager.stageAreaTop + this.displayManager.stageAreaHeight - (this.slideNumberDisplay.height + 16); this.slideNumberDisplay.setPosition(left, top); }, positionHelpPlacard: function() { var left = (this.displayManager.usableDisplayWidth - this.helpPlacard.width) / 2; var top = (this.displayManager.usableDisplayHeight - this.helpPlacard.height) / 2; this.helpPlacard.setPosition(left, top); }, handleFullscreenChangeEvent: function() { if (document.webkitIsFullScreen || document.mozFullScreen) { this.isFullscreen = true; } else { this.isFullscreen = false; } setTimeout((function() { this.displayManager.layoutDisplay(); }).bind(this), 0); }, handleWindowResizeEvent: function() { clearTimeout(this.resizeTimer); this.resizeTimer = setTimeout(this.changeWindowSize.bind(this), 1000); }, changeWindowSize: function() { if (this.delegate.setViewScale) { this.scriptManager.reapplyScaleFactor(); this.textureManager.slideCache = null; this.textureManager.slideCache = {}; var sceneIndexToUse = this.currentSceneIndex; if (this.state === kShowControllerState_IdleAtFinalState) { if (this.currentSceneIndex < this.script.numScenes - 1) { sceneIndexToUse = this.currentSceneIndex + 1; } else { if (this.script.loopSlideshow) { sceneIndexToUse = 0; } } } this.jumpToScene(sceneIndexToUse, false); } document.fire(kShowSizeDidChangeEvent, { width: this.script.slideWidth, height: this.script.slideHeight }); }, handleStageSizeDidChangeEvent: function(event) { // update TouchController with new track area this.touchController.setTrackArea(event.memo.left, event.memo.top, event.memo.width, event.memo.height); }, handleKeyDownEvent: function(event) { var key = event.charCode || event.keyCode; // allow F11 and F12 to work on IE if (key === kKeyCode_F11 || key === kKeyCode_F12) { return; } var modifiers = { altKey: !!event.altKey, ctrlKey: !!event.ctrlKey, shiftKey: !!event.shiftKey, metaKey: !!event.metaKey }; if (modifiers.metaKey) { if (key === kKeyCode_Period || key === kKeyCode_Dot) { // cmd - . to exit show this.exitShow(true); } else if (key != kKeyCode_Return) { // allow browsers to handle cmd-key return; } } else if (modifiers.ctrlKey) { // allow browsers to handle ctrl-key return; } event.stop(); this.onKeyPress(key, modifiers); }, handleContextMenuEvent: function(event) { event.stop(); }, handleClickEvent: function(event) { if (this.isRecording) { return; } var x, y; if (event.pageX || event.pageY) { x = event.pageX; y = event.pageY; } else { x = event.clientX; y = event.clientY; } var displayCoOrds = { pointX: x, pointY: y }; // for IE make sure windows has focus if (isIE) { window.focus(); } // For video element, let the event to propagate if (event.target.nodeName.toLowerCase() === "video") { return; } this.processClickOrTapAtDisplayCoOrds(displayCoOrds); }, handleTapEvent: function(event) { var displayCoOrds = { pointX: event.memo.pointX, pointY: event.memo.pointY }; var target = event.memo.target; var slideNumber; if (target) { slideNumber = this.slideNumberFromTarget(target); } if (slideNumber) { this.navigatorController.select(slideNumber); } else { this.processClickOrTapAtDisplayCoOrds(displayCoOrds); } }, slideNumberFromTarget: function(target) { // return null if there is no target if (!target) { return null; } // search up to the node below body node while (target.slideNumber == null && target.nodeName.toLowerCase() !== "body") { target = target.parentNode; } return target.slideNumber; }, processClickOrTapAtDisplayCoOrds: function(displayCoOrds) { var isHyperlink = false; var hyperlink; if (this.slideNumberController.isShowing) { if (this.slideNumberTimeout) { clearTimeout(this.slideNumberTimeout); } this.slideNumberTimeout = setTimeout(this.hideAndResetSlideNumberController.bind(this), 0); return; } if (this.helpPlacard.isShowing) { this.helpPlacard.hide(); return; } var showCoOrds = this.displayManager.convertDisplayCoOrdsToShowCoOrds(displayCoOrds); if (showCoOrds.pointX != -1) { hyperlink = this.findHyperlinkAtCoOrds(showCoOrds); } if (hyperlink) { this.processHyperlink(hyperlink); } else { this.advanceToNextBuild("processClickOrTapAtDisplayCoOrds"); } }, handleSwipeEvent: function(event) { var memo = event.memo; var direction = memo.direction; // if the navigator is showing then hide it when swiping either left or right if (this.displayManager.navigatorIsShowing && (direction === "left" || direction === "right")) { this.navigatorController.thumbnailSidebar.hide(this.navigatorController.leftSidebar); return; } // Toggle the slide navigator if swipe event is in navigator area. // The thumbnail scroller is 129px. For now we use 150px but can be adjusted later if needed. if (memo.swipeStartX && memo.swipeStartX < 150) { if (memo.direction === "right") { this.navigatorController.thumbnailSidebar.show(this.navigatorController.leftSidebar); } else if (memo.direction === "left") { this.navigatorController.thumbnailSidebar.hide(this.navigatorController.leftSidebar); } return; } if (event.memo.direction === "left") { switch (event.memo.fingers) { case 1: this.advanceToNextBuild("handleSwipeEvent"); break; case 2: this.advanceToNextSlide("handleSwipeEvent"); break; default: break; } } else if (event.memo.direction === "right") { switch (event.memo.fingers) { case 1: this.goBackToPreviousSlide("handleSwipeEvent"); break; case 2: this.goBackToPreviousBuild("handleSwipeEvent"); break; default: break; } } }, onMouseDown: function(mouseDownEvent) { if (mouseDownEvent.leftClick) { this.advanceToNextBuild("onMouseDown"); } else if (mouseDownEvent.rightClick) { this.goBackToPreviousBuild("onMouseDown"); } }, onKeyPress: function(key, modifier) { if ((key >= kKeyCode_Numeric_0) && (key <= kKeyCode_Numeric_9)) { key = kKeyCode_0 + (key - kKeyCode_Numeric_0); } key += (modifier.shiftKey ? kKeyModifier_Shift : 0); key += (modifier.altKey ? kKeyModifier_Alt : 0); key += (modifier.ctrlKey ? kKeyModifier_Ctrl : 0); key += (modifier.metaKey ? kKeyModifier_Meta : 0); if (this.isRecording) { return; } var digitEncountered = false; switch (key) { case kKeyCode_Escape: this.exitShow(true); break; /* * case kKeyCode_Return + kKeyModifier_Ctrl: * this.displayManager.showWaitingIndicator(); break; * * case kKeyCode_Return + kKeyModifier_Alt: * this.displayManager.hideWaitingIndicator(); break; * * case kKeyCode_Return + kKeyModifier_Meta: this.debugDiagnosticDump(); * break; */ case kKeyCode_Slash: case kKeyCode_Slash + kKeyModifier_Shift: if (this.helpPlacard.isShowing) { this.helpPlacard.hide(); } else { this.helpPlacard.show(); } break; case kKeyCode_Q: this.exitShow(true); break; case kKeyCode_S: if (this.slideNumberController.isShowing) { if (this.slideNumberTimeout) { clearTimeout(this.slideNumberTimeout); } this.slideNumberTimeout = setTimeout(this.hideAndResetSlideNumberController.bind(this), 0); } if (this.slideNumberDisplay.isShowing) { this.slideNumberDisplay.hide(); } else { this.slideNumberDisplay.setSlideNumber(this.currentSlideIndex + 1); this.slideNumberDisplay.show(); } break; case kKeyCode_Return: if (this.accumulatingDigits) { // return pressed while accumulating digits. this.accumulatingDigits = false; if (this.script.showMode != kShowModeHyperlinksOnly) { if (this.digitAccumulator > this.script.slideCount) { this.digitAccumulator = this.script.slideCount; } else if (this.digitAccumulator < 1) { this.digitAccumulator = 1; } this.slideNumberController.setSlideNumber(this.digitAccumulator); this.jumpToSlide(this.digitAccumulator); } else { debugMessage(kDebugShowController_OnKeyPress, "- can't do it, we're in hyperlinks only mode"); } break; } // fall through case kKeyCode_N: case kKeyCode_Space: case kKeyCode_DownArrow: case kKeyCode_RightArrow: case kKeyCode_PageDown: // advance to next build... this.advanceToNextBuild("onKeyPress"); break; case kKeyCode_RightArrow + kKeyModifier_Shift: case kKeyCode_CloseBracket: // advance and skip build... this.advanceAndSkipBuild("onKeyPress"); break; case kKeyCode_DownArrow + kKeyModifier_Shift: case kKeyCode_PageDown + kKeyModifier_Shift: case kKeyCode_CloseBracket: case kKeyCode_Equal + kKeyModifier_Shift: case kKeyCode_Equal: case kKeyCode_Plus: // advance to next slide... this.advanceToNextSlide("onKeyPress"); break; case kKeyCode_LeftArrow + kKeyModifier_Shift: case kKeyCode_PageUp + kKeyModifier_Shift: case kKeyCode_OpenBracket: // go back to previous build... this.goBackToPreviousBuild("onKeyPress"); break; case kKeyCode_P: case kKeyCode_PageUp: case kKeyCode_LeftArrow: case kKeyCode_UpArrow: case kKeyCode_UpArrow + kKeyModifier_Shift: case kKeyCode_Hyphen: case kKeyCode_Minus: // go back to previous slide... this.goBackToPreviousSlide("onKeyPress"); break; case kKeyCode_Delete: digitEncountered = true; if (this.accumulatingDigits) { if (this.digitAccumulator < 10) { if (this.slideNumberTimeout) { clearTimeout(this.slideNumberTimeout); } this.slideNumberTimeout = setTimeout(this.hideAndResetSlideNumberController.bind(this), 0); } else { if (this.slideNumberTimeout) { clearTimeout(this.slideNumberTimeout); } this.slideNumberTimeout = setTimeout(this.hideAndResetSlideNumberController.bind(this), 7000); var digit = this.digitAccumulator.toString(); this.digitAccumulator = parseInt(digit.substring(0, digit.length - 1)); this.slideNumberController.setSlideNumber(this.digitAccumulator); } } break; case kKeyCode_Home: // go back to first slide... if (this.script.showMode != kShowModeHyperlinksOnly) { this.jumpToSlide(1); } else { debugMessage(kDebugShowController_OnKeyPress, "- can't do it, we're in hyperlinks only mode"); } break; case kKeyCode_End: // go back to last slide... if (this.script.showMode != kShowModeHyperlinksOnly) { this.jumpToSlide(this.script.slideCount); } else { debugMessage(kDebugShowController_OnKeyPress, "- can't do it, we're in hyperlinks only mode"); } break; default: if (this.slideNumberTimeout) { clearTimeout(this.slideNumberTimeout); } this.slideNumberTimeout = setTimeout(this.hideAndResetSlideNumberController.bind(this), 7000); if ((key >= kKeyCode_0) && (key <= kKeyCode_9)) { if (this.slideNumberDisplay.isShowing) { this.slideNumberDisplay.hide(); } digitEncountered = true; if (this.accumulatingDigits === false) { // digit entered, start accumulating digits... this.accumulatingDigits = true; this.digitAccumulator = 0; } if (this.digitAccumulator.toString().length < 4) { this.digitAccumulator *= 10; this.digitAccumulator += (key - kKeyCode_0); this.slideNumberController.setSlideNumber(this.digitAccumulator); if (!this.slideNumberController.isShowing) { this.slideNumberController.show(); } } } else { digitEncountered = true; } break; } if (this.accumulatingDigits && (digitEncountered === false)) { // non-digit entered, stop accumulating digits... //this.accumulatingDigits = false; //this.digitAccumulator = 0; } }, hideAndResetSlideNumberController: function() { if (this.slideNumberTimeout) { clearTimeout(this.slideNumberTimeout); } this.accumulatingDigits = false; this.digitAccumulator = 0; this.slideNumberController.hide(); }, hideSlideNumberDisplay: function() { this.slideNumberDisplay.hide(); }, toggleFullscreen: function() { // IE does not support fullscreen mode, return for now if (isIE) { return; } setTimeout((function() { this.displayManager.stageArea.style.opacity = 0; }).bind(this), 0); // hide hud immediately this.displayManager.hideHUD(true); if (document.webkitIsFullScreen || document.mozFullScreen) { this.isFullscreen = false; (document.webkitCancelFullScreen && document.webkitCancelFullScreen()) || (document.mozCancelFullScreen && document.mozCancelFullScreen()); } else { this.isFullscreen = true; (document.body.webkitRequestFullScreen && document.body.webkitRequestFullScreen()) || (document.body.mozRequestFullScreen && document.body.mozRequestFullScreen()); } }, // State Management // ================ changeState: function(newState) { if (newState != this.state) { //this.accumulatingDigits = false; //this.digitAccumulator = 0; this.leavingState(); this.state = newState; this.enteringState(); } }, leavingState: function() { switch (this.state) { case kShowControllerState_Stopped: break; case kShowControllerState_Starting: break; case kShowControllerState_SettingUpScene: break; case kShowControllerState_IdleAtFinalState: break; case kShowControllerState_IdleAtInitialState: break; case kShowControllerState_WaitingToJump: break; case kShowControllerState_ReadyToJump: break; case kShowControllerState_WaitingToPlay: this.displayManager.hideWaitingIndicator(); break; case kShowControllerState_ReadyToPlay: break; case kShowControllerState_Playing: break; } }, enteringState: function() { switch (this.state) { case kShowControllerState_Stopped: break; case kShowControllerState_Starting: this.displayManager.showWaitingIndicator(); break; case kShowControllerState_SettingUpScene: break; case kShowControllerState_IdleAtFinalState: // unload slide cache not next to the current slide this.unloadTextures(); case kShowControllerState_IdleAtInitialState: this.updateSlideNumber(); runInNextEventLoop(this.doIdleProcessing.bind(this)); break; case kShowControllerState_WaitingToJump: // don't show spinner here, do it in pollForSceneToLoad after a few // polls so the spinner doesn't come up right away break; case kShowControllerState_ReadyToJump: break; case kShowControllerState_WaitingToPlay: this.displayManager.showWaitingIndicator(); break; case kShowControllerState_ReadyToPlay: break; case kShowControllerState_Playing: break; } }, preloadTextures: function() { var script = this.script; var sceneIndexToUse = this.currentSceneIndex; if (this.state === kShowControllerState_IdleAtFinalState) { if (sceneIndexToUse < script.numScenes - 1) { sceneIndexToUse = sceneIndexToUse + 1; } else if (script.loopSlideshow) { sceneIndexToUse = 0; } } // preload textures this.textureManager.loadScene(sceneIndexToUse); }, unloadTextures: function() { var script = this.script; var sceneIndexToUse = this.currentSceneIndex; if (this.state === kShowControllerState_IdleAtFinalState) { if (sceneIndexToUse < script.numScenes - 1) { sceneIndexToUse = sceneIndexToUse + 1; } else if (script.loopSlideshow) { sceneIndexToUse = 0; } } var currentSlideIndex = script.slideIndexFromSceneIndexLookup[sceneIndexToUse]; var slideCache = this.textureManager.slideCache; for (var index in slideCache) { // detect current slide index and its previous and next slide in slideCache buffer if (index < currentSlideIndex - 1 || index > currentSlideIndex + 1) { // remove slide cache var cache = slideCache[index]; for (var textureId in cache.textureAssets) { var canvas = cache.textureAssets[textureId]; if (canvas) { // clear canvas object var context = canvas.getContext("2d"); if (context) { context.clearRect(0, 0, canvas.width, canvas.height); } // remove reference delete cache.textureAssets[textureId]; } } delete this.textureManager.slideCache[index].textureAssets; delete this.textureManager.slideCache[index].textureRequests; delete this.textureManager.slideCache[index].requested; // call internal pdf document destroy method if (cache.pdf) { cache.pdf.destroy(); delete this.textureManager.slideCache[index].pdf; } // finally remove slide cache reference delete this.textureManager.slideCache[index]; } } }, doIdleProcessing: function() { // preload textures for next slide if applicable this.preloadTextures(); if (this.queuedUserAction != null) { // executing queued user action... this.queuedUserAction(); this.queuedUserAction = null; } else { var stage = this.stageManager.stage; if (stage.childNodes.length !== 0) { this.updateNavigationButtons(); } } // create hyperlink in setTimeout using background thread clearTimeout(this.createHyperlinksForCurrentStateTimeout); this.createHyperlinksForCurrentStateTimeout = setTimeout((function() {this.createHyperlinksForCurrentState("idle");}).bind(this), 100); }, truncatedSlideIndex: function(slideIndex) { return this.truncatedIndex(slideIndex, this.script.lastSlideIndex, this.script.loopSlideshow); }, truncatedSceneIndex: function(sceneIndex) { return this.truncatedIndex(sceneIndex, this.script.lastSceneIndex, this.script.loopSlideshow); }, truncatedIndex: function(index, lastIndex, isLooping) { if (index < 0) { if (isLooping) { index = index + lastIndex + 1; } else { index = -1; } } else if (index > lastIndex) { if (isLooping) { index = index - lastIndex - 1; } else { index = -1; } } return index; }, advanceToNextBuild: function(context) { // do not proceed if the script is not available if (!this.script) { return false; } if (this.script.showMode === kShowModeHyperlinksOnly && context != "currentSceneDidComplete") { return false; } if (this.displayManager.infoPanelIsShowing) { return false; } var result = false; switch (this.state) { case kShowControllerState_IdleAtFinalState: if (this.nextSceneIndex === -1) { if (this.delegate.getKPFJsonStringForShow) { this.stopSoundTrack(); this.exitShow(); } else { this.stopSoundTrack(); break; } } // idle on final state, jump to next scene result = true; this.jumpToScene(this.nextSceneIndex, true); break; case kShowControllerState_IdleAtInitialState: if (this.currentSceneIndex >= this.script.numScenes) { if (this.script.loopSlideshow) { // we're at the end but this IS a looping show, jump to start result = true; this.jumpToScene(0, false); } else { if (this.delegate.getKPFJsonStringForShow) { this.stopSoundTrack(); this.exitShow(); } else { this.stopSoundTrack(); break; } } } else { // we're sitting idle on initial state, preload next scene and play current scene result = true; this.playCurrentScene(); } break; default: debugMessage(kDebugShowController_AdvanceToNextBuild, "nextSceneIndex: " + this.nextSceneIndex + " can't advance now, not in an idle state (currently in '" + this.state + "' state), queue up action to run in next idle time"); if (this.queuedUserAction == null) { result = true; this.queuedUserAction = this.advanceToNextBuild.bind(this, context); } break; } return result; }, advanceToNextSlide: function(context) { // do not proceed if the script is not available if (!this.script) { return false; } if (this.script.showMode == kShowModeHyperlinksOnly) { return; } if (this.displayManager.infoPanelIsShowing) { return; } var sceneIndexToUse = this.currentSceneIndex; switch (this.state) { case kShowControllerState_IdleAtFinalState: sceneIndexToUse = sceneIndexToUse + 1; // Fall through case kShowControllerState_IdleAtInitialState: var currentSlideIndex = this.scriptManager.slideIndexFromSceneIndex(sceneIndexToUse); var nextSlideIndex ; if (currentSlideIndex === this.script.slideCount - 1) { if (this.script.loopSlideshow) { nextSlideIndex = 0; } else { return; } } else { nextSlideIndex = this.currentSlideIndex + 1; } var sceneIndex = this.scriptManager.sceneIndexFromSlideIndex(nextSlideIndex); var event = this.script.events[sceneIndex]; var automaticPlay = event.automaticPlay == 1 || event.automaticPlay == true; this.jumpToSlide(nextSlideIndex + 1, automaticPlay); break; default: debugMessage(kDebugShowController_AdvanceToNextSlide, "can't advance now, not in an idle state (currently in '" + this.state + "' state), queue up action to run in next idle time"); if (this.queuedUserAction == null) { this.queuedUserAction = this.advanceToNextSlide.bind(this, context); } break; } }, goBackToPreviousBuild: function(context) { // do not proceed if the script is not available if (!this.script) { return false; } // going back to previous build, remove all media cache this.resetMediaCache(); if (this.script.showMode == kShowModeHyperlinksOnly) { return; } if (this.displayManager.infoPanelIsShowing) { return; } var sceneIndexToUse = this.currentSceneIndex; switch (this.state) { case kShowControllerState_IdleAtFinalState: sceneIndexToUse = sceneIndexToUse + 1; // Fall through case kShowControllerState_Playing: case kShowControllerState_IdleAtInitialState: var previousSceneIndex ; if (sceneIndexToUse === 0) { if (this.script.loopSlideshow) { previousSceneIndex = this.script.events.length - 1; } else { return; } } else { previousSceneIndex = sceneIndexToUse - 1; } this.jumpToScene(previousSceneIndex, false); break; default: debugMessage(kDebugShowController_GoBackToPreviousBuild, "can't go back now, not in an idle state (currently in '" + this.state + "' state)"); if (this.queuedUserAction == null) { this.queuedUserAction = this.goBackToPreviousBuild.bind(this, context); } break; } }, advanceAndSkipBuild: function(context) { // do not proceed if the script is not available if (!this.script) { return false; } if (this.script.showMode == kShowModeHyperlinksOnly) { return; } var sceneIndexToUse = this.currentSceneIndex; switch (this.state) { case kShowControllerState_IdleAtFinalState: sceneIndexToUse = sceneIndexToUse + 1; // Fall through case kShowControllerState_IdleAtInitialState: var nextSceneIndex; if (sceneIndexToUse >= this.script.numScenes - 1) { if (this.script.loopSlideshow) { nextSceneIndex = 0; } else { return; } } else { nextSceneIndex = sceneIndexToUse + 1; } this.jumpToScene(nextSceneIndex, false); break; default: debugMessage(kDebugShowController_GoBackToPreviousBuild, "can't go back now, not in an idle state (currently in '" + this.state + "' state)"); if (this.queuedUserAction == null) { this.queuedUserAction = this.advanceAndSkipBuild.bind(this, context); } break; } }, goBackToPreviousSlide: function(context) { // do not proceed if the script is not available if (!this.script) { return false; } if (this.script.showMode == kShowModeHyperlinksOnly) { return; } if (this.displayManager.infoPanelIsShowing) { return; } var sceneIndexToUse = this.currentSceneIndex; switch (this.state) { case kShowControllerState_IdleAtFinalState: sceneIndexToUse = sceneIndexToUse + 1; // Fall through case kShowControllerState_Playing: case kShowControllerState_IdleAtInitialState: var currentSlideIndex = this.scriptManager.slideIndexFromSceneIndex(sceneIndexToUse); var sceneIndexForCurrentSlideIndex = this.scriptManager.sceneIndexFromSlideIndex(currentSlideIndex); var previousSlideIndex; if (currentSlideIndex === 0) { if (sceneIndexToUse > 0) { // if we are not on first build of the slide, go back to first build of the slide previousSlideIndex = 0; } else { if (this.script.loopSlideshow) { previousSlideIndex = this.script.slideCount - 1; } else { previousSlideIndex = 0; } } } else if (currentSlideIndex === -1 && sceneIndexToUse > 0) { previousSlideIndex = this.script.slideCount - 1; } else { if (sceneIndexToUse > sceneIndexForCurrentSlideIndex) { // if we are not on first build of the slide, go back to first build of the slide previousSlideIndex = this.currentSlideIndex; } else { // if we are on first build of the slide, go back to previous slide previousSlideIndex = this.currentSlideIndex - 1; } } this.jumpToSlide(previousSlideIndex + 1); break; default: debugMessage(kDebugShowController_GoBackToPreviousSlide, "can't go back now, not in an idle state (currently in '" + this.state + "' state)"); if (this.queuedUserAction == null) { this.queuedUserAction = this.goBackToPreviousSlide.bind(this, context); } break; } }, calculatePreviousSceneIndex: function(sceneIndex) { if (sceneIndex == -1) { previousSceneIndex = -1; } else { previousSceneIndex = sceneIndex - 1; } return previousSceneIndex; }, jumpToSlide: function(slideNumber, automaticPlay) { var slideIndex = slideNumber - 1; var sceneIndex = this.scriptManager.sceneIndexFromSlideIndex(slideIndex); // we are jumping to slide, remove all media cache this.resetMediaCache(); // enable automatic play when the slide it advances to has automatic play // see Next slide does not auto advance when using keyboard to advance without transiti if (automaticPlay == null) { automaticPlay = false; } this.jumpToScene(sceneIndex, automaticPlay); }, jumpToScene: function(sceneIndex, playAnimations) { this.lastSlideViewedIndex = this.scriptManager.slideIndexFromSceneIndex(this.currentSceneIndex); if (sceneIndex === -1) { return; } switch (this.state) { case kShowControllerState_Starting: // There is a bug in webkit - cursor not being able to change if sitting idle unless it has been moved // The workaround for the bug without moving the mouse cursor is to change DOM structure to force a redraw // References: http://code.google.com/p/chromium/issues/detail?id=26723 var cssText = "position:absolute;background-color:transparent; left:0px; top:0px; width:" + this.displayManager.usableDisplayWidth +"px; height:" + this.displayManager.usableDisplayHeight + "px;"; this.starting = true; this.maskElement = document.createElement("div"); this.maskElement.setAttribute("style",cssText); document.body.appendChild(this.maskElement); case kShowControllerState_IdleAtInitialState: case kShowControllerState_IdleAtFinalState: case kShowControllerState_ReadyToJump: break; default: debugMessage(kDebugShowController_JumpToScene, "can't jump now, currently in '" + this.state + "' state which does not supports jumping..."); return; } if (this.textureManager.isScenePreloaded(sceneIndex) === false) { this.changeState(kShowControllerState_WaitingToJump); // loadScene with callback handler and params var sceneToLoadInfo = { sceneIndex: sceneIndex, automaticPlay: playAnimations }; this.waitForSceneToLoadTimeout = setTimeout(this.handleSceneDidNotLoad.bind(this, sceneToLoadInfo), kMaxSceneDownloadWaitTime); this.textureManager.loadScene(sceneIndex, this.handleSceneDidLoad.bind(this, sceneToLoadInfo)); return; } this.changeState(kShowControllerState_SettingUpScene); runInNextEventLoop(this.jumpToScene_partThree.bind(this, sceneIndex, playAnimations)); }, handleSceneDidLoad: function(sceneToLoadInfo) { clearTimeout(this.waitForSceneToLoadTimeout); switch (this.state) { case kShowControllerState_WaitingToJump: this.changeState(kShowControllerState_ReadyToJump); this.jumpToScene_partTwo(sceneToLoadInfo.sceneIndex, sceneToLoadInfo.automaticPlay); break; default: break; } }, handleSceneDidNotLoad: function(sceneToLoadInfo) { clearTimeout(this.waitForSceneToLoadTimeout); this.queuedUserAction = null; var tryAgain = this.promptUserToTryAgain(kUnableToReachiWorkTryAgain); if (tryAgain) { // restarting player with sceneIndex var currentUrl = window.location.href; var croppedUrl; var indexOfRestartParam = currentUrl.indexOf("&restartingSceneIndex"); if (indexOfRestartParam === -1) { croppedUrl = currentUrl; } else { croppedUrl = currentUrl.substring(0, indexOfRestartParam); } var newUrl = croppedUrl + "&restartingSceneIndex=" + sceneToLoadInfo.sceneIndex; window.location.replace(newUrl); } else { this.changeState(kShowControllerState_IdleAtFinalState); } }, jumpToScene_partTwo: function(sceneIndex, playAnimations) { this.changeState(kShowControllerState_SettingUpScene); // state changed (UI controls should disable), run partThree in next event loop runInNextEventLoop(this.jumpToScene_partThree.bind(this, sceneIndex, playAnimations)); }, jumpToScene_partThree: function(sceneIndex, playAnimations) { var delayBeforeNextPart = false; if (delayBeforeNextPart) { runInNextEventLoop(this.jumpToScene_partFour.bind(this, sceneIndex, playAnimations)); } else { this.jumpToScene_partFour(sceneIndex, playAnimations); } }, jumpToScene_partFour: function(sceneIndex, playAnimations) { this.displayScene(sceneIndex); if (this.starting) { // There is a bug in webkit - cursor not being able to change if sitting idle unless it has been moved // The workaround for the bug without moving the mouse cursor is to change DOM structure to force a redraw // References: http://code.google.com/p/chromium/issues/detail?id=26723 if (this.maskElement != null) { document.body.removeChild(this.maskElement); this.maskElement = null; this.starting = false; } window.focus(); } if (this.helpPlacard.isShowing) { this.helpPlacard.hide(); } if (this.slideNumberDisplay.isShowing) { this.slideNumberDisplay.hide(); } if (this.slideNumberController.isShowing) { if (this.slideNumberTimeout) { clearTimeout(this.slideNumberTimeout); } this.slideNumberTimeout = setTimeout(this.hideAndResetSlideNumberController.bind(this), 500); } if (playAnimations) { var script = this.script; if (script.showMode === kShowModeAutoplay) { var event = script.events[sceneIndex]; var effects = event.effects; if (effects && effects.length > 0) { var delay = effects[0].type === "transition" ? script.autoplayTransitionDelay : script.autoplayBuildDelay; setTimeout((function(){this.playCurrentScene();}).bind(this), delay * 1000); } else { this.playCurrentScene(); } } else { this.playCurrentScene(); } } else { this.changeState(kShowControllerState_IdleAtInitialState); if (this.isRecording && !this.isRecordingStarted) { this.narrationManager.start(); this.isRecordingStarted = true; } } }, displayScene: function(sceneIndex, hyperlinkEvent) { if (sceneIndex === -1) { return; } // remove all css this.animationManager.deleteAllAnimations(); // clean up media cache if we are advancing to different slide var outgoingSlideIndex = this.scriptManager.slideIndexFromSceneIndex(this.currentSceneIndex); var incomingSlideIndex = hyperlinkEvent ? hyperlinkEvent.slideIndex : this.scriptManager.slideIndexFromSceneIndex(sceneIndex); if (outgoingSlideIndex !== incomingSlideIndex) { this.resetMediaCache(); } // set currentSceneIndex this.setCurrentSceneIndexTo(sceneIndex); if (hyperlinkEvent) { this.playbackController.renderEvent(hyperlinkEvent); } else { var slideIndex = this.script.slideIndexFromSceneIndexLookup[sceneIndex]; var slideId = this.script.slideList[slideIndex]; var kpfEvent = new KPFEvent({ "slideId": slideId, "slideIndex": slideIndex, "sceneIndex": sceneIndex, "event": this.script.events[sceneIndex], "animationSupported": this.animationSupported }); this.playbackController.renderEvent(kpfEvent); } this.updateNavigationButtons(); }, setCurrentSceneIndexTo: function(sceneIndex) { this.currentSceneIndex = sceneIndex; this.assignNextSceneIndex(); this.updateSlideNumber(); this.updateNavigationButtons(); }, assignNextSceneIndex: function() { this.nextSceneIndex = this.calculateNextSceneIndex(this.currentSceneIndex); }, calculateNextSceneIndex: function(sceneIndex) { var nextSceneIndex = this.calculateNextSceneIndex_internal(sceneIndex); return nextSceneIndex; }, calculateNextSceneIndex_internal: function(sceneIndex) { var nextSceneIndex = -1; if (sceneIndex < this.script.lastSceneIndex) { nextSceneIndex = sceneIndex + 1; } else { if (this.script.loopSlideshow) { nextSceneIndex = 0; } else { nextSceneIndex = -1; } } return nextSceneIndex; }, updateSlideNumber: function() { var adjustedSceneIndex = this.currentSceneIndex; if (this.state === kShowControllerState_IdleAtFinalState) { // because we're waiting at end state, we need to add one... adjustedSceneIndex = this.nextSceneIndex; } var newSlideIndex = this.scriptManager.slideIndexFromSceneIndex(adjustedSceneIndex); if (this.firstSlide) { this.displayManager.hideWaitingIndicator(); runInNextEventLoop((function() { this.startSoundTrack(); this.displayManager.clearLaunchMode(); }).bind(this)); this.firstSlide = false; } if (this.currentSlideIndex != newSlideIndex) { this.previousSlideIndex = this.currentSlideIndex; this.currentSlideIndex = newSlideIndex; this.delegate.propertyChanged(kPropertyName_currentSlide, this.currentSlideIndex + 1); // fire SlideIndexDidChangeEvent document.fire(kSlideIndexDidChangeEvent, { slideIndex: this.currentSlideIndex }); } }, updateNavigationButtons: function() { var sceneIndexToUse = this.currentSceneIndex; if (this.state === kShowControllerState_IdleAtFinalState) { sceneIndexToUse++; } this.updateWindowHistory(sceneIndexToUse); var enableBackwardButton = false; var enableForwardButton = false if (this.script.lastSceneIndex === -1) { // this slideshow has only 1 slide with no builds, both buttons are // disabled enableForwardButton = false; enableBackwardButton = false; } else if (this.script.loopSlideshow) { // this is a looping slideshow, both buttons are ALWAYS enabled enableForwardButton = true; enableBackwardButton = true; } else { if (sceneIndexToUse > 0) { // sceneIndexToUse > 0, so enable backward button enableBackwardButton = true; } if (sceneIndexToUse === 0 && this.script.lastSceneIndex === 0) { // sceneIndexToUse & lastSceneIndex are both 0 - show with 1 // slide with 1 build, so enable forward button enableForwardButton = true; } else if (this.currentSceneIndex < this.script.lastSceneIndex) { // currentSceneIndex < lastSceneIndex, so enable forward button enableForwardButton = true; } else if (this.currentSceneIndex === this.script.lastSceneIndex) { if (this.state === kShowControllerState_IdleAtInitialState) { // currentSceneIndex === lastSceneIndex, but we're at the // intitial state, so enable forward button enableForwardButton = true; } else { // currentSceneIndex === lastSceneIndex, and we're at the // final state, so disable forward button enableForwardButton = false; } } else { // currentSceneIndex > lastSceneIndex, show with 1 slide and no // builds, so disable forward button enableForwardButton = false; } } }, playCurrentScene: function(hyperlinkEventInfo) { var previousState = this.state; var sceneIndexToJump; var delay = 0; var duration = this.playbackController.eventOverallEndTime(); this.changeState(kShowControllerState_Playing); this.clearAllHyperlinks(); if (this.helpPlacard.isShowing) { this.helpPlacard.hide(); } if (this.slideNumberDisplay.isShowing) { this.slideNumberDisplay.hide(); } if (hyperlinkEventInfo) { sceneIndexToJump = hyperlinkEventInfo.sceneIndexToJump; // clean up media cache if we are playing hyperlink transition this.resetMediaCache(); } else { sceneIndexToJump = this.nextSceneIndex; // clean up media cache if we are advancing to different slide var outgoingSlideIndex = this.scriptManager.slideIndexFromSceneIndex(this.currentSceneIndex); var incomingSlideIndex = this.scriptManager.slideIndexFromSceneIndex(sceneIndexToJump); if (outgoingSlideIndex !== incomingSlideIndex) { this.resetMediaCache(); } // for transition, the delay time is no longer coded into animations. // set delay time and duration for automaticPlay transition if (this.playbackController.kpfEvent.event.automaticPlay == true && this.playbackController.kpfEvent.event.effects[0].type === "transition") { delay = this.playbackController.kpfEvent.event.effects[0].beginTime; duration = this.playbackController.kpfEvent.event.effects[0].duration; } } if (this.animationSupported) { // animate events // Chrome chokes up in some animations if starting animations immediately after layer drawings all on the main thread // Start animations in a background thread will improve performance // see DEMO: Chrome performance issues with cube, flip, push clearTimeout(this.animateTimeout); var effects = this.playbackController.kpfEvent.event.effects; // if the event has no effects then no need to render effects if (effects.length === 0) { this.animateTimeout = setTimeout((function(){ setTimeout(this.currentSceneDidComplete.bind(this, sceneIndexToJump), duration * 1000 + 100); }).bind(this), delay * 1000); } else { var renderedEffects; // for performance consideration, render the effects as soon as we start playing so the effects are prepared and ready to animate // for blinds there is an issue with incoming particles on top of ougoing particles // since we don't want to show incoming particles before the animations start, we will render the effect later after delay as an exception case if (effects[0].type === "transition") { if (isIE || isEdge) { // Blinds fallback to dissolve on IE so ok to render here without seeing particle in reverse order renderedEffects = this.playbackController.renderEffects(); } else { if (effects[0].name != "com.apple.iWork.Keynote.BLTBlinds") { renderedEffects = this.playbackController.renderEffects(); } } } // account for transition delay and start animating the effects after transition delay this.animateTimeout = setTimeout((function(renderedEffects){ if (renderedEffects == null) { renderedEffects = this.playbackController.renderEffects(); } this.playbackController.animateEffects(renderedEffects); setTimeout(this.currentSceneDidComplete.bind(this, sceneIndexToJump), duration * 1000 + 100); }).bind(this, renderedEffects), delay * 1000); } } else { var automatic = this.script.events[this.currentSceneIndex].automaticPlay; if (sceneIndexToJump === -1) { this.updateNavigationButtons(); if (this.delegate.getKPFJsonStringForShow) { if (automatic) { setTimeout(this.exitShow.bind(this), 2000); } else { this.exitShow(); } } else { this.changeState(kShowControllerState_IdleAtInitialState); } } else { // For IE9, animate current event means jump to next slide since there is no animated end state if (automatic) { // For IE9, do a flat 2 sec delay for any automatic event to jump to next scene setTimeout( (function(){ this.changeState(kShowControllerState_IdleAtInitialState); this.jumpToScene(sceneIndexToJump, this.script.events[sceneIndexToJump].automaticPlay); }).bind(this), 2000); } else { // if it's not automatic then jump to next scene this.changeState(kShowControllerState_IdleAtInitialState); setTimeout(this.jumpToScene.bind(this, sceneIndexToJump, this.script.events[sceneIndexToJump].automaticPlay), 100); } } } }, currentSceneDidComplete: function(sceneIndexToJump) { var script = this.script; var showMode = script.showMode; // hide slide number display after playing current scene if (this.slideNumberDisplay.isShowing) { this.slideNumberDisplay.hide(); } // change state to final after animation completed this.changeState(kShowControllerState_IdleAtFinalState); if (showMode == kShowModeHyperlinksOnly || (sceneIndexToJump != -1 && sceneIndexToJump != this.nextSceneIndex)) { // if the show mode is hyperlink only // or if we play hyperlink transition that jumps to a slide which is not its next slide // then jump to the slide after transition var event = script.events[sceneIndexToJump]; var automaticPlay = event.automaticPlay == 1 || event.automaticPlay == true; this.jumpToScene(sceneIndexToJump, automaticPlay); } else if (this.nextSceneIndex === -1) { // if next index is -1 this.updateNavigationButtons(); if (this.delegate.getKPFJsonStringForShow) { this.stopSoundTrack(); this.exitShow(); } else { this.stopSoundTrack(); } } else if (script.events[this.nextSceneIndex].automaticPlay || showMode === kShowModeAutoplay) { // invoking advanceToNextBuild() on next event loop runInNextEventLoop(this.advanceToNextBuild.bind(this, "currentSceneDidComplete")); } }, resetMediaCache: function() { this.resetMovieCache(); this.resetAudioCache(); }, resetMovieCache: function() { for (var movieId in this.movieCache) { delete this.movieCache[movieId].videoElement; delete this.movieCache[movieId]; } this.movieCache = null; }, resetAudioCache: function() { for (var audioId in this.audioCache) { this.audioCache[audioId].pause(); this.audioCache[audioId].src = ""; delete this.audioCache[audioId]; } this.audioCache = null; }, updateWindowHistory: function(sceneIndex) { // update url if (typeof(window.history.replaceState) != "undefined") { var currentUrl = document.URL.split("?"); var fragments = currentUrl[0].split("#"); if (window.location.protocol !== "file:") { window.history.replaceState(null, "Keynote", fragments[0] + "#" + sceneIndex + (currentUrl[1] ? "?" + currentUrl[1] : "")); } } }, startSoundTrack: function() { if (this.script.soundtrack == null) { return; } if (this.script.soundtrack.tracks == null) { return; } if (this.script.soundtrack.mode === kSoundTrackModeOff) { return; } this.currentSoundTrackIndex = 0; this.playNextItemInSoundTrack(); }, stopSoundTrack: function() { if (this.soundTrackPlayer) { this.soundTrackPlayer.stopObserving("ended"); this.soundTrackPlayer.pause(); this.soundTrackPlayer = null; } }, playNextItemInSoundTrack: function() { var soundtrackUrl = this.script.soundtrack.tracks[this.currentSoundTrackIndex]; this.soundTrackPlayer = new Audio(); this.soundTrackPlayer.src = "../" + soundtrackUrl; this.soundTrackPlayer.volume = this.script.soundtrack.volume; this.soundTrackPlayer.observe("ended", this.soundTrackItemDidComplete.bind(this), false); this.soundTrackPlayer.load(); this.soundTrackPlayer.play(); }, soundTrackItemDidComplete: function() { // check to see if there's anything else to play this.currentSoundTrackIndex++; if (this.currentSoundTrackIndex < this.script.soundtrack.tracks.length) { this.playNextItemInSoundTrack(); } else { if (this.script.soundtrack.mode === kSoundTrackModePlayOnce) { this.soundTrackPlayer = null; } else if (this.script.soundtrack.mode === kSoundTrackModeLooping) { // nope, but we're in loop mode so take it from the top this.startSoundTrack(); } } }, processHyperlink: function(hyperlink) { var hyperlinkUrl = hyperlink.url; var hyperlinkEffect; // perform hyperlink jump if (hyperlinkUrl.indexOf("?slide=") === 0) { var key = hyperlinkUrl.substring(7); var newSlideIndex = -1; if (key === "first") { newSlideIndex = 0; } else if (key === "last") { newSlideIndex = this.script.slideCount - 1; } else { var sceneIndexToUse = this.currentSceneIndex; var nextSlideIndex = -1; switch (this.state) { case kShowControllerState_IdleAtFinalState: sceneIndexToUse = sceneIndexToUse + 1; case kShowControllerState_IdleAtInitialState: var currentSlideIndex = this.scriptManager.slideIndexFromSceneIndex(sceneIndexToUse); if (key === "next") { if (currentSlideIndex === this.script.slideCount - 1) { if (this.script.loopSlideshow) { nextSlideIndex = 0; } else { if (this.delegate.getKPFJsonStringForShow) { this.exitShow(); } } } else { nextSlideIndex = currentSlideIndex + 1; } } else if (key === "previous") { if (currentSlideIndex === 0) { if (this.script.loopSlideshow) { nextSlideIndex = this.script.slideCount - 1; } else { nextSlideIndex = 0; } } else { nextSlideIndex = currentSlideIndex - 1; } } break; default: break; } newSlideIndex = nextSlideIndex; } if (newSlideIndex != -1) { this.jumpToHyperlinkSlide(newSlideIndex, hyperlink); } } else if (hyperlinkUrl.indexOf("?slideid=") === 0) { // find by slideId var slideId = hyperlinkUrl.substring(9); var slideList = this.script.slideList; var newSlideIndex = -1; for (var i = 0, length = slideList.length; i < length; i++) { if (slideList[i] === slideId) { newSlideIndex = i; break; } } if (newSlideIndex != -1) { this.jumpToHyperlinkSlide(newSlideIndex, hyperlink); } } else if (hyperlinkUrl.indexOf("?action=retreat") === 0) { // jump to the last slide viewed if (this.lastSlideViewedIndex != -1) { this.jumpToHyperlinkSlide(this.lastSlideViewedIndex, hyperlink); } } else if (hyperlinkUrl.indexOf("?action=exitpresentation") === 0) { // exit show this.exitShow(); } else if (hyperlinkUrl.indexOf("http:") === 0 || hyperlinkUrl.indexOf("https:") === 0) { // jump to a web page window.open(hyperlinkUrl, "_blank", null); } else if (hyperlinkUrl.indexOf("mailto:") === 0) { // email link window.location = hyperlinkUrl; } }, jumpToHyperlinkSlide: function(slideIndexToJump, hyperlink) { //var hyperlinkEffects = hyperlink.effects; var hyperlinkEvents = hyperlink.events; var sceneIndexToJump = this.script.sceneIndexFromSlideIndexLookup[slideIndexToJump]; if (hyperlinkEvents) { // if hyperlink has effect then display scene and use hyperlink event var slideIdToJump = this.script.slideList[slideIndexToJump]; var hyperlinkEvent = hyperlinkEvents[slideIdToJump]; if (hyperlinkEvent) { var sceneIndexOfHyperlink = this.currentSceneIndex; switch (this.state) { case kShowControllerState_IdleAtFinalState: if (sceneIndexOfHyperlink < this.script.numScenes - 1) { sceneIndexOfHyperlink = sceneIndexOfHyperlink + 1; } else { if (this.script.loopSlideshow) { sceneIndexOfHyperlink = 0; } } case kShowControllerState_IdleAtInitialState: var slideIndexOfHyperlink = this.script.slideIndexFromSceneIndexLookup[sceneIndexOfHyperlink]; var slideIdOfHyperlink = this.script.slideList[slideIndexOfHyperlink]; var kpfEvent = new KPFEvent({ "slideId": slideIdOfHyperlink, "slideIndex": slideIndexOfHyperlink, "sceneIndex": sceneIndexOfHyperlink, "event": hyperlinkEvent, "animationSupported": this.animationSupported }); // display scene that contains hyperlink and use hyperlink event this.displayScene(sceneIndexOfHyperlink, kpfEvent); // play current hyperlink scene this.playCurrentScene({"sceneIndexToJump": sceneIndexToJump}); break; default: return; } } else { // if no hyperlink effect, just jump to slide var event = this.script.events[sceneIndexToJump]; var automaticPlay = event.automaticPlay == 1 || event.automaticPlay == true; this.jumpToSlide(slideIndexToJump + 1, automaticPlay); } } else { // if no hyperlink effect, just jump to slide var event = this.script.events[sceneIndexToJump]; var automaticPlay = event.automaticPlay == 1 || event.automaticPlay == true; this.jumpToSlide(slideIndexToJump + 1, automaticPlay); } }, addMovieHyperlink: function(targetRectangle, url) { var newHyperlink = { targetRectangle: targetRectangle, url: url }; this.movieHyperlinks.push(newHyperlink); }, clearMovieHyperlinks: function() { delete this.movieHyperlinks; this.movieHyperlinks = new Array(); }, clearAllHyperlinks: function() { this.stageManager.clearAllHyperlinks(); delete this.activeHyperlinks; this.activeHyperlinks = new Array(); }, findHyperlinkAtCoOrds: function(showCoOrds) { var numHyperlinks = this.activeHyperlinks != null ? this.activeHyperlinks.length : 0; for (var i = numHyperlinks; i > 0; i--) { var hyperlink = this.activeHyperlinks[i - 1]; var hyperlinkRect = hyperlink.targetRectangle; hyperlinkLeft = Math.floor(hyperlinkRect.x); hyperlinkTop = Math.floor(hyperlinkRect.y); hyperlinkRight = hyperlinkLeft + Math.floor(hyperlinkRect.width); hyperlinkBottom = hyperlinkTop + Math.floor(hyperlinkRect.height); if ((showCoOrds.pointX >= hyperlinkLeft) && (showCoOrds.pointX <= hyperlinkRight) && (showCoOrds.pointY >= hyperlinkTop) && (showCoOrds.pointY <= hyperlinkBottom)) { return hyperlink; } } return null; }, createHyperlinksForCurrentState: function(context) { var sceneIndexOfHyperlinks = -1; switch (this.state) { case kShowControllerState_IdleAtInitialState: // use current scene index sceneIndexOfHyperlinks = this.currentSceneIndex; break; case kShowControllerState_IdleAtFinalState: // idle at final state, grab hyperlink from appropriate scene if (this.currentSceneIndex < this.script.lastSceneIndex) { sceneIndexOfHyperlinks = this.currentSceneIndex + 1; } else { // check if hyperlinks only mode if (this.script.showMode == kShowModeHyperlinksOnly) { sceneIndexOfHyperlinks = this.currentSceneIndex; } else { // check if loop slide show if (this.script.loopSlideshow) { sceneIndexOfHyperlinks = 0; } } } break; default: break; } if (sceneIndexOfHyperlinks != -1) { this.clearAllHyperlinks(); this.createHyperlinks(sceneIndexOfHyperlinks); } }, createHyperlinks: function(hyperlinkSceneIndex) { if (hyperlinkSceneIndex === -1) { return; } var eventTimeLine = this.script.events[hyperlinkSceneIndex]; if (eventTimeLine == null) { return; } var hyperlinks = eventTimeLine.hyperlinks; if (hyperlinks == null) { return; } var numHyperlinks = hyperlinks.length; var iHyperlink; var kMinHyperlinkWidth = 150; var kMinHyperlinkHeight = 50; var showWidth = this.displayManager.showWidth; var showHeight = this.displayManager.showHeight; for (iHyperlink = 0; iHyperlink < numHyperlinks; iHyperlink++) { var hyperlink = hyperlinks[iHyperlink]; var hyperlinkRect = hyperlink.targetRectangle; var activeHyperlink = { targetRectangle: hyperlinkRect, events: hyperlink.events, url: hyperlink.url }; var spaceOnLeft = hyperlinkRect.x; var spaceOnTop = hyperlinkRect.y; var spaceOnRight = showWidth - (hyperlinkRect.x + hyperlinkRect.width); var spaceOnBottom = showHeight - (hyperlinkRect.y + hyperlinkRect.top); this.stageManager.addHyperlink(activeHyperlink.targetRectangle); this.activeHyperlinks[iHyperlink] = activeHyperlink; } if (this.movieHyperlinks.length > 0) { for (var iMovieHyperlink = 0; iMovieHyperlink < this.movieHyperlinks.length; iMovieHyperlink++) { var movieHyperlink = this.movieHyperlinks[iMovieHyperlink]; this.stageManager.addHyperlink(movieHyperlink.targetRectangle); this.activeHyperlinks[iHyperlink++] = movieHyperlink; } } } });