From 821413607a0718156f9d25d895e89b1c3d37aa8b Mon Sep 17 00:00:00 2001 From: Daniel Narvaez Date: Wed, 06 Feb 2013 13:49:14 +0000 Subject: Copy various bits from gaia --- (limited to 'apps/system/js/cards_view.js') diff --git a/apps/system/js/cards_view.js b/apps/system/js/cards_view.js new file mode 100644 index 0000000..8bce02b --- /dev/null +++ b/apps/system/js/cards_view.js @@ -0,0 +1,676 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +// +// CardsView is responsible for managing opened apps +// + +'use strict'; + +var CardsView = (function() { + + //display icon of an app on top of app's card + var DISPLAY_APP_ICON = false; + var USER_DEFINED_ORDERING = false; + // If 'true', scrolling moves the list one card + // at time, and snaps the list so the current card + // is centered in the view + // If 'false', use free, physics-based scrolling + // (Gaia default) + var SNAPPING_SCROLLING = true; + // if 'true' user can close the app + // by dragging it upwards + var MANUAL_CLOSING = true; + + var cardsView = document.getElementById('cards-view'); + var screenElement = document.getElementById('screen'); + var cardsList = cardsView.firstElementChild; + var displayedApp; + var runningApps; + // Unkillable apps which have attention screen now + var attentionScreenApps = []; + // Card which we are re-ordering now + var reorderedCard = null; + var currentDisplayed = 0; + // Timer between scrolling CardList further, + // when reordering Cards + var scrollWhileSortingTimer; + // We don't allow user to scroll CardList + // before the timer ticks while in reordering + // mode + var allowScrollingWhileSorting = false; + // Initial margin of the reordered card + var dragMargin = 0; + // Are we reordering or removing the card now? + var draggingCardUp = false; + // Are we moving card left or right? + var sortingDirection; + // List of sorted apps + var userSortedApps = []; + var HVGA = document.documentElement.clientWidth < 480; + var cardsViewShown = false; + + // init events + var gd = new GestureDetector(cardsView); + gd.startDetecting(); + + // A list of all the URLs we've created via URL.createObjectURL which we + // haven't yet revoked. + var screenshotObjectURLs = []; + + /* + * Returns an icon URI + * + * @param{String} the app's origin + */ + function getIconURI(origin) { + var icons = runningApps[origin].manifest.icons; + if (!icons) { + return null; + } + + var sizes = Object.keys(icons).map(function parse(str) { + return parseInt(str, 10); + }); + + sizes.sort(function(x, y) { return y - x; }); + + var index = sizes[(HVGA) ? sizes.length - 1 : 0]; + var iconPath = icons[index]; + + if (iconPath.indexOf('data:') !== 0) { + iconPath = origin + iconPath; + } + + return iconPath; + } + + // Build and display the card switcher overlay + // Note that we rebuild the switcher each time we need it rather + // than trying to keep it in sync with app launches. Performance is + // not an issue here given that the user has to hold the HOME button down + // for one second before the switcher will appear. + function showCardSwitcher() { + if (cardSwitcherIsShown()) + return; + + // events to handle + window.addEventListener('lock', CardsView); + + // Close utility tray if it is opened. + UtilityTray.hide(true); + + // Apps info from WindowManager + displayedApp = WindowManager.getDisplayedApp(); + currentDisplayed = 0; + runningApps = WindowManager.getRunningApps(); + + // Switch to homescreen + WindowManager.launch(null); + cardsViewShown = true; + + // If user is not able to sort apps manualy, + // display most recetly active apps on the far left + if (!USER_DEFINED_ORDERING) { + var sortable = []; + for (var origin in runningApps) + sortable.push({origin: origin, app: runningApps[origin]}); + + sortable.sort(function(a, b) { + return b.app.launchTime - a.app.launchTime; + }); + runningApps = {}; + + // I assume that object properties are enumerated in + // the same order they were defined. + // There is nothing about that in spec, but I've never + // seen any unexpected behavior. + sortable.forEach(function(element) { + runningApps[element.origin] = element.app; + }); + + // First add an item to the cardsList for each running app + for (var origin in runningApps) { + addCard(origin, runningApps[origin], function showCards() { + screenElement.classList.add('cards-view'); + cardsView.classList.add('active'); + }); + } + + } else { // user ordering + + // first run + if (userSortedApps.length === 0) { + for (var origin in runningApps) { + userSortedApps.push(origin); + } + } else { + for (var origin in runningApps) { + // if we have some new app opened + if (userSortedApps.indexOf(origin) === -1) { + userSortedApps.push(origin); + } + } + } + + userSortedApps.forEach(function(origin) { + addCard(origin, runningApps[origin], function showCards() { + screenElement.classList.add('cards-view'); + cardsView.classList.add('active'); + }); + }); + + cardsView.addEventListener('contextmenu', CardsView); + + } + + if (SNAPPING_SCROLLING) { + cardsView.style.overflow = 'hidden'; //disabling native scrolling + } + + if (SNAPPING_SCROLLING || MANUAL_CLOSING) { + cardsView.addEventListener('mousedown', CardsView); + } + + // Make sure we're in portrait mode + screen.mozLockOrientation('portrait-primary'); + + // If there is a displayed app, take keyboard focus away + if (displayedApp) + runningApps[displayedApp].frame.blur(); + + function addCard(origin, app, displayedAppCallback) { + // Display card switcher background first to make user focus on the + // frame closing animation without disturbing by homescreen display. + if (displayedApp == origin && displayedAppCallback) { + setTimeout(displayedAppCallback); + } + // Not showing homescreen + if (app.frame.classList.contains('homescreen')) { + return; + } + + // Build a card representation of each window. + // And add it to the card switcher + var card = document.createElement('li'); + card.classList.add('card'); + card.dataset.origin = origin; + + //display app icon on the tab + if (DISPLAY_APP_ICON) { + var iconURI = getIconURI(origin); + if (iconURI) { + var appIcon = document.createElement('img'); + appIcon.classList.add('appIcon'); + appIcon.src = iconURI; + card.appendChild(appIcon); + } + } + + var title = document.createElement('h1'); + title.textContent = app.name; + card.appendChild(title); + + var frameForScreenshot = app.iframe; + + if (PopupManager.getPopupFromOrigin(origin)) { + var popupFrame = PopupManager.getPopupFromOrigin(origin); + frameForScreenshot = popupFrame; + + var subtitle = document.createElement('p'); + subtitle.textContent = + PopupManager.getOpenedOriginFromOpener(origin); + card.appendChild(subtitle); + card.classList.add('popup'); + } else if (getOffOrigin(app.frame.dataset.url ? + app.frame.dataset.url : app.frame.src, origin)) { + var subtitle = document.createElement('p'); + subtitle.textContent = getOffOrigin(app.frame.dataset.url ? + app.frame.dataset.url : app.frame.src, origin); + card.appendChild(subtitle); + } + + if (TrustedUIManager.hasTrustedUI(origin)) { + var popupFrame = TrustedUIManager.getDialogFromOrigin(origin); + frameForScreenshot = popupFrame.frame; + var header = document.createElement('section'); + header.setAttribute('role', 'region'); + header.classList.add('skin-organic'); + header.innerHTML = '

