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:
parent
f18e8e6ac0
commit
5698f3f7cb
73
area.js
73
area.js
|
|
@ -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();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
134
elements.js
134
elements.js
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue