From 643bda60954570c3a4de7629b7c61888fae5e962 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 25 Nov 2014 16:00:21 +0000 Subject: [PATCH] add pdfng module and use it by default --- services/web/public/coffee/base.coffee | 3 +- services/web/public/coffee/ide.coffee | 2 +- .../public/coffee/ide/pdfng/PdfManager.coffee | 21 ++ .../pdfng/controllers/PdfController.coffee | 293 ++++++++++++++ .../PdfViewToggleController.coffee | 17 + .../pdfng/directives/pdfAnnotations.coffee | 42 +++ .../ide/pdfng/directives/pdfHighlights.coffee | 27 ++ .../coffee/ide/pdfng/directives/pdfJs.coffee | 198 ++++++++++ .../ide/pdfng/directives/pdfPage.coffee | 80 ++++ .../ide/pdfng/directives/pdfRenderer.coffee | 185 +++++++++ .../ide/pdfng/directives/pdfTextLayer.coffee | 207 ++++++++++ .../ide/pdfng/directives/pdfViewer.coffee | 357 ++++++++++++++++++ 12 files changed, 1430 insertions(+), 2 deletions(-) create mode 100644 services/web/public/coffee/ide/pdfng/PdfManager.coffee create mode 100644 services/web/public/coffee/ide/pdfng/controllers/PdfController.coffee create mode 100644 services/web/public/coffee/ide/pdfng/controllers/PdfViewToggleController.coffee create mode 100644 services/web/public/coffee/ide/pdfng/directives/pdfAnnotations.coffee create mode 100644 services/web/public/coffee/ide/pdfng/directives/pdfHighlights.coffee create mode 100644 services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee create mode 100644 services/web/public/coffee/ide/pdfng/directives/pdfPage.coffee create mode 100644 services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee create mode 100644 services/web/public/coffee/ide/pdfng/directives/pdfTextLayer.coffee create mode 100644 services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee diff --git a/services/web/public/coffee/base.coffee b/services/web/public/coffee/base.coffee index c79f8e6d45..6859e31d34 100644 --- a/services/web/public/coffee/base.coffee +++ b/services/web/public/coffee/base.coffee @@ -11,6 +11,7 @@ define [ "underscore" "ngSanitize" "ipCookie" + "pdfViewerApp" ]) - return App \ No newline at end of file + return App diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 17142eb51d..75bdf68cae 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -6,7 +6,7 @@ define [ "ide/online-users/OnlineUsersManager" "ide/track-changes/TrackChangesManager" "ide/permissions/PermissionsManager" - "ide/pdf/PdfManager" + "ide/pdfng/PdfManager" "ide/binary-files/BinaryFilesManager" "ide/settings/index" "ide/share/index" diff --git a/services/web/public/coffee/ide/pdfng/PdfManager.coffee b/services/web/public/coffee/ide/pdfng/PdfManager.coffee new file mode 100644 index 0000000000..0c3fd7f19b --- /dev/null +++ b/services/web/public/coffee/ide/pdfng/PdfManager.coffee @@ -0,0 +1,21 @@ +define [ + "ide/pdfng/controllers/PdfController" + "ide/pdfng/controllers/PdfViewToggleController" + "ide/pdfng/directives/pdfJs" +], () -> + class PdfManager + constructor: (@ide, @$scope) -> + @$scope.pdf = + url: null # Pdf Url + error: false # Server error + timeout: false # Server timed out + failure: false # PDF failed to compile + compiling: false + uncompiled: true + logEntries: [] + logEntryAnnotations: {} + rawLog: "" + view: null # 'pdf' 'logs' + showRawLog: false + highlights: [] + position: null diff --git a/services/web/public/coffee/ide/pdfng/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdfng/controllers/PdfController.coffee new file mode 100644 index 0000000000..e5eaf6f352 --- /dev/null +++ b/services/web/public/coffee/ide/pdfng/controllers/PdfController.coffee @@ -0,0 +1,293 @@ +define [ + "base" + "libs/latex-log-parser" +], (App, LogParser) -> + App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking) -> + autoCompile = true + $scope.$on "project:joined", () -> + return if !autoCompile + autoCompile = false + $scope.recompile(isAutoCompile: true) + $scope.hasPremiumCompile = $scope.project.features.compileGroup == "priority" + + sendCompileRequest = (options = {}) -> + url = "/project/#{$scope.project_id}/compile" + if options.isAutoCompile + url += "?auto_compile=true" + return $http.post url, { + settingsOverride: + rootDoc_id: options.rootDocOverride_id or null + _csrf: window.csrfToken + } + + parseCompileResponse = (response) -> + # Reset everything + $scope.pdf.error = false + $scope.pdf.timedout = false + $scope.pdf.failure = false + $scope.pdf.uncompiled = false + $scope.pdf.url = null + + if response.status == "timedout" + $scope.pdf.timedout = true + else if response.status == "autocompile-backoff" + $scope.pdf.uncompiled = true + else if response.status == "failure" + $scope.pdf.failure = true + fetchLogs() + else if response.status == "success" + $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf?cache_bust=#{Date.now()}" + fetchLogs() + + IGNORE_FILES = ["output.fls", "output.fdb_latexmk"] + $scope.pdf.outputFiles = [] + for file in response.outputFiles + if IGNORE_FILES.indexOf(file.path) == -1 + # Turn 'output.blg' into 'blg file'. + if file.path.match(/^output\./) + file.name = "#{file.path.replace(/^output\./, "")} file" + else + file.name = file.path + $scope.pdf.outputFiles.push file + + fetchLogs = () -> + $http.get "/project/#{$scope.project_id}/output/output.log" + .success (log) -> + $scope.pdf.rawLog = log + logEntries = LogParser.parse(log, ignoreDuplicates: true) + $scope.pdf.logEntries = logEntries + $scope.pdf.logEntries.all = logEntries.errors.concat(logEntries.warnings).concat(logEntries.typesetting) + + $scope.pdf.logEntryAnnotations = {} + for entry in logEntries.all + entry.file = normalizeFilePath(entry.file) + + entity = ide.fileTreeManager.findEntityByPath(entry.file) + if entity? + $scope.pdf.logEntryAnnotations[entity.id] ||= [] + $scope.pdf.logEntryAnnotations[entity.id].push { + row: entry.line - 1 + type: if entry.level == "error" then "error" else "warning" + text: entry.message + } + + .error () -> + $scope.pdf.logEntries = [] + $scope.pdf.rawLog = "" + + getRootDocOverride_id = () -> + doc = ide.editorManager.getCurrentDocValue() + return null if !doc? + for line in doc.split("\n") + match = line.match /(.*)\\documentclass/ + if match and !match[1].match /%/ + return ide.editorManager.getCurrentDocId() + return null + + normalizeFilePath = (path) -> + path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "") + path = path.replace(/^\/compile\//, "") + + rootDocDirname = ide.fileTreeManager.getRootDocDirname() + if rootDocDirname? + path = path.replace(/^\.\//, rootDocDirname + "/") + + return path + + compileCount = 0 + $scope.recompile = (options = {}) -> + return if $scope.pdf.compiling + $scope.pdf.compiling = true + + if !options.isAutoCompile + compileCount++ + if compileCount == 1 + event_tracking.send('editor-interaction', 'single-compile') + else if compileCount == 3 + event_tracking.send('editor-interaction', 'multi-compile') + + options.rootDocOverride_id = getRootDocOverride_id() + + sendCompileRequest(options) + .success (data) -> + $scope.pdf.view = "pdf" + $scope.pdf.compiling = false + parseCompileResponse(data) + .error () -> + $scope.pdf.compiling = false + $scope.pdf.error = true + + # This needs to be public. + ide.$scope.recompile = $scope.recompile + + $scope.clearCache = () -> + $http { + url: "/project/#{$scope.project_id}/output" + method: "DELETE" + headers: + "X-Csrf-Token": window.csrfToken + } + + $scope.toggleLogs = () -> + if !$scope.pdf.view? or $scope.pdf.view == "pdf" + $scope.pdf.view = "logs" + else + $scope.pdf.view = "pdf" + + $scope.showPdf = () -> + $scope.pdf.view = "pdf" + + $scope.toggleRawLog = () -> + $scope.pdf.showRawLog = !$scope.pdf.showRawLog + + $scope.openOutputFile = (file) -> + window.open("/project/#{$scope.project_id}/output/#{file.path}") + + $scope.openClearCacheModal = () -> + modalInstance = $modal.open( + templateUrl: "clearCacheModalTemplate" + controller: "ClearCacheModalController" + scope: $scope + ) + + $scope.syncToCode = (position) -> + synctex + .syncToCode(position) + .then (data) -> + {doc, line} = data + ide.editorManager.openDoc(doc, gotoLine: line) + + $scope.switchToFlatLayout = () -> + $scope.ui.pdfLayout = 'flat' + $scope.ui.view = 'pdf' + $.localStorage "pdf.layout", "flat" + + $scope.switchToSideBySideLayout = () -> + $scope.ui.pdfLayout = 'sideBySide' + $scope.ui.view = 'editor' + $.localStorage "pdf.layout", "split" + + if pdfLayout = $.localStorage("pdf.layout") + $scope.switchToSideBySideLayout() if pdfLayout == "split" + $scope.switchToFlatLayout() if pdfLayout == "flat" + else + $scope.switchToSideBySideLayout() + + App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) -> + synctex = + syncToPdf: (cursorPosition) -> + deferred = $q.defer() + + doc_id = ide.editorManager.getCurrentDocId() + if !doc_id? + deferred.reject() + return deferred.promise + doc = ide.fileTreeManager.findEntityById(doc_id) + if !doc? + deferred.reject() + return deferred.promise + path = ide.fileTreeManager.getEntityPath(doc) + if !path? + deferred.reject() + return deferred.promise + + # If the root file is folder/main.tex, then synctex sees the + # path as folder/./main.tex + rootDocDirname = ide.fileTreeManager.getRootDocDirname() + if rootDocDirname? and rootDocDirname != "" + path = path.replace(RegExp("^#{rootDocDirname}"), "#{rootDocDirname}/.") + + {row, column} = cursorPosition + + $http({ + url: "/project/#{ide.project_id}/sync/code", + method: "GET", + params: { + file: path + line: row + 1 + column: column + } + }) + .success (data) -> + deferred.resolve(data.pdf or []) + .error (error) -> + deferred.reject(error) + + return deferred.promise + + syncToCode: (position, options = {}) -> + deferred = $q.defer() + if !position? + deferred.reject() + return deferred.promise + + # It's not clear exactly where we should sync to if it wasn't directly + # clicked on, but a little bit down from the very top seems best. + if options.includeVisualOffset + position.offset.top = position.offset.top + 80 + + $http({ + url: "/project/#{ide.project_id}/sync/pdf", + method: "GET", + params: { + page: position.page + 1 + h: position.offset.left.toFixed(2) + v: position.offset.top.toFixed(2) + } + }) + .success (data) -> + if data.code? and data.code.length > 0 + doc = ide.fileTreeManager.findEntityByPath(data.code[0].file) + return if !doc? + deferred.resolve({doc: doc, line: data.code[0].line}) + .error (error) -> + deferred.reject(error) + + return deferred.promise + + return synctex + ] + + App.controller "PdfSynctexController", ["$scope", "synctex", "ide", ($scope, synctex, ide) -> + @cursorPosition = null + ide.$scope.$on "cursor:editor:update", (event, @cursorPosition) => + + $scope.syncToPdf = () => + return if !@cursorPosition? + synctex + .syncToPdf(@cursorPosition) + .then (highlights) -> + $scope.pdf.highlights = highlights + + $scope.syncToCode = () -> + synctex + .syncToCode($scope.pdf.position, includeVisualOffset: true) + .then (data) -> + {doc, line} = data + ide.editorManager.openDoc(doc, gotoLine: line) + ] + + App.controller "PdfLogEntryController", ["$scope", "ide", ($scope, ide) -> + $scope.openInEditor = (entry) -> + entity = ide.fileTreeManager.findEntityByPath(entry.file) + return if !entity? or entity.type != "doc" + if entry.line? + line = entry.line + ide.editorManager.openDoc(entity, gotoLine: line) + ] + + App.controller 'ClearCacheModalController', ["$scope", "$modalInstance", ($scope, $modalInstance) -> + $scope.state = + inflight: false + + $scope.clear = () -> + $scope.state.inflight = true + $scope + .clearCache() + .then () -> + $scope.state.inflight = false + $modalInstance.close() + + $scope.cancel = () -> + $modalInstance.dismiss('cancel') + ] \ No newline at end of file diff --git a/services/web/public/coffee/ide/pdfng/controllers/PdfViewToggleController.coffee b/services/web/public/coffee/ide/pdfng/controllers/PdfViewToggleController.coffee new file mode 100644 index 0000000000..156d46dd3e --- /dev/null +++ b/services/web/public/coffee/ide/pdfng/controllers/PdfViewToggleController.coffee @@ -0,0 +1,17 @@ +define [ + "base" +], (App) -> + App.controller "PdfViewToggleController", ($scope) -> + $scope.togglePdfView = () -> + if $scope.ui.view == "pdf" + $scope.ui.view = "editor" + else + $scope.ui.view = "pdf" + + $scope.fileTreeClosed = false + $scope.$on "layout:main:resize", (e, state) -> + if state.west.initClosed + $scope.fileTreeClosed = true + else + $scope.fileTreeClosed = false + $scope.$apply() \ No newline at end of file diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfAnnotations.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfAnnotations.coffee new file mode 100644 index 0000000000..a2d9d1751a --- /dev/null +++ b/services/web/public/coffee/ide/pdfng/directives/pdfAnnotations.coffee @@ -0,0 +1,42 @@ +app = angular.module 'pdfAnnotations', [] + +app.factory 'pdfAnnotations', [ () -> + class pdfAnnotations + + @EXTERNAL_LINK_TARGET = "_blank"; + + constructor: (options) -> + @annotationsLayerDiv = options.annotations; + @viewport = options.viewport + @navigateFn = options.navigateFn + + setAnnotations: (annotations) -> + for annotation in annotations + switch annotation.subtype + when 'Link' then @addLink(annotation); + when 'Text' then continue + + addLink: (link) -> + element = @buildLinkElementFromRect(link.rect); + @setLinkTarget(element, link); + @annotationsLayerDiv.appendChild(element); + + buildLinkElementFromRect: (rect) -> + rect = @viewport.convertToViewportRectangle(rect); + rect = PDFJS.Util.normalizeRect(rect); + element = document.createElement("a"); + element.style.left = Math.floor(rect[0]) + 'px'; + element.style.top = Math.floor(rect[1]) + 'px'; + element.style.width = Math.ceil(rect[2] - rect[0]) + 'px'; + element.style.height = Math.ceil(rect[3] - rect[1]) + 'px'; + element + + setLinkTarget: (element, link) -> + if link.url + element.href = link.url; + element.target = @EXTERNAL_LINK_TARGET; + else if (link.dest) + element.href = "#" + link.dest; + element.onclick = (e) => + @navigateFn link +] diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfHighlights.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfHighlights.coffee new file mode 100644 index 0000000000..e3fd2f3ac2 --- /dev/null +++ b/services/web/public/coffee/ide/pdfng/directives/pdfHighlights.coffee @@ -0,0 +1,27 @@ +app = angular.module 'pdfHighlights', [] + +app.factory 'pdfHighlights', [ () -> + class pdfHighlights + + constructor: (options) -> + @highlightsLayerDiv = options.highlights + @viewport = options.viewport + @highlightElements = [] + + addHighlight: (left, top, width, height) -> + rect = @viewport.convertToViewportRectangle([left, top, left + width, top + height]) + rect = PDFJS.Util.normalizeRect(rect) + element = document.createElement("div") + element.style.left = Math.floor(rect[0]) + 'px' + element.style.top = Math.floor(rect[1]) + 'px' + element.style.width = Math.ceil(rect[2] - rect[0]) + 'px' + element.style.height = Math.ceil(rect[3] - rect[1]) + 'px' + @highlightElements.push(element) + @highlightsLayerDiv.appendChild(element) + element + + clearHighlights: () -> + for h in @highlightElements + h.remove() + @highlightElements = [] +] diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee new file mode 100644 index 0000000000..32b09dc293 --- /dev/null +++ b/services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee @@ -0,0 +1,198 @@ +define [ + "base" + "ide/pdfng/directives/pdfViewer" + "ide/pdfng/directives/pdfPage" + "ide/pdfng/directives/pdfRenderer" + "ide/pdfng/directives/pdfTextLayer" + "ide/pdfng/directives/pdfAnnotations" + "ide/pdfng/directives/pdfHighlights" + "libs/pdf" + "text!libs/pdfListView/TextLayer.css" + "text!libs/pdfListView/AnnotationsLayer.css" + "text!libs/pdfListView/HighlightsLayer.css" +], ( + App + pdfViewerApp + pdfPage + pdfRenderer + pdfTextLayer + pdfAnnotations + pdfHighlights + pdf + textLayerCss + annotationsLayerCss + highlightsLayerCss +) -> + if PDFJS? + PDFJS.workerSrc = window.pdfJsWorkerPath + PDFJS.disableAutoFetch = true + + style = $("