diff --git a/area.js b/area.js index 14b44cf..d447ae7 100644 --- a/area.js +++ b/area.js @@ -813,7 +813,9 @@ var DrawingArea = new Lang.Class({ this.textHasCursor = true; this._redisplay(); - // Do not hide and do not set opacity to 0 because ibusCandidatePopup need a mapped text entry to init correctly its position. + // Do not hide and do not set opacity to 0 because: + // 1. ibusCandidatePopup need a mapped text entry to init correctly its position. + // 2. 'cursor-changed' signal is no emitted if the text entry is not visible. this.textEntry = new St.Entry({ opacity: 1, x: stageX + x, y: stageY + y }); this.insert_child_below(this.textEntry, null); this.textEntry.grab_key_focus(); @@ -832,17 +834,26 @@ var DrawingArea = new Lang.Class({ this.textEntry.connect('destroy', () => ibusCandidatePopup.disconnect(this.ibusHandler)); } + this.textEntry.clutterText.set_single_line_mode(false); this.textEntry.clutterText.connect('activate', (clutterText) => { this._stopWriting(); }); - this.textEntry.clutterText.connect('text-changed', (clutterText) => { - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - this.currentElement.text = clutterText.text; - this.currentElement.cursorPosition = clutterText.cursorPosition; - this._updateTextCursorTimeout(); - this._redisplay(); - }); + let showCursorOnPositionChanged = true; + this.textEntry.clutterText.connect('text-changed', clutterText => { + this.textEntry.y = stageY + y + (this.textEntry.clutterText.get_layout().get_line_count() - 1) * this.currentElement.height; + this.currentElement.text = clutterText.text; + showCursorOnPositionChanged = false; + this._redisplay(); + }); + + this.textEntry.clutterText.connect('cursor-changed', clutterText => { + this.currentElement.cursorPosition = clutterText.cursorPosition; + this._updateTextCursorTimeout(); + let cursorPosition = clutterText.cursorPosition == -1 ? clutterText.text.length : clutterText.cursorPosition; + this.textHasCursor = showCursorOnPositionChanged || GLib.unichar_isspace(clutterText.text.charAt(cursorPosition - 1)); + showCursorOnPositionChanged = true; + this._redisplay(); }); this.textEntry.clutterText.connect('key-press-event', (clutterText, event) => { @@ -853,53 +864,25 @@ var DrawingArea = new Lang.Class({ } else if (event.has_shift_modifier() && (event.get_key_symbol() == Clutter.KEY_Return || event.get_key_symbol() == Clutter.KEY_KP_Enter)) { - let startNewLine = true; - this._stopWriting(startNewLine); - clutterText.text = ""; + clutterText.insert_unichar('\n'); return Clutter.EVENT_STOP; } - // 'cursor-changed' signal is not emitted if the text entry is not visible. - // So key events related to the cursor must be listened. - if (event.get_key_symbol() == Clutter.KEY_Left || event.get_key_symbol() == Clutter.KEY_Right || - event.get_key_symbol() == Clutter.KEY_Home || event.get_key_symbol() == Clutter.KEY_End) { - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - this.currentElement.cursorPosition = clutterText.cursorPosition; - this._updateTextCursorTimeout(); - this.textHasCursor = true; - this._redisplay(); - }); - } - return Clutter.EVENT_PROPAGATE; }); }, - _stopWriting: function(startNewLine) { + _stopWriting: function() { if (this.currentElement.text.length > 0) this.elements.push(this.currentElement); - if (startNewLine && this.currentElement.points.length == 2) { - this.currentElement.lineIndex = this.currentElement.lineIndex || 0; - // copy object, the original keep existing in this.elements - this.currentElement = Object.create(this.currentElement); - this.currentElement.lineIndex ++; - // define a new 'points' array, the original keep existing in this.elements - this.currentElement.points = [ - [this.currentElement.points[0][0], this.currentElement.points[0][1] + this.currentElement.height], - [this.currentElement.points[1][0], this.currentElement.points[1][1] + this.currentElement.height] - ]; - this.currentElement.text = ""; - this.textEntry.set_y(this.currentElement.y); - } else { - this.currentElement = null; - this._stopTextCursorTimeout(); - this.textEntry.destroy(); - delete this.textEntry; - this.grab_key_focus(); - this.updateActionMode(); - this.updatePointerCursor(); - } + this.currentElement = null; + this._stopTextCursorTimeout(); + this.textEntry.destroy(); + delete this.textEntry; + this.grab_key_focus(); + this.updateActionMode(); + this.updatePointerCursor(); this._redisplay(); }, diff --git a/elements.js b/elements.js index 5890187..1267826 100644 --- a/elements.js +++ b/elements.js @@ -645,7 +645,6 @@ const _DrawingElement = new Lang.Class({ }, // The figure rotation center before transformations (original). - // this.textWidth is computed during Cairo building. _getOriginalCenter: function() { if (!this._originalCenter) { let points = this.points; @@ -710,7 +709,6 @@ const TextElement = new Lang.Class({ eraser: this.eraser, transformations: this.transformations, text: this.text, - lineIndex: this.lineIndex !== undefined ? this.lineIndex : undefined, textRightAligned: this.textRightAligned, font: this.font.to_string(), points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) @@ -730,42 +728,50 @@ const TextElement = new Lang.Class({ return Math.abs(this.points[1][1] - this.points[0][1]); }, - // When rotating grouped lines, lineOffset is used to retrieve the rotation center of the first line. - get lineOffset() { - return (this.lineIndex || 0) * this.height; + // this.lineWidths is computed during Cairo building. + _getLineX: function(index) { + return this.points[1][0] - (this.textRightAligned && this.lineWidths && this.lineWidths[index] ? this.lineWidths[index] : 0); }, _drawCairo: function(cr, params) { - if (this.points.length == 2) { - let layout = PangoCairo.create_layout(cr); - let fontSize = this.height * Pango.SCALE; - this.font.set_absolute_size(fontSize); - layout.set_font_description(this.font); - layout.set_text(this.text, -1); - this.textWidth = layout.get_pixel_size()[0]; - cr.moveTo(this.x, this.y); - layout.set_text(this.text, -1); - PangoCairo.show_layout_line(cr, layout.get_line(0)); + if (this.points.length != 2) + return; + + let layout = PangoCairo.create_layout(cr); + let fontSize = this.height * Pango.SCALE; + this.font.set_absolute_size(fontSize); + layout.set_font_description(this.font); + layout.set_text(this.text, -1); + this.textWidth = layout.get_pixel_size()[0]; + this.lineWidths = layout.get_lines_readonly().map(layoutLine => layoutLine.get_pixel_extents()[1].width) + + layout.get_lines_readonly().forEach((layoutLine, index) => { + cr.moveTo(this._getLineX(index), this.y + this.height * index); + PangoCairo.show_layout_line(cr, layoutLine); + }); + + // Cannot use 'layout.index_to_line_x(cursorPosition, 0)' because character position != byte index + if (params.showTextCursor) { + let layoutCopy = layout.copy(); + if (this.cursorPosition != -1) + layoutCopy.set_text(this.text.slice(0, this.cursorPosition), -1); - if (params.showTextCursor) { - let cursorPosition = this.cursorPosition == -1 ? this.text.length : this.cursorPosition; - layout.set_text(this.text.slice(0, cursorPosition), -1); - let width = layout.get_pixel_size()[0]; - cr.rectangle(this.x + width, this.y, - this.height / 25, - this.height); - cr.fill(); - } - - if (params.showTextRectangle) { - cr.rectangle(this.x, this.y - this.lineOffset, - this.textWidth, - this.height); - setDummyStroke(cr); - } else if (params.drawTextRectangle) { - cr.rectangle(this.x, this.y, - this.textWidth, - this.height); - // Only draw the rectangle to find the element, not to show it. - cr.setLineWidth(0); - } + let cursorLineIndex = layoutCopy.get_line_count() - 1; + let cursorX = this._getLineX(cursorLineIndex) + layoutCopy.get_line_readonly(cursorLineIndex).get_pixel_extents()[1].width; + let cursorY = this.y + this.height * cursorLineIndex; + cr.rectangle(cursorX, cursorY, this.height / 25, - this.height); + cr.fill(); + } + + if (params.showTextRectangle) { + cr.rectangle(this.x, this.y - this.height, + this.textWidth, this.height * layout.get_line_count()); + setDummyStroke(cr); + } else if (params.drawTextRectangle) { + cr.rectangle(this.x, this.y - this.height, + this.textWidth, this.height * layout.get_line_count()); + // Only draw the rectangle to find the element, not to show it. + cr.setLineWidth(0); } }, @@ -774,32 +780,48 @@ const TextElement = new Lang.Class({ }, _drawSvg: function(transAttribute, bgcolorString) { - let row = "\n "; - let [x, y, height] = [Math.round(this.x*100)/100, Math.round(this.y*100)/100, Math.round(this.height*100)/100]; + if (this.points.length != 2) + return ""; + + let row = ""; + let height = Math.round(this.height * 100) / 100; let color = this.eraser ? bgcolorString : this.color.toJSON(); let attributes = this.eraser ? `class="eraser" ` : ''; + attributes += `fill="${color}" ` + + `font-size="${height}" ` + + `font-family="${this.font.get_family()}"`; - if (this.points.length == 2) { - attributes += `fill="${color}" ` + - `font-size="${height}" ` + - `font-family="${this.font.get_family()}"`; - - // this.font.to_string() is not valid to fill the svg 'font' shorthand property. - // Each property must be filled separately. - ['Stretch', 'Style', 'Variant'].forEach(attribute => { - let lower = attribute.toLowerCase(); - if (this.font[`get_${lower}`]() != Pango[attribute].NORMAL) { - let font = new Pango.FontDescription(); - font[`set_${lower}`](this.font[`get_${lower}`]()); - attributes += ` font-${lower}="${font.to_string()}"`; - } - }); - if (this.font.get_weight() != Pango.Weight.NORMAL) - attributes += ` font-weight="${this.font.get_weight()}"`; - row += `${this.text}`; + // this.font.to_string() is not valid to fill the svg 'font' shorthand property. + // Each property must be filled separately. + ['Stretch', 'Style', 'Variant'].forEach(attribute => { + let lower = attribute.toLowerCase(); + if (this.font[`get_${lower}`]() != Pango[attribute].NORMAL) { + let font = new Pango.FontDescription(); + font[`set_${lower}`](this.font[`get_${lower}`]()); + attributes += ` font-${lower}="${font.to_string()}"`; + } + }); + if (this.font.get_weight() != Pango.Weight.NORMAL) + attributes += ` font-weight="${this.font.get_weight()}"`; + + // It is a fallback for thumbnails. The following layout is not the same than the Cairo one and + // layoutLine.get_pixel_extents does not return the correct value with non-latin fonts. + // An alternative would be to store this.lineWidths in the json. + if (this.textRightAligned && !this.lineWidths) { + let clutterText = new Clutter.Text({ text: this.text }); + let layout = clutterText.get_layout(); + let fontSize = height * Pango.SCALE; + this.font.set_absolute_size(fontSize); + layout.set_font_description(this.font); + this.lineWidths = layout.get_lines_readonly().map(layoutLine => layoutLine.get_pixel_extents()[1].width); } + this.text.split(/\r\n|\r|\n/).forEach((text, index) => { + let x = Math.round(this._getLineX(index) * 100) / 100; + let y = Math.round((this.y + this.height * index) * 100) / 100; + row += `\n ${text}`; + }); + return row; }, @@ -827,7 +849,7 @@ const TextElement = new Lang.Class({ _getOriginalCenter: function() { if (!this._originalCenter) { let points = this.points; - this._originalCenter = this.textWidth ? [points[1][0], Math.max(points[0][1], points[1][1]) - this.lineOffset] : + this._originalCenter = points.length == 2 ? [points[1][0], Math.max(points[0][1], points[1][1])] : points.length >= 3 ? getCentroid(points) : getNaiveCenter(points); }