' + popupFrame.name; + header.innerHTML += '

'; + card.appendChild(header); + card.classList.add('trustedui'); + } else if (attentionScreenApps.indexOf(origin) == -1) { + var closeButton = document.createElement('div'); + closeButton.classList.add('close-card'); + card.appendChild(closeButton); + } + + cardsList.appendChild(card); + // rect is the final size (considering CSS transform) of the card. + var rect = card.getBoundingClientRect(); + + // And then switch it with screenshots when one will be ready + // (instead of -moz-element backgrounds) + frameForScreenshot.getScreenshot(rect.width, rect.height).onsuccess = + function gotScreenshot(screenshot) { + if (screenshot.target.result) { + var objectURL = URL.createObjectURL(screenshot.target.result); + screenshotObjectURLs.push(objectURL); + card.style.backgroundImage = 'url(' + objectURL + ')'; + } + }; + + // Set up event handling + // A click elsewhere in the card switches to that task + card.addEventListener('tap', runApp); + } + } + + function runApp(e) { + // Handle close events + if (e.target.classList.contains('close-card')) { + var element = e.target.parentNode; + cardsList.removeChild(element); + closeApp(element, true); + return; + } + + var origin = this.dataset.origin; + alignCard(currentDisplayed, function cardAligned() { + WindowManager.launch(origin); + }); + } + + function closeApp(element, removeImmediately) { + // Stop the app itself + WindowManager.kill(element.dataset.origin); + + // Fix for non selectable cards when we remove the last card + // Described in https://bugzilla.mozilla.org/show_bug.cgi?id=825293 + if (cardsList.children.length === currentDisplayed) { + currentDisplayed--; + } + + // If there are no cards left, then dismiss the task switcher. + if (!cardsList.children.length) + hideCardSwitcher(removeImmediately); + } + + function getOriginObject(url) { + var parser = document.createElement('a'); + parser.href = url; + + return { + protocol: parser.protocol, + hostname: parser.hostname, + port: parser.port + }; + } + + function getOffOrigin(src, origin) { + // Use src and origin as cache key + var cacheKey = JSON.stringify(Array.prototype.slice.call(arguments)); + if (!getOffOrigin.cache[cacheKey]) { + var native = getOriginObject(origin); + var current = getOriginObject(src); + if (current.protocol == 'http:') { + // Display http:// protocol anyway + getOffOrigin.cache[cacheKey] = current.protocol + '//' + + current.hostname; + } else if (native.protocol == current.protocol && + native.hostname == current.hostname && + native.port == current.port) { + // Same origin policy + getOffOrigin.cache[cacheKey] = ''; + } else if (current.protocol == 'app:') { + // Avoid displaying app:// protocol + getOffOrigin.cache[cacheKey] = ''; + } else { + getOffOrigin.cache[cacheKey] = current.protocol + '//' + + current.hostname; + } + } + + return getOffOrigin.cache[cacheKey]; + } + + getOffOrigin.cache = {}; + + function hideCardSwitcher(removeImmediately) { + if (!cardSwitcherIsShown()) + return; + + // events to handle + window.removeEventListener('lock', CardsView); + + // Make the cardsView overlay inactive + cardsView.classList.remove('active'); + cardsViewShown = false; + + // Release our screenshot blobs. + screenshotObjectURLs.forEach(function(url) { + URL.revokeObjectURL(url); + }); + screenshotObjectURLs = []; + + // And remove all the cards from the document after the transition + function removeCards() { + cardsView.removeEventListener('transitionend', removeCards); + screenElement.classList.remove('cards-view'); + + while (cardsList.firstElementChild) { + cardsList.removeChild(cardsList.firstElementChild); + } + } + if (removeImmediately) { + removeCards(); + } else { + cardsView.addEventListener('transitionend', removeCards); + } + } + + function cardSwitcherIsShown() { + return cardsViewShown; + } + + //scrolling cards + var initialCardViewPosition; + var initialTouchPosition = {}; + var threshold = window.innerWidth / 4; + // Distance after which dragged card starts moving + var moveCardThreshold = window.innerHeight / 6; + var removeCardThreshold = window.innerHeight / 4; + + function alignCard(number, callback) { + if (!cardsList.children[number]) + return; + + var scrollLeft = cardsView.scrollLeft; + var targetScrollLeft = cardsList.children[number].offsetLeft; + + if (Math.abs(scrollLeft - targetScrollLeft) < 4) { + cardsView.scrollLeft = cardsList.children[number].offsetLeft; + if (callback) + callback(); + return; + } + + cardsView.scrollLeft = scrollLeft + (targetScrollLeft - scrollLeft) / 2; + + window.mozRequestAnimationFrame(function newFrameCallback() { + alignCard(number, callback); + }); + } + + function onStartEvent(evt) { + evt.stopPropagation(); + evt.target.setCapture(true); + cardsView.addEventListener('mousemove', CardsView); + cardsView.addEventListener('swipe', CardsView); + + initialCardViewPosition = cardsView.scrollLeft; + initialTouchPosition = { + x: evt.touches ? evt.touches[0].pageX : evt.pageX, + y: evt.touches ? evt.touches[0].pageY : evt.pageY + }; + } + + function onMoveEvent(evt) { + evt.stopPropagation(); + var touchPosition = { + x: evt.touches ? evt.touches[0].pageX : evt.pageX, + y: evt.touches ? evt.touches[0].pageY : evt.pageY + }; + + if (evt.target.classList.contains('card') && MANUAL_CLOSING) { + var differenceY = initialTouchPosition.y - touchPosition.y; + if (differenceY > moveCardThreshold) { + // We don't want user to scroll the CardsView when one of the card is + // already dragger upwards + draggingCardUp = true; + evt.target.style.MozTransform = 'scale(0.6) translate(0, -' + + differenceY + 'px)'; + } + } + + // If we are not reordering or removing Cards now + // and Snapping Scrolling is enabled, we want to scroll + // the CardList + if (SNAPPING_SCROLLING && reorderedCard === null && !draggingCardUp) { + var differenceX = initialTouchPosition.x - touchPosition.x; + cardsView.scrollLeft = initialCardViewPosition + differenceX; + } + + // If re are in reordering mode (there is a DOM element in) + // reorderedCard variable) we are able to put this element somewere + // among the others + if (USER_DEFINED_ORDERING && reorderedCard !== null) { + var differenceX = touchPosition.x - initialTouchPosition.x; + // Probably there is more clever solution for calculating + // position of transformed DOM element, but this was my + // first thought and it seems to work + var moveOffset = (cardsList.children[currentDisplayed].offsetLeft / 0.6) + + differenceX - (dragMargin / 0.6); + + reorderedCard.style.MozTransform = + 'scale(0.6) translate(' + moveOffset + 'px, 0)'; + + if (Math.abs(differenceX) > threshold) { + // We don't want to jump to the next page immediately, + // We are waiting half a second for user to decide if + // he wants to leave the Card here or scroll further + if (allowScrollingWhileSorting) { + allowScrollingWhileSorting = false; + + scrollWhileSortingTimer = setTimeout(function() { + allowScrollingWhileSorting = true; + }, 500); + + if (differenceX > 0 && + currentDisplayed <= cardsList.children.length) { + currentDisplayed++; + sortingDirection = 'right'; + alignCard(currentDisplayed); + } else if (differenceX < 0 && currentDisplayed > 0) { + currentDisplayed--; + sortingDirection = 'left'; + alignCard(currentDisplayed); + } + } + } + } + } + + function onEndEvent(evt) { + evt.stopPropagation(); + var element = evt.target; + var eventDetail = evt.detail; + var direction = eventDetail.direction; + + document.releaseCapture(); + cardsView.removeEventListener('mousemove', CardsView); + cardsView.removeEventListener('swipe', CardsView); + + var touchPosition = { + x: eventDetail.end.pageX, + y: eventDetail.end.pageY + }; + + if (SNAPPING_SCROLLING && !draggingCardUp && reorderedCard === null) { + if (Math.abs(eventDetail.dx) > threshold) { + if ( + direction === 'left' && + currentDisplayed < cardsList.children.length - 1 + ) { + currentDisplayed++; + alignCard(currentDisplayed); + } else if (direction === 'right' && currentDisplayed > 0) { + currentDisplayed--; + alignCard(currentDisplayed); + } + } else { + alignCard(currentDisplayed); + } + } + + // if the element we start dragging on + // is a card and we are not in reordering mode + if ( + element.classList.contains('card') && + MANUAL_CLOSING && + reorderedCard === null + ) { + + draggingCardUp = false; + // Prevent user from closing the app with a attention screen + if (-eventDetail.dy > removeCardThreshold && + attentionScreenApps.indexOf(element.dataset.origin) == -1 + ) { + + // remove the app also from the ordering list + if ( + userSortedApps.indexOf(element.dataset.origin) !== -1 && + USER_DEFINED_ORDERING + ) { + userSortedApps.splice( + userSortedApps.indexOf(element.dataset.origin), + 1 + ); + } + + // Without removing the listener before closing card + // sometimes the 'click' event fires, even if 'mouseup' + // uses stopPropagation() + element.removeEventListener('tap', runApp); + + // Remove the icon from the task list + cardsList.removeChild(element); + + closeApp(element); + + return; + } else { + element.style.MozTransform = ''; + } + } + + if (USER_DEFINED_ORDERING && reorderedCard !== null) { + // Position of the card depends on direction of scrolling + if (sortingDirection === 'right') { + if (currentDisplayed <= cardsList.children.length) { + cardsList.insertBefore( + reorderedCard, + cardsList.children[currentDisplayed + 1] + ); + } else { + cardsList.appendChild(reorderedCard); + } + } else if (sortingDirection === 'left') { + cardsList.insertBefore( + reorderedCard, + cardsList.children[currentDisplayed] + ); + } + reorderedCard.style.MozTransform = ''; + reorderedCard.dataset['edit'] = 'false'; + reorderedCard = null; + + alignCard(currentDisplayed); + + // remove the app origin from ordering array + userSortedApps.splice( + userSortedApps.indexOf(element.dataset.origin), + 1 + ); + // and put in on the new position + userSortedApps.splice(currentDisplayed, 0, element.dataset.origin); + } + } + + function manualOrderStart(evt) { + evt.preventDefault(); + reorderedCard = evt.target; + allowScrollingWhileSorting = true; + if (reorderedCard.classList.contains('card')) { + dragMargin = reorderedCard.offsetLeft; + reorderedCard.dataset['edit'] = true; + sortingDirection = 'left'; + } + } + + window.addEventListener('applicationuninstall', + function removeUninstaledApp(evt) { + var origin = evt.detail.application.origin; + if (userSortedApps.indexOf(origin) !== -1) { + userSortedApps.splice(userSortedApps.indexOf(origin), 1); + } + }, + false); + + function cv_handleEvent(evt) { + switch (evt.type) { + case 'mousedown': + onStartEvent(evt); + break; + + case 'mousemove': + onMoveEvent(evt); + break; + + case 'swipe': + onEndEvent(evt); + break; + + case 'contextmenu': + manualOrderStart(evt); + break; + + case 'home': + if (!cardSwitcherIsShown()) + return; + + evt.stopImmediatePropagation(); + hideCardSwitcher(); + break; + + case 'lock': + case 'attentionscreenshow': + attentionScreenApps = AttentionScreen.getAttentionScreenOrigins(); + hideCardSwitcher(); + break; + + case 'attentionscreenhide': + attentionScreenApps = AttentionScreen.getAttentionScreenOrigins(); + break; + + case 'holdhome': + if (LockScreen.locked) + return; + + SleepMenu.hide(); + showCardSwitcher(); + break; + + case 'appwillopen': + hideCardSwitcher(); + break; + } + } + + // Public API of CardsView + return { + showCardSwitcher: showCardSwitcher, + hideCardSwitcher: hideCardSwitcher, + cardSwitcherIsShown: cardSwitcherIsShown, + handleEvent: cv_handleEvent + }; +})(); + +window.addEventListener('attentionscreenshow', CardsView); +window.addEventListener('attentionscreenhide', CardsView); +window.addEventListener('holdhome', CardsView); +window.addEventListener('home', CardsView); +window.addEventListener('appwillopen', CardsView); + -- cgit v0.9.1