I'm creating pyqt6 app where I can replace normal fonts/text with svg fonts(.svg files like a.svg, b.svg etc).
My issue is with word-wrap. In normal mode(svg mode is disabled) QTextEdit correctly moves a word to nex line if it does not fit completely in the line. But in svg mode it does not behave like that. It breaks word and pushes its some characters to the next line instead of moving the whole word to the next line.
I tried replacing space(' ') with '\u200B' or ' ' + '\u200B'. But nothing works. Enter works as expected.
Below is the relevant code:
NON_SVG_CHARS = {' ', '\t', '\u2029', '\r', '\n'}
class SvgTextObject(QObject, QTextObjectInterface):
def __init__(self, char_path, parent=None):
QObject.__init__(self, parent)
QTextObjectInterface.__init__(self)
self.char_path = char_path
self.svg_renderer_cache = {}
self.char_properties = {}
self.normalized_svg_sizes = {}
def intrinsicSize(self, doc, pos, fmt):
char_name = fmt.stringProperty(SVGSettings.SVG_TEXT_FORMAT)
format_id = fmt.property(SVGSettings.SVG_INSTANCE_ID)
properties = self.getProperties(format_id)
normalized_size = self.getNormalizedSize(char_name)
if not normalized_size.isValid():
return QSizeF()
font_size = properties.get(SVGSettings.PropertyStrings.FONT_SIZE, SVGSettings.FontSize.DEFAULT)
letter_width_percent = properties.get(SVGSettings.PropertyStrings.LETTER_WIDTH, 100.0)
letter_height_percent = properties.get(SVGSettings.PropertyStrings.LETTER_HEIGHT, 100.0)
letter_spacing = properties.get(SVGSettings.PropertyStrings.LETTER_SPACING, 0.0)
width_scale_factor = (100.0 + letter_width_percent) / 100.0
height_scale_factor = (100.0 + letter_height_percent) / 100.0
base_scale = font_size / normalized_size.height()
width = normalized_size.width() * base_scale * width_scale_factor
height = normalized_size.height() * base_scale * height_scale_factor
#Add spacing to width for layout purposes
if pos > 0:
prev_char = doc.characterAt(pos - 1)
if not prev_char.isspace() and prev_char != QChar('\n'):
width += letter_spacing
return QSizeF(width, height)
def drawObject(self, painter: QPainter, rect, doc, pos, fmt):
painter.save()
char_name = fmt.stringProperty(SVGSettings.SVG_TEXT_FORMAT)
format_id = fmt.property(SVGSettings.SVG_INSTANCE_ID)
properties = self.getProperties(format_id)
letter_slant_value = properties.get(SVGSettings.PropertyStrings.LETTER_SLANTING, 0.0)
letter_base_offset = properties.get(SVGSettings.PropertyStrings.LETTER_BASE_OFFSET, 0.0)
letter_spacing = properties.get(SVGSettings.PropertyStrings.LETTER_SPACING, 0.0)
normalized_size = self.getNormalizedSize(char_name)
if not normalized_size.isValid():
painter.restore()
return
font_size = properties.get(SVGSettings.PropertyStrings.FONT_SIZE, SVGSettings.FontSize.DEFAULT)
letter_width_percent = properties.get(SVGSettings.PropertyStrings.LETTER_WIDTH, 100.0)
letter_height_percent = properties.get(SVGSettings.PropertyStrings.LETTER_HEIGHT, 100.0)
width_scale_factor = (100.0 + letter_width_percent) / 100.0
height_scale_factor = (100.0 + letter_height_percent) / 100.0
# Calculate scales and offsets based on the font size and character properties
base_scale = font_size / normalized_size.height()
final_scale_x = base_scale * width_scale_factor
final_scale_y = base_scale * height_scale_factor
painter.translate(rect.x(), rect.y())
current_x_offset = 0.0
if pos > 0:
prev_char = doc.characterAt(pos - 1)
if not prev_char.isspace() and prev_char != QChar('\n'):
current_x_offset += letter_spacing
current_y_offset = letter_base_offset
intrinsic_height = self.intrinsicSize(doc, pos, fmt).height()
scaled_svg_height = normalized_size.height() * final_scale_y
extra_height_space = intrinsic_height - scaled_svg_height
current_y_offset += extra_height_space / 2
painter.translate(current_x_offset, current_y_offset)
if letter_slant_value != 0.0:
transform = painter.transform()
transform.shear(letter_slant_value / 100.0, 0)
painter.setTransform(transform)
painter.scale(final_scale_x, final_scale_y)
renderer = self.svg_renderer_cache.get(char_name)
if renderer and renderer.isValid():
renderer.render(painter, QRectF(0, 0, normalized_size.width(), normalized_size.height()))
painter.restore()
class CustomTextEdit(QGraphicsRectItem):
def __init__(self, properties_provider=None, svg_font_directory=None, x=20, y=50, w=200, h=100):
super().__init__(x, y, w, h)
self.svg_font_directory = svg_font_directory
self.svg_enabled = True
self.text = EditableText(self, properties_provider)
self.text.setReadOnly(True)
self.text.pasteRequested.connect(self._handle_paste_request)
self.text.document().contentsChange.connect(self.handle_contents_change)
# self.text.textChanged.connect(self.handle_contents_change)
# Register the custom SVG text object with the document's layout
self.svg_object = SvgTextObject(self.svg_font_directory) # SVG directory
self.text.document().documentLayout().registerHandler(SVGSettings.SVG_TEXT_FORMAT, self.svg_object)
def handle_contents_change(self, position, chars_removed, chars_added):
# check if replacing chars with svg chars is enabled
if not self.svg_enabled:
return
if self.text._is_pasting:
return
# We only care about single-character additions
if chars_removed == 0 and chars_added == 1:
doc = self.text.document()
new_char = doc.characterAt(position)
if new_char in SVGSettings.NON_SVG_CHARS:
return
# already converted to unicode
if new_char == '\uFFFC':
return
cursor = self.text.textCursor()
self.text.document().blockSignals(True) # Block signals for safety
cursor.beginEditBlock()
cursor.setPosition(position)
cursor.setPosition(position + 1, QTextCursor.MoveMode.KeepAnchor)
svg_char_format = QTextCharFormat()
svg_char_format.setObjectType(SVGSettings.SVG_TEXT_FORMAT)
svg_char_format.setProperty(SVGSettings.SVG_TEXT_FORMAT, new_char)
instance_id = str(uuid.uuid4())
svg_char_format.setProperty(SVGSettings.SVG_INSTANCE_ID, instance_id)
# Retrieve properties and set them before insertion
properties = {}
current_properties = self.text.properties_provider.get_current_properties()
for prop_name, slider_values in current_properties.items():
settings_class = SVGSettings.SLIDER_MAPPING.get(prop_name)
if settings_class:
step_size = settings_class.STEP
properties[prop_name] = get_random_value_with_step(slider_values, step_size)
else:
properties[prop_name] = slider_values
self.svg_object.setRandomProperties(instance_id, properties)
cursor.insertText("\uFFFC", svg_char_format)
cursor.endEditBlock()
self.text.document().blockSignals(False) # Unblock signals
displayproperty of CSS, probably because the common text document layout simply doesn't allow such possibility. The only way to achieve this with QTextDocument would be to create your own QAbstractTextDocumentLayout subclass (which is quite complex). Consider using the QtWebEngine module.contentsChangeis emitted before the layout is notified, then it should be fine). The real problem is detecting the deletion, for which you may need to filter keyboard events (backspace/delete or Ctrl-X) or the context menu actions.handle_contents_change(), then use a simple bool flag: check it at the beginning of the function, if it's set, then return immediately, otherwise set it right before making any change, and unset it at the end of the process (or whenever you return). If you still want to block signals, it's preferable to use QSignalBlocker, which is safer to use.