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._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();
},

View File

@ -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 += `<text ${attributes} x="${x}" `;
row += `y="${y}"${transAttribute}>${this.text}</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 ${attributes} x="${x}" y="${y}"${transAttribute}>${text}</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);
}