diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee
index b3bbba555c..3bb489ea12 100644
--- a/services/web/app/coffee/Features/Editor/EditorController.coffee
+++ b/services/web/app/coffee/Features/Editor/EditorController.coffee
@@ -112,12 +112,18 @@ module.exports = EditorController =
return callback(error) if error?
client.get "last_name", (error, last_name) ->
return callback(error) if error?
- cursorData.id = client.id
- if first_name? and last_name?
- cursorData.name = first_name + " " + last_name
- else
- cursorData.name = "Anonymous"
- EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientUpdated", cursorData)
+ client.get "email", (error, email) ->
+ return callback(error) if error?
+ client.get "user_id", (error, user_id) ->
+ return callback(error) if error?
+ cursorData.id = client.id
+ cursorData.user_id = user_id if user_id?
+ cursorData.email = email if email?
+ if first_name? and last_name?
+ cursorData.name = first_name + " " + last_name
+ else
+ cursorData.name = "Anonymous"
+ EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientUpdated", cursorData)
addUserToProject: (project_id, email, privileges, callback = (error, collaborator_added)->)->
email = email.toLowerCase()
diff --git a/services/web/public/coffee/app/ide/editor/annotations/AnnotationsManager.coffee b/services/web/public/coffee/app/ide/editor/annotations/AnnotationsManager.coffee
index 4bba724d5e..f8839ac025 100644
--- a/services/web/public/coffee/app/ide/editor/annotations/AnnotationsManager.coffee
+++ b/services/web/public/coffee/app/ide/editor/annotations/AnnotationsManager.coffee
@@ -4,34 +4,139 @@ define [
Range = require("ace/range").Range
class AnnotationsManager
- constructor: (@$scope, @editor) ->
+ constructor: (@$scope, @editor, @element) ->
@markerIds = []
+ @labels = []
+
+ @$scope.annotationLabel = {
+ show: false
+ right: "auto"
+ left: "auto"
+ top: "auto"
+ bottom: "auto"
+ backgroundColor: "black"
+ text: ""
+ }
@$scope.$watch "annotations", (value) =>
- if value?
- @redrawAnnotations()
+ @redrawAnnotations()
+
+ @$scope.$watch "theme", (value) =>
+ @redrawAnnotations()
+
+ @editor.on "mousemove", (e) =>
+ position = @editor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
+ e.position = position
+ @showAnnotationLabels(position)
redrawAnnotations: () ->
console.log "REDRAWING ANNOTATIONS"
+ @_clearMarkers()
+ @_clearLabels()
+
+ for annotation in @$scope.annotations or []
+ do (annotation) =>
+ colorScheme = @_getColorScheme(annotation.hue)
+ console.log "DRAWING ANNOTATION", annotation, colorScheme
+ if annotation.cursor?
+ @labels.push {
+ text: annotation.text
+ range: new Range(
+ annotation.cursor.row, annotation.cursor.column,
+ annotation.cursor.row, annotation.cursor.column + 1
+ )
+ colorScheme: colorScheme
+ snapToStartOfRange: true
+ }
+ @_drawCursor(annotation, colorScheme)
+
+ showAnnotationLabels: (position) ->
+ labelToShow = null
+ for label in @labels or []
+ if label.range.contains(position.row, position.column)
+ labelToShow = label
+
+ @$scope.$apply () =>
+ if !labelToShow?
+ @$scope.annotationLabel.show = false
+ else
+ $ace = $(@editor.renderer.container).find(".ace_scroller")
+ # Move the label into the Ace content area so that offsets and positions are easy to calculate.
+ $ace.append(@element.find(".annotation-label"))
+
+ if labelToShow.snapToStartOfRange
+ coords = @editor.renderer.textToScreenCoordinates(labelToShow.range.start.row, labelToShow.range.start.column)
+ else
+ coords = @editor.renderer.textToScreenCoordinates(position.row, position.column)
+
+ offset = $ace.offset()
+ height = $ace.height()
+ coords.pageX = coords.pageX - offset.left
+ coords.pageY = coords.pageY - offset.top
+
+ if coords.pageY > 100
+ console.log "middle of page", height - coords.pageY
+ top = "auto"
+ bottom = height - coords.pageY
+ else
+ console.log "top of page", coords.pageY
+ top = coords.pageY + @editor.renderer.lineHeight
+ bottom = "auto"
+
+ left = coords.pageX
+
+ console.log "TOP BOTTOM", top, bottom
+
+
+ @$scope.annotationLabel = {
+ show: true
+ left: left
+ bottom: bottom
+ top: top
+ backgroundColor: labelToShow.colorScheme.labelBackgroundColor
+ text: labelToShow.text
+ }
+
+ _clearMarkers: () ->
for marker_id in @markerIds
@editor.getSession().removeMarker(marker_id)
@markerIds = []
- for annotation in @$scope.annotations or []
- do (annotation) =>
- console.log "DRAWING ANNOTATION", annotation
- @markerIds.push @editor.getSession().addMarker new Range(
- annotation.cursor.row, annotation.cursor.column,
- annotation.cursor.row, annotation.cursor.column + 1
- ), "remote-cursor", (html, range, left, top, config) ->
- div = """
-
-
-
#{$('
').text(annotation.text).html()}
-
- """
- html.push div
- , true
\ No newline at end of file
+ _clearLabels: () ->
+ @labels = []
+
+ _drawCursor: (annotation, colorScheme) ->
+ @markerIds.push @editor.getSession().addMarker new Range(
+ annotation.cursor.row, annotation.cursor.column,
+ annotation.cursor.row, annotation.cursor.column + 1
+ ), "remote-cursor", (html, range, left, top, config) ->
+ div = """
+
+ """
+ html.push div
+ , true
+
+ _getColorScheme: (hue) ->
+ if @_isDarkTheme()
+ return {
+ cursor: "hsl(#{hue}, 100%, 50%)"
+ labelBackgroundColor: "hsl(#{hue}, 100%, 50%)"
+ }
+ else
+ return {
+ cursor: "hsl(#{hue}, 100%, 50%)"
+ labelBackgroundColor: "hsl(#{hue}, 100%, 50%)"
+ }
+
+ _isDarkTheme: () ->
+ rgb = @element.find(".ace_editor").css("background-color");
+ [m, r, g, b] = rgb.match(/rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)/)
+ r = parseInt(r, 10)
+ g = parseInt(g, 10)
+ b = parseInt(b, 10)
+ return r + g + b < 3 * 128
\ No newline at end of file
diff --git a/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee
index de577f321c..0d668dafa9 100644
--- a/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee
+++ b/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee
@@ -39,10 +39,10 @@ define [
editor = Ace.edit(element.find(".ace-editor-body")[0])
- autoCompleteManager = new AutoCompleteManager(scope, editor)
- spellCheckManager = new SpellCheckManager(scope, editor, element)
- undoManager = new UndoManager(scope, editor)
- annotationsManagaer = new AnnotationsManager(scope, editor)
+ autoCompleteManager = new AutoCompleteManager(scope, editor, element)
+ spellCheckManager = new SpellCheckManager(scope, editor, element)
+ undoManager = new UndoManager(scope, editor, element)
+ annotationsManagaer = new AnnotationsManager(scope, editor, element)
# Prevert Ctrl|Cmd-S from triggering save dialog
editor.commands.addCommand
@@ -91,6 +91,7 @@ define [
session.setMode(new LatexMode())
autoCompleteManager.bindToSession(session)
+ annotationsManagaer.redrawAnnotations()
doc = session.getDocument()
doc.on "change", () ->
@@ -138,6 +139,20 @@ define [
+
+ {{ annotationLabel.text }}
+
"""
}
diff --git a/services/web/public/coffee/app/ide/online-users/OnlineUsersManager.coffee b/services/web/public/coffee/app/ide/online-users/OnlineUsersManager.coffee
index 1d63ccb02b..2f382cc805 100644
--- a/services/web/public/coffee/app/ide/online-users/OnlineUsersManager.coffee
+++ b/services/web/public/coffee/app/ide/online-users/OnlineUsersManager.coffee
@@ -1,4 +1,6 @@
-define [], () ->
+define [
+ "../../../libs/md5"
+], () ->
class OnlineUsersManager
constructor: (@ide, @$scope) ->
@$scope.onlineUsers = {}
@@ -17,6 +19,7 @@ define [], () ->
@updateCursorHighlights()
@ide.socket.on "clientTracking.clientDisconnected", (client_id) =>
+ console.log "CLIENT DISCONNECTED", client_id
@$scope.$apply () =>
delete @$scope.onlineUsers[client_id]
@updateCursorHighlights()
@@ -26,12 +29,14 @@ define [], () ->
@$scope.onlineUserCursorAnnotations = {}
for client_id, client of @$scope.onlineUsers
doc_id = client.doc_id
+ continue if !doc_id?
@$scope.onlineUserCursorAnnotations[doc_id] ||= []
@$scope.onlineUserCursorAnnotations[doc_id].push {
text: client.name
cursor:
row: client.row
column: client.column
+ hue: @getHueForUserId(client.user_id)
}
UPDATE_INTERVAL: 500
@@ -52,5 +57,20 @@ define [], () ->
, @UPDATE_INTERVAL
else
console.log "NOT UPDATING"
-
+
+ OWN_HUE: 200 # We will always appear as this color to ourselves
+ ANONYMOUS_HUE: 100
+ getHueForUserId: (user_id) ->
+ if !user_id?
+ return @ANONYMOUS_HUE
+
+ if window.user.id == user_id
+ return @OWN_HUE
+
+ hash = CryptoJS.MD5(user_id)
+ hue = parseInt(hash.toString().slice(0,8), 16) % 320
+ # Avoid 20 degrees either side of the personal hue
+ if hue > @OWNER_HUE - 20
+ hue = hue + 40
+ return hue
diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less
index 41736b4538..d8ffcb2f2d 100644
--- a/services/web/public/stylesheets/app/editor.less
+++ b/services/web/public/stylesheets/app/editor.less
@@ -193,28 +193,25 @@
background-repeat: repeat-x;
background-position: bottom left;
}
- @cursor-color: rgb(14, 158, 0);
.remote-cursor {
position: absolute;
z-index: 2;
- border-left: 2px solid @cursor-color;
- .name {
- font-size: 0.8em;
- background-color: @cursor-color;
- color: white;
- padding: 2px 6px;
- border-radius: 3px 3px 3px 0;
- position: absolute;
- left: -4px;
- }
+ border-left: 2px solid transparent;
.nubbin {
- height: 6px;
- width: 6px;
- background-color: @cursor-color;
+ height: 5px;
+ width: 5px;
position: absolute;
- left: -4px;
+ left: -2px;
}
}
+ .annotation-label {
+ padding: (@line-height-computed / 4) (@line-height-computed / 2);
+ font-size: 0.8rem;
+ z-index: 100;
+ font-family: @font-family-sans-serif;
+ color: white;
+ font-weight: 700;
+ }
}
.ui-layout-resizer {
diff --git a/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee
index 8f4ad5bb6f..2f2335e19c 100644
--- a/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee
@@ -246,15 +246,16 @@ describe "EditorController", ->
row: @row = 42
column: @column = 37
}
- @clientParams = {
- project_id: @project_id
- first_name: @first_name = "Douglas"
- last_name: @last_name = "Adams"
- }
- @client.get = (param, callback) => callback null, @clientParams[param]
-
describe "with a logged in user", ->
beforeEach ->
+ @clientParams = {
+ project_id: @project_id
+ first_name: @first_name = "Douglas"
+ last_name: @last_name = "Adams"
+ email: @email = "joe@example.com"
+ user_id: @user_id = "user-id-123"
+ }
+ @client.get = (param, callback) => callback null, @clientParams[param]
@EditorController.updateClientPosition @client, @update
it "should send the update to the project room with the user's name", ->
@@ -265,13 +266,17 @@ describe "EditorController", ->
name: "#{@first_name} #{@last_name}"
row: @row
column: @column
+ email: @email
+ user_id: @user_id
})
.should.equal true
describe "with an anonymous user", ->
beforeEach ->
- @clientParams.first_name = null
- @clientParams.last_name = null
+ @clientParams = {
+ project_id: @project_id
+ }
+ @client.get = (param, callback) => callback null, @clientParams[param]
@EditorController.updateClientPosition @client, @update
it "should send the update to the project room with an anonymous name", ->