multi-line text elements

* Can browse and break lines anywhere
* Can past multi-line texts
* Do not need lineIndex anymore and grouped lines are preserved permanently

Related to #56
This commit is contained in:
abakkk 2021-02-17 11:33:32 +01:00
parent f18e8e6ac0
commit 5698f3f7cb
2 changed files with 106 additions and 101 deletions

73
area.js
View File

@ -813,7 +813,9 @@ var DrawingArea = new Lang.Class({
this.textHasCursor = true; this.textHasCursor = true;
this._redisplay(); 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.textEntry = new St.Entry({ opacity: 1, x: stageX + x, y: stageY + y });
this.insert_child_below(this.textEntry, null); this.insert_child_below(this.textEntry, null);
this.textEntry.grab_key_focus(); this.textEntry.grab_key_focus();
@ -832,17 +834,26 @@ var DrawingArea = new Lang.Class({
this.textEntry.connect('destroy', () => ibusCandidatePopup.disconnect(this.ibusHandler)); this.textEntry.connect('destroy', () => ibusCandidatePopup.disconnect(this.ibusHandler));
} }
this.textEntry.clutterText.set_single_line_mode(false);
this.textEntry.clutterText.connect('activate', (clutterText) => { this.textEntry.clutterText.connect('activate', (clutterText) => {
this._stopWriting(); this._stopWriting();
}); });
this.textEntry.clutterText.connect('text-changed', (clutterText) => { let showCursorOnPositionChanged = true;
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { this.textEntry.clutterText.connect('text-changed', clutterText => {
this.currentElement.text = clutterText.text; this.textEntry.y = stageY + y + (this.textEntry.clutterText.get_layout().get_line_count() - 1) * this.currentElement.height;
this.currentElement.cursorPosition = clutterText.cursorPosition; this.currentElement.text = clutterText.text;
this._updateTextCursorTimeout(); showCursorOnPositionChanged = false;
this._redisplay(); 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) => { this.textEntry.clutterText.connect('key-press-event', (clutterText, event) => {
@ -853,53 +864,25 @@ var DrawingArea = new Lang.Class({
} else if (event.has_shift_modifier() && } else if (event.has_shift_modifier() &&
(event.get_key_symbol() == Clutter.KEY_Return || (event.get_key_symbol() == Clutter.KEY_Return ||
event.get_key_symbol() == Clutter.KEY_KP_Enter)) { event.get_key_symbol() == Clutter.KEY_KP_Enter)) {
let startNewLine = true; clutterText.insert_unichar('\n');
this._stopWriting(startNewLine);
clutterText.text = "";
return Clutter.EVENT_STOP; 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; return Clutter.EVENT_PROPAGATE;
}); });
}, },
_stopWriting: function(startNewLine) { _stopWriting: function() {
if (this.currentElement.text.length > 0) if (this.currentElement.text.length > 0)
this.elements.push(this.currentElement); this.elements.push(this.currentElement);
if (startNewLine && this.currentElement.points.length == 2) { this.currentElement = null;
this.currentElement.lineIndex = this.currentElement.lineIndex || 0; this._stopTextCursorTimeout();
// copy object, the original keep existing in this.elements this.textEntry.destroy();
this.currentElement = Object.create(this.currentElement); delete this.textEntry;
this.currentElement.lineIndex ++; this.grab_key_focus();
// define a new 'points' array, the original keep existing in this.elements this.updateActionMode();
this.currentElement.points = [ this.updatePointerCursor();
[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._redisplay(); this._redisplay();
}, },

View File

@ -645,7 +645,6 @@ const _DrawingElement = new Lang.Class({
}, },
// The figure rotation center before transformations (original). // The figure rotation center before transformations (original).
// this.textWidth is computed during Cairo building.
_getOriginalCenter: function() { _getOriginalCenter: function() {
if (!this._originalCenter) { if (!this._originalCenter) {
let points = this.points; let points = this.points;
@ -710,7 +709,6 @@ const TextElement = new Lang.Class({
eraser: this.eraser, eraser: this.eraser,
transformations: this.transformations, transformations: this.transformations,
text: this.text, text: this.text,
lineIndex: this.lineIndex !== undefined ? this.lineIndex : undefined,
textRightAligned: this.textRightAligned, textRightAligned: this.textRightAligned,
font: this.font.to_string(), font: this.font.to_string(),
points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) 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]); 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. // this.lineWidths is computed during Cairo building.
get lineOffset() { _getLineX: function(index) {
return (this.lineIndex || 0) * this.height; return this.points[1][0] - (this.textRightAligned && this.lineWidths && this.lineWidths[index] ? this.lineWidths[index] : 0);
}, },
_drawCairo: function(cr, params) { _drawCairo: function(cr, params) {
if (this.points.length == 2) { if (this.points.length != 2)
let layout = PangoCairo.create_layout(cr); return;
let fontSize = this.height * Pango.SCALE;
this.font.set_absolute_size(fontSize); let layout = PangoCairo.create_layout(cr);
layout.set_font_description(this.font); let fontSize = this.height * Pango.SCALE;
layout.set_text(this.text, -1); this.font.set_absolute_size(fontSize);
this.textWidth = layout.get_pixel_size()[0]; layout.set_font_description(this.font);
cr.moveTo(this.x, this.y); layout.set_text(this.text, -1);
layout.set_text(this.text, -1); this.textWidth = layout.get_pixel_size()[0];
PangoCairo.show_layout_line(cr, layout.get_line(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 cursorLineIndex = layoutCopy.get_line_count() - 1;
let cursorPosition = this.cursorPosition == -1 ? this.text.length : this.cursorPosition; let cursorX = this._getLineX(cursorLineIndex) + layoutCopy.get_line_readonly(cursorLineIndex).get_pixel_extents()[1].width;
layout.set_text(this.text.slice(0, cursorPosition), -1); let cursorY = this.y + this.height * cursorLineIndex;
let width = layout.get_pixel_size()[0]; cr.rectangle(cursorX, cursorY, this.height / 25, - this.height);
cr.rectangle(this.x + width, this.y, cr.fill();
this.height / 25, - this.height); }
cr.fill();
} if (params.showTextRectangle) {
cr.rectangle(this.x, this.y - this.height,
if (params.showTextRectangle) { this.textWidth, this.height * layout.get_line_count());
cr.rectangle(this.x, this.y - this.lineOffset, setDummyStroke(cr);
this.textWidth, - this.height); } else if (params.drawTextRectangle) {
setDummyStroke(cr); cr.rectangle(this.x, this.y - this.height,
} else if (params.drawTextRectangle) { this.textWidth, this.height * layout.get_line_count());
cr.rectangle(this.x, this.y, // Only draw the rectangle to find the element, not to show it.
this.textWidth, - this.height); cr.setLineWidth(0);
// 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) { _drawSvg: function(transAttribute, bgcolorString) {
let row = "\n "; if (this.points.length != 2)
let [x, y, height] = [Math.round(this.x*100)/100, Math.round(this.y*100)/100, Math.round(this.height*100)/100]; return "";
let row = "";
let height = Math.round(this.height * 100) / 100;
let color = this.eraser ? bgcolorString : this.color.toJSON(); let color = this.eraser ? bgcolorString : this.color.toJSON();
let attributes = this.eraser ? `class="eraser" ` : ''; let attributes = this.eraser ? `class="eraser" ` : '';
attributes += `fill="${color}" ` +
`font-size="${height}" ` +
`font-family="${this.font.get_family()}"`;
if (this.points.length == 2) { // this.font.to_string() is not valid to fill the svg 'font' shorthand property.
attributes += `fill="${color}" ` + // Each property must be filled separately.
`font-size="${height}" ` + ['Stretch', 'Style', 'Variant'].forEach(attribute => {
`font-family="${this.font.get_family()}"`; let lower = attribute.toLowerCase();
if (this.font[`get_${lower}`]() != Pango[attribute].NORMAL) {
// this.font.to_string() is not valid to fill the svg 'font' shorthand property. let font = new Pango.FontDescription();
// Each property must be filled separately. font[`set_${lower}`](this.font[`get_${lower}`]());
['Stretch', 'Style', 'Variant'].forEach(attribute => { attributes += ` font-${lower}="${font.to_string()}"`;
let lower = attribute.toLowerCase(); }
if (this.font[`get_${lower}`]() != Pango[attribute].NORMAL) { });
let font = new Pango.FontDescription(); if (this.font.get_weight() != Pango.Weight.NORMAL)
font[`set_${lower}`](this.font[`get_${lower}`]()); attributes += ` font-weight="${this.font.get_weight()}"`;
attributes += ` font-${lower}="${font.to_string()}"`;
} // 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.
if (this.font.get_weight() != Pango.Weight.NORMAL) // An alternative would be to store this.lineWidths in the json.
attributes += ` font-weight="${this.font.get_weight()}"`; if (this.textRightAligned && !this.lineWidths) {
row += `<text ${attributes} x="${x}" `; let clutterText = new Clutter.Text({ text: this.text });
row += `y="${y}"${transAttribute}>${this.text}</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 ${attributes} x="${x}" y="${y}"${transAttribute}>${text}</text>`;
});
return row; return row;
}, },
@ -827,7 +849,7 @@ const TextElement = new Lang.Class({
_getOriginalCenter: function() { _getOriginalCenter: function() {
if (!this._originalCenter) { if (!this._originalCenter) {
let points = this.points; 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) : points.length >= 3 ? getCentroid(points) :
getNaiveCenter(points); getNaiveCenter(points);
} }