Просмотр исходного кода

Merge pull request #321 from sharelatex/ja-recompile-tweaks

Animate toolbar for auto-compile and consolidate trigger logic
James Allen 8 лет назад
Родитель
Сommit
a7fabb8e43

+ 5 - 5
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(' ', '<br>')+"'",
 			tooltip-placement="bottom"
 		)
-			i.fa.fa-file
+			i.fa.fa-fw.fa-file
 		a(
 			href,
 			ng-click="openNewFolderModal()",
 			tooltip-html="'"+translate('new_folder').replace(' ', '<br>')+"'",
 			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(

+ 16 - 4
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')+" <span class=\"keyboard-shortcut\">({{modifierKey}} + Enter)</span>'"
@@ -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()"

+ 5 - 0
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

+ 2 - 0
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)
 

+ 3 - 0
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.

+ 98 - 29
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
-
-			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
-
-			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
+			$scope.recompile(isAutoCompileOnChange: true)
+
+		startTryingAutoCompile = () ->
+			return if autoCompileInterval?
+			$scope.startedTryingAutoCompileAt = Date.now()
+			autoCompileInterval = setInterval autoCompileIfReady, 200
+
+		stopTryingAutoCompile = () ->
+			clearInterval autoCompileInterval
+			autoCompileInterval = null
+
+		$scope.$watch "uncompiledChanges", (uncompiledChanges) ->
+			if uncompiledChanges
+				startTryingAutoCompile()
+			else
+				stopTryingAutoCompile()
+
+		$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
-				# Extend remainder of timeout
-				autoCompileTimeout = setTimeout () ->
-					autoCompileTimeout = null
-					triggerAutoCompile()
-				, AUTO_COMPILE_TIMEOUT - timeSinceLastCompile
+				$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()
 
-		autoCompileListener = null
+		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

+ 38 - 17
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 {

+ 7 - 1
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;

+ 1 - 0
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)