diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index 79a128493f..c4490204ac 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -6,21 +6,21 @@ aside.file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected' tooltip-html="'"+translate('new_file').replace(' ', '
')+"'", tooltip-placement="bottom" ) - i.fa.fa-file + i.fa.fa-fw.fa-file a( href, ng-click="openNewFolderModal()", tooltip-html="'"+translate('new_folder').replace(' ', '
')+"'", tooltip-placement="bottom" ) - i.fa.fa-folder + i.fa.fa-fw.fa-folder a( href, ng-click="openUploadFileModal()", tooltip=translate('upload'), tooltip-placement="bottom" ) - i.fa.fa-upload + i.fa.fa-fw.fa-upload .toolbar-right a( @@ -30,7 +30,7 @@ aside.file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected' tooltip-placement="bottom", ng-show="multiSelectedCount == 0" ) - i.fa.fa-pencil + i.fa.fa-fw.fa-pencil a( href, ng-click="openDeleteModalForSelected()", @@ -38,7 +38,7 @@ aside.file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected' tooltip-placement="bottom", tooltip-append-to-body="true" ) - i.fa.fa-trash-o + i.fa.fa-fw.fa-trash-o .file-tree-inner( diff --git a/services/web/app/views/project/editor/pdf.pug b/services/web/app/views/project/editor/pdf.pug index 9681c4fc9a..a87b765cf2 100644 --- a/services/web/app/views/project/editor/pdf.pug +++ b/services/web/app/views/project/editor/pdf.pug @@ -1,5 +1,5 @@ div.full-size.pdf(ng-controller="PdfController") - .toolbar.toolbar-pdf + .toolbar.toolbar-pdf(ng-class="{ 'uncompiled-changes': uncompiledChanges && !autoCompileLintingError }") .btn-group.btn-recompile-group#recompile( dropdown, tooltip-html="'"+translate('recompile_pdf')+" ({{modifierKey}} + Enter)'" @@ -65,7 +65,7 @@ div.full-size.pdf(ng-controller="PdfController") tooltip=translate('stop_compile') tooltip-placement="bottom" ) - i.fa.fa-stop() + i.fa.fa-fw.fa-stop() a.log-btn( href ng-click="toggleLogs()" @@ -73,7 +73,7 @@ div.full-size.pdf(ng-controller="PdfController") tooltip=translate('logs_and_output_files') tooltip-placement="bottom" ) - i.fa.fa-file-text-o + i.fa.fa-fw.fa-file-text-o span.label( ng-show="pdf.logEntries.warnings.length + pdf.logEntries.errors.length > 0" ng-class="{\ @@ -89,9 +89,21 @@ div.full-size.pdf(ng-controller="PdfController") tooltip=translate('download_pdf') tooltip-placement="bottom" ) - i.fa.fa-download + i.fa.fa-fw.fa-download .toolbar-right + span.auto-compile-status.small( + ng-show="uncompiledChanges && !compiling && !autoCompileLintingError" + ) #{translate('uncompiled_changes')} + span.auto-compile-status.auto-compile-error.small( + ng-show="autoCompileLintingError" + tooltip-placement="bottom" + tooltip=translate("code_check_failed_explanation") + tooltip-append-to-body="true" + ) + i.fa.fa-fw.fa-exclamation-triangle + | + | #{translate("code_check_failed")} a( href, ng-click="switchToFlatLayout()" diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index afdd924588..1965eaefd8 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -352,8 +352,13 @@ define [ v: version @doc.on "change", (ops, oldSnapshot, msg) => @_applyOpsToRanges(ops, oldSnapshot, msg) + @ide.$scope.$emit "doc:changed", + doc_id: @doc_id @doc.on "flipped_pending_to_inflight", () => @trigger "flipped_pending_to_inflight" + @doc.on "saved", () => + @ide.$scope.$emit "doc:saved", + doc_id: @doc_id _onError: (error, meta = {}) -> meta.doc_id = @doc_id diff --git a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee index 63fca95856..20ccdea056 100644 --- a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee +++ b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee @@ -46,6 +46,8 @@ define [ @trigger "remoteop", args... @_doc.on "flipped_pending_to_inflight", () => @trigger "flipped_pending_to_inflight" + @_doc.on "saved", () => + @trigger "saved" @_doc.on "error", (e) => @_handleError(e) diff --git a/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee b/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee index aca3560b49..edfed10386 100644 --- a/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee +++ b/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee @@ -164,6 +164,9 @@ class Doc @inflightOp = null @inflightSubmittedIds.length = 0 + if @pendingOp == null # All ops are acked + @emit 'saved' + error = msg.error if error # The server has rejected an op from the client for some reason. diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index d5d3a41af5..bf55961589 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -5,8 +5,12 @@ define [ "libs/bib-log-parser" "services/log-hints-feedback" ], (App, Ace, HumanReadableLogs, BibLogParser) -> - AUTO_COMPILE_TIMEOUT = 5000 - OP_ACKNOWLEDGEMENT_TIMEOUT = 1100 + AUTO_COMPILE_MAX_WAIT = 5000 + # We add a 1 second debounce to sending user changes to server if they aren't + # collaborating with anyone. This needs to be higher than that, and allow for + # client to server latency, otherwise we compile before the op reaches the server + # and then again on ack. + AUTO_COMPILE_DEBOUNCE = 2000 App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, logHintsFeedback, localStorage) -> # enable per-user containers by default @@ -75,38 +79,102 @@ define [ $scope.pdf.view = 'errors' $scope.pdf.renderingError = true - autoCompileTimeout = null + autoCompileInterval = null + autoCompileIfReady = () -> + if $scope.pdf.compiling + return + + # Only checking linting if syntaxValidation is on and visible to the user + autoCompileLintingError = ide.$scope.hasLintingError and ide.$scope.settings.syntaxValidation + if $scope.autoCompileLintingError != autoCompileLintingError + $scope.$apply () -> + $scope.autoCompileLintingError = autoCompileLintingError + # We've likely been waiting a while until the user fixed the linting, but we + # don't want to compile as soon as it is fixed, so reset the timeout. + $scope.startedTryingAutoCompileAt = Date.now() + $scope.docLastChangedAt = Date.now() + if autoCompileLintingError + return + + # If there's a longish compile, don't compile immediately after if user is still typing + startedTryingAt = Math.max($scope.startedTryingAutoCompileAt, $scope.lastFinishedCompileAt || 0) + + timeSinceStartedTrying = Date.now() - startedTryingAt + timeSinceLastChange = Date.now() - $scope.docLastChangedAt + + shouldCompile = false + if timeSinceLastChange > AUTO_COMPILE_DEBOUNCE # Don't compile in the middle of the user typing + shouldCompile = true + else if timeSinceStartedTrying > AUTO_COMPILE_MAX_WAIT # Unless they type for a long time + shouldCompile = true + else if timeSinceStartedTrying < 0 or timeSinceLastChange < 0 + # If time is non-monotonic, assume that the user's system clock has been + # changed and continue with compile + shouldCompile = true + + if shouldCompile + triggerAutoCompile() + triggerAutoCompile = () -> - return if autoCompileTimeout or $scope.ui.pdfHidden + $scope.recompile(isAutoCompileOnChange: true) - timeSinceLastCompile = Date.now() - $scope.recompiledAt - # If time is non-monotonic, assume that the user's system clock has been - # changed and continue with recompile - isTimeNonMonotonic = timeSinceLastCompile < 0 + startTryingAutoCompile = () -> + return if autoCompileInterval? + $scope.startedTryingAutoCompileAt = Date.now() + autoCompileInterval = setInterval autoCompileIfReady, 200 - if isTimeNonMonotonic || timeSinceLastCompile >= AUTO_COMPILE_TIMEOUT - # If user has code check disabled, it is likely because they have - # linting errors that they are ignoring. Therefore it doesn't make sense - # to block auto compiles. It also causes problems where server-provided - # linting errors aren't cleared after typing - if (ide.$scope.settings.syntaxValidation and !ide.$scope.hasLintingError) - $scope.recompile(isAutoCompileOnChange: true) # compile if no linting errors - else if !ide.$scope.settings.syntaxValidation - $scope.recompile(isAutoCompileOnChange: true) # always recompile + stopTryingAutoCompile = () -> + clearInterval autoCompileInterval + autoCompileInterval = null + + $scope.$watch "uncompiledChanges", (uncompiledChanges) -> + if uncompiledChanges + startTryingAutoCompile() else - # Extend remainder of timeout - autoCompileTimeout = setTimeout () -> - autoCompileTimeout = null - triggerAutoCompile() - , AUTO_COMPILE_TIMEOUT - timeSinceLastCompile + stopTryingAutoCompile() - autoCompileListener = null + $scope.uncompiledChanges = false + recalculateUncompiledChanges = () -> + if $scope.ui.pdfHidden + # Don't bother auto-compiling if pdf isn't visible + $scope.uncompiledChanges = false + else if !$scope.docLastChangedAt? + $scope.uncompiledChanges = false + else if !$scope.lastStartedCompileAt? or $scope.docLastChangedAt > $scope.lastStartedCompileAt + $scope.uncompiledChanges = true + else + $scope.uncompiledChanges = false + + _updateDocLastChangedAt = () -> + $scope.docLastChangedAt = Date.now() + recalculateUncompiledChanges() + + onDocChanged = () -> + $scope.autoCompileLintingError = false + _updateDocLastChangedAt() + + onDocSaved = () -> + # We use the save as a trigger too, to account for the delay between the client + # and server. Otherwise, we might have compiled after the user made + # the change on the client, but before the server had it. + _updateDocLastChangedAt() + + onCompilingStateChanged = (compiling) -> + recalculateUncompiledChanges() + + autoCompileListeners = [] toggleAutoCompile = (enabling) -> if enabling - autoCompileListener = ide.$scope.$on "ide:opAcknowledged", _.debounce(triggerAutoCompile, OP_ACKNOWLEDGEMENT_TIMEOUT) + autoCompileListeners = [ + ide.$scope.$on "doc:changed", onDocChanged + ide.$scope.$on "doc:saved", onDocSaved + $scope.$watch "pdf.compiling", onCompilingStateChanged + ] else - autoCompileListener() if autoCompileListener - autoCompileListener = null + for unbind in autoCompileListeners + unbind() + autoCompileListeners = [] + $scope.autoCompileLintingError = false $scope.autocompile_enabled = localStorage("autocompile_enabled:#{$scope.project_id}") or false $scope.$watch "autocompile_enabled", (newValue, oldValue) -> @@ -428,6 +496,7 @@ define [ event_tracking.sendMBSampled "editor-recompile-sampled", options + $scope.lastStartedCompileAt = Date.now() $scope.pdf.compiling = true $scope.pdf.isAutoCompileOnLoad = options?.isAutoCompileOnLoad # initial autocompile @@ -460,7 +529,7 @@ define [ $scope.pdf.error = true $scope.pdf.view = 'errors' .finally () -> - $scope.recompiledAt = Date.now() + $scope.lastFinishedCompileAt = Date.now() # This needs to be public. ide.$scope.recompile = $scope.recompile diff --git a/services/web/public/stylesheets/app/editor/pdf.less b/services/web/public/stylesheets/app/editor/pdf.less index d59ca93069..0f76816fd5 100644 --- a/services/web/public/stylesheets/app/editor/pdf.less +++ b/services/web/public/stylesheets/app/editor/pdf.less @@ -1,13 +1,34 @@ +@stripe-width: 20px; +@keyframes pdf-toolbar-stripes { + from { background-position: 0 0; } + to { background-position: @stripe-width 0; } +} + .pdf .toolbar.toolbar-pdf when (@is-overleaf = true) { .toolbar-small-mixin; .toolbar-alt-mixin; border-bottom: 0; padding-right: 5px; + &.uncompiled-changes { + #gradient > .striped(@color: rgba(255,255,255,.10), @angle: -45deg); + background-size: @stripe-width @stripe-width; + .animation(pdf-toolbar-stripes 2s linear infinite); + } + .auto-compile-status { + color: white; + margin-right: (@line-height-computed / 2); + i { + color: @brand-danger; + } + } } .pdf .toolbar.toolbar-pdf when (@is-overleaf = false) { .toolbar-tall-mixin; padding: 0 (@line-height-computed / 2); + .auto-compile-status { + display: none; + } } .pdf { @@ -31,29 +52,30 @@ .btn-recompile-group when (@is-overleaf = true) { align-self: stretch; - margin-right: 5px; + margin-right: 6px; + .btn-recompile { + height: 100%; + .btn-primary; + padding-top: 3px; + padding-bottom: 3px; + &:first-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + &[disabled] { + background-color: mix(@btn-primary-bg, @toolbar-alt-bg-color, 65%); + .opacity(1.0); + } + } } .btn-recompile-group when (@is-overleaf = false) { margin-right: (@line-height-computed / 2); -} - - -.btn-recompile when (@is-overleaf = true) { - height: 100%; - .btn-primary; - padding-top: 3px; - padding-bottom: 3px; - &:first-child { - border-top-left-radius: 0; - border-bottom-left-radius: 0; + .btn-recompile { + .btn-info; } } -.btn-recompile when (@is-overleaf = false) { - .btn-info; -} - .btn-split-screen when (@is-overleaf = false) { .fa { display: none; @@ -146,7 +168,6 @@ width: 16px; height: 16px; border: 1px solid @gray-light; - margin-top: 5px; } i.full-screen { diff --git a/services/web/public/stylesheets/app/editor/toolbar.less b/services/web/public/stylesheets/app/editor/toolbar.less index 8370e40959..3175aa3b29 100644 --- a/services/web/public/stylesheets/app/editor/toolbar.less +++ b/services/web/public/stylesheets/app/editor/toolbar.less @@ -25,7 +25,9 @@ .toolbar-right > a:not(.btn) { display: inline-block; color: @toolbar-icon-btn-color; - padding: 0 5px; + padding: 4px 2px; + line-height: 1; + height: 24px; border-radius: @border-radius-small; &.toolbar-header-back-projects { padding: 5px 10px 4px; @@ -49,6 +51,10 @@ } } + &.toolbar-pdf > a:not(.btn) { + margin-right: 3px; + } + .btn-full-height { border: none; border-radius: 0; diff --git a/services/web/public/stylesheets/components/dropdowns.less b/services/web/public/stylesheets/components/dropdowns.less index 516db7178e..8744c1f35c 100755 --- a/services/web/public/stylesheets/components/dropdowns.less +++ b/services/web/public/stylesheets/components/dropdowns.less @@ -13,6 +13,7 @@ border-top: @caret-width-base solid; border-right: @caret-width-base solid transparent; border-left: @caret-width-base solid transparent; + margin-top: -@caret-width-base/2; } // The dropdown wrapper (div)