Files
esl-reader/lib/words.py
Christopher T. Johnson c46ee65662 Move Fragment out of Word
2024-03-27 16:20:24 -04:00

911 lines
32 KiB
Python

import copy
import json
import re
from typing import Any, Optional
import requests
from PyQt6.QtCore import QPoint, QRect, QUrl, Qt, pyqtSignal
from PyQt6.QtGui import (
QBrush,
QColor,
QFont,
QFontDatabase,
QFontMetrics,
QMouseEvent,
QPainter,
QPaintEvent,
QResizeEvent,
QTextOption,
QTransform,
)
from PyQt6.QtSql import QSqlQuery
from PyQt6.QtWidgets import QScrollArea, QWidget
from lib import query_error
class Fragment:
"""A structure to hold typed values of a fragment."""
_type: str # Function of this fragment. Think text, span, button
_text: str # The simple utf-8 text
_content: list['Fragment']
_audio: QUrl # Optional audio URL
_font: QFont # The font, with all options, to use for this fragment
_align: QTextOption # Alignment information
_rect: QRect # The rect that contains _text
_padding: list[int] # space to add around the text
_border: list[int] # Size of the border (all the same)
_margin: list[int] # Space outside of the border
_color: QColor # the pen color
_wref: str # a word used as a 'href'
_position: QPoint # where to drawText
_borderRect: QRect # where to drawRect
_radius: int # Radius for rounded rects
_asis: bool = False
_left: int = 0
TYPES = [ 'text', 'span', 'button' ]
def __init__(self,
text:str,
font:QFont,
t:str = 'text',
audio:str = '',
color: Optional[QColor] = None,
asis: bool = False,
) -> None:
if t not in self.TYPES:
raise Exception(f"Unknown fragment type{t}")
self._type = t
self._text = text
self._font = font
self._audio = QUrl(audio)
self._align = QTextOption(
Qt.AlignmentFlag.AlignLeft
| Qt.AlignmentFlag.AlignBaseline
)
self._padding = [0, 0, 0, 0]
self._border = [0, 0, 0, 0]
self._margin = [0, 0, 0, 0]
self._wref = ''
self._position = QPoint()
self._borderRect = QRect()
if color:
self._color = color
else:
self._color = QColor()
self._asis = asis
return
def __str__(self) -> str:
return self._text
#
# Setters
#
def setType(self, t:str) -> None:
if t not in self.TYPES:
raise Exception(f"Unknown fragment type{t}")
self._type = t
return
def setText(self, text:str) -> None:
self._text = text
return
def setFont(self, font:QFont) -> None:
self._font = font
return
def setAudio(self, audio:str) -> None:
self._audio = QUrl(audio)
return
def setAlign(self, align:QTextOption) -> None:
self._align = align
return
def setRect(self,rect:QRect) -> None:
self._rect = rect
return
def setPadding(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None:
if top > -1 or right > -1 or bottom > -1 or left > -1:
if top >= 0:
self._padding[0] = top
if right >= 0:
self._padding[1] = right
if bottom >= 0:
self._padding[2] = bottom
if left >= 0:
self._padding[3] = left
return
if len(args) == 4:
self._padding = [args[0], args[1], args[2], args[3]]
elif len(args) == 3:
self._padding = [args[0], args[1], args[2], args[1]]
elif len(args) == 2:
self._padding = [args[0], args[1], args[0], args[1]]
elif len(args) == 1:
self._padding = [args[0], args[0], args[0], args[0]]
else:
raise Exception("argument error")
return
def setBorder(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None:
if top > -1 or right > -1 or bottom > -1 or left > -1:
if top >= 0:
self._border[0] = top
if right >= 0:
self._border[1] = right
if bottom >= 0:
self._border[2] = bottom
if left >= 0:
self._border[3] = left
return
if len(args) == 4:
self._border = [args[0], args[1], args[2], args[3]]
elif len(args) == 3:
self._border = [args[0], args[1], args[2], args[1]]
elif len(args) == 2:
self._border = [args[0], args[1], args[0], args[1]]
elif len(args) == 1:
self._border = [args[0], args[0], args[0], args[0]]
else:
raise Exception("argument error")
return
def setMargin(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None:
if top > -1 or right > -1 or bottom > -1 or left > -1:
if top >= 0:
self._margin[0] = top
if right >= 0:
self._margin[1] = right
if bottom >= 0:
self._margin[2] = bottom
if left >= 0:
self._margin[3] = left
return
if len(args) == 4:
self._margin = [args[0], args[1], args[2], args[3]]
elif len(args) == 3:
self._margin = [args[0], args[1], args[2], args[1]]
elif len(args) == 2:
self._margin = [args[0], args[1], args[0], args[1]]
elif len(args) == 1:
self._margin = [args[0], args[0], args[0], args[0]]
else:
raise Exception("argument error")
return
def setWRef(self, ref:str) -> None:
self._wref = ref
return
def setPosition(self, pnt:QPoint) -> None:
self._position = pnt
return
def setBorderRect(self, rect:QRect) -> None:
self._borderRect = rect
return
def setColor(self,color:QColor) -> None:
self._color = color
return
def setLeft(self, left:int) -> None:
self._left = left
return
#
# Getters
#
def wRef(self) -> str:
return self._wref
def type(self) -> str:
return self._type
def text(self) -> str:
return self._text
def font(self) -> QFont:
return self._font
def audio(self) -> str:
return self._audio.url()
def align(self) -> QTextOption:
return self._align
def rect(self) -> QRect:
return self._rect
def padding(self) -> list[int]:
return self._padding
def border(self) -> list[int]:
return self._border
def margin(self) -> list[int]:
return self._margin
def position(self) -> QPoint:
return self._position
def borderRect(self) -> QRect:
return self._borderRect
def color(self) -> QColor:
return self._color
def asis(self) -> bool:
return self._asis
def left(self) -> int:
return self._left
API = "https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
MWAPI = "https://www.dictionaryapi.com/api/v3/references/collegiate/json/{word}?key=51d9df34-ee13-489e-8656-478c215e846c"
class Word:
"""All processing of a dictionary word."""
_words: dict[str, Any] = {}
class Line:
def __init__(self) -> None:
self._maxHeight = -1
self._baseLine = -1
self._leading = -1
self._fragments:list[Fragment] = []
return
def __repr__(self) -> str:
return '|'.join([x.text() for x in self._fragments])+f'|{self._maxHeight}'
def parseText(self, frag: Fragment) -> list[Fragment]:
org = frag.text()
if frag.asis():
return [frag]
#
# Needed Fonts
#
bold = QFont(frag.font())
bold.setWeight(QFont.Weight.Bold)
italic = QFont(frag.font())
italic.setItalic(True)
smallCaps = QFont(frag.font())
smallCaps.setCapitalization(QFont.Capitalization.SmallCaps)
script = QFont(frag.font())
script.setPixelSize(int(script.pixelSize()/4))
results: list[Fragment] = []
while True:
text = frag.text()
start = text.find('{')
if start < 0:
results.append(frag)
return results
if start > 0:
newFrag = copy.copy(frag)
newFrag.setText(text[:start])
results.append(newFrag)
frag.setText(text[start:])
continue
#
# Start == 0
#
#
# If the token is an end-token, return now.
#
if text.startswith('{/'):
results.append(frag)
return results
#
# extract this token
#
end = text.find('}')
token = text[1:end]
frag.setText(text[end+1:])
newFrag = copy.copy(frag)
oldFont = QFont(frag.font())
if token == 'bc':
results.append(Fragment(': ', bold, color=QColor('#fff')))
continue
if token in ['b', 'inf', 'it', 'sc', 'sup', 'phrase', 'parahw', 'gloss',
'qword', 'wi', 'dx', 'dx_def', 'dx_ety', 'ma']:
if token == 'b':
frag.setFont(bold)
elif token in ['it', 'qword', 'wi']:
frag.setFont(italic)
elif token == 'sc':
frag.setFont(smallCaps)
elif token in ['inf', 'sup']:
frag.setFont(script)
elif token == 'phrase':
font = QFont(bold)
font.setItalic(True)
frag.setFont(font)
elif token == 'parahw':
font = QFont(smallCaps)
font.setWeight(QFont.Weight.Bold)
frag.setFont(font)
elif token == 'gloss':
frag.setText('[' + frag.text())
elif token in ['dx', 'dx_ety']:
frag.setText('\u2014'+frag.text())
elif token == 'ma':
frag.setText('\u2014 more at '+frag.text())
elif token == 'dx_def':
frag.setText('('+frag.text())
else:
raise Exception(f"Unknown block marker: {token}")
results += self.parseText(frag)
frag = results.pop()
frag.setFont(oldFont)
text = frag.text()
if not text.startswith('{/'+token+'}'):
raise Exception(f"No matching close for {token} in {org}")
if token == 'gloss':
results[-1].setText(results[-1].text() + ']')
elif token == 'dx_def':
results[-1].setText(results[-1].text() + ')')
end = text.find('}')
text = text[end+1:]
frag.setText(text)
continue
#
# These are codes that include all information within the token
#
fields = token.split('|')
token = fields[0]
if token in [ 'a_line', 'd_link', 'dxt', 'et_link', 'i_link',
'mat', 'sx']:
wref = ''
htext = fields[1]
oldFont = QFont(frag.font())
if token == 'a_link':
wref = fields[1]
elif token in ['d_link', 'et_link', 'mat', 'sx', 'i_link']:
if fields[2] == '':
wref = fields[1]
else:
wref = fields[2]
if token == 'i_link':
frag.setFont(italic)
elif token == 'i_link':
if fields[2] == '':
wref = fields[1]
else:
wref = fields[2]
else:
raise Exception(f"Unknown code: {token} in {org}")
newFrag = copy.copy(frag)
newFrag.setText(htext)
newFrag.setWRef(wref)
results.append(newFrag)
frag.setFont(oldFont)
text = frag.text()
continue
raise Exception(f"Unable to locate a known token {token} in {org}")
def addFragment(self, frag: Fragment,) -> None:
SPEAKER = "\U0001F508"
if frag.audio():
frag.setText(frag.text() + ' ' + SPEAKER)
text = frag.text()
text = re.sub(r"\*", "\u2022", text)
text = re.sub(r"\{ldquo\}", "\u201c", text)
text = re.sub(r"\{rdquo\}", "\u201d", text)
frag.setText(text)
if frag.type() == 'btn':
frag.setPadding(3)
frag.setBorder(1)
frag.setMargin(2)
items = self.parseText(frag)
self._fragments += items
return
def finalizeLine(self, maxWidth:int) -> None:
"""Create all of the positions for all the fragments."""
#
# Find the maximum hight and max baseline
#
maxHeight = -1
baseLine = -1
leading = -1
for frag in self._fragments:
fm = QFontMetrics(frag.font())
rect = fm.boundingRect(frag.text(), frag.align())
height = rect.height()
bl = height - fm.descent()
#
# Add the padding, border and margin to adjust the baseline and height
#
b = frag.padding()
height += b[0] + b[2]
bl += b[2]
b = frag.border()
height += b[0] + b[2]
bl += b[2]
b = frag.margin()
height += b[0] + b[2]
bl += b[2]
if height > maxHeight:
maxHeight = height
if bl > baseLine:
baseLine = bl
self._baseLine = baseLine
self._maxHeight = maxHeight
self._leading = 0 # XXX - How should this be calculated?
x = 0
for frag in self._fragments:
#
# TODO - Wordwrap
#
if x < frag.left():
x = frag.left()
fm = QFontMetrics(frag.font())
width = fm.horizontalAdvance(frag.text())
padding = frag.padding()
offset = padding[3] # Left margin
width += padding[1] + padding[3]
border = frag.border()
offset += border[3]
width += border[1] + border[3]
margin = frag.margin()
offset += margin[3]
width += margin[1] + margin[3]
frag.setPosition(QPoint(x+offset, self._baseLine))
if frag.border()[0] != 0:
#
# self._baseLine is where the text will be drawn
# fm.descent is the distance from the baseline of the
# text to the bottom of the rect
# The top of the bounding rect is at self._baseLine
# + fm.descent - rect.height
# The border is drawn at top-padding-border-margin+marin
#
top = self._baseLine + fm.descent() - rect.height() -1
y = top - padding[0] - border[0]
frag.setBorderRect(QRect(x+margin[3], y, width, height))
if x + width > maxWidth:
print(f'Wrap text: {frag.text()}, {x}+{width} = {x + width}')
x += width
return
def getLine(self) -> list[Fragment]:
return self._fragments
def getLeading(self) -> int:
return self._leading + self._maxHeight
_lines: list[Line] = []
def __init__(self, word: str) -> None:
self.resources = {}
#
# Have we already retrieved this word?
#
try:
self.current = json.loads(Word._words[word])
return
except KeyError:
pass
query = QSqlQuery()
query.prepare("SELECT * FROM words " "WHERE word = :word")
query.bindValue(":word", word)
if not query.exec():
query_error(query)
if query.next():
Word._words[word] = {
'word': word,
'source': query.value('source'),
'definition': json.loads(query.value("definition")),
}
self.current = Word._words[word]
return
source = 'mw'
response = requests.get(MWAPI.format(word=word))
if response.status_code != 200:
self.current = {}
return
data = json.loads(response.content.decode("utf-8"))
print(data)
self._words[word] = {
'word': word,
'source': source,
'definition': data,
}
self.current = Word._words[word]
query.prepare(
"INSERT INTO words "
"(word, source, definition) "
"VALUES (:word, :source, :definition)"
)
query.bindValue(":word", self.current['word'])
query.bindValue(":source", self.current['source'])
query.bindValue(":definition", json.dumps(self.current['definition']))
if not query.exec():
query_error(query)
return
def getWord(self) -> str:
return self.current['word']
def get_html(self) -> str | None:
if self.current['source'] == 'mw':
return self.mw_html()
elif self.current['source'] == 'apidictionary':
return self.apidictionary_html()
else:
raise Exception(f"Unknown source: {self.current['source']}")
def get_def(self) -> list[Line] | None:
if len(self._lines) > 0:
return self._lines
if len(self.resources.keys()) < 1:
#
# Colors we used
#
headerFont = QFontDatabase.font("OpenDyslexic", None, 10)
headerFont.setPixelSize(48)
labelFont = QFont(headerFont)
labelFont.setPixelSize(30)
boldFont = QFont(headerFont)
boldFont.setPixelSize(20)
textFont = QFont(boldFont)
italicFont = QFont(boldFont)
headerFont.setWeight(QFont.Weight.Bold)
boldFont.setBold(True)
italicFont.setItalic(True)
phonicFont = QFontDatabase.font("Gentium", None, 10)
phonicFont.setPixelSize(20)
self.resources = {
'colors': {
'base':QColor(Qt.GlobalColor.white),
'blue': QColor("#4a7d95"),
},
'fonts': {
'header': headerFont,
'label': labelFont,
'phonic': phonicFont,
'bold': boldFont,
'italic': italicFont,
'text': textFont,
}
}
if self.current['source'] == 'mw':
return self.mw_def()
elif self.current['source'] == 'apidictionary':
return None
else:
raise Exception(f"Unknown source: {self.current['source']}")
def mw_def(self) -> list[Line]:
lines: list[Word.Line] = []
for entry in self.current['definition']:
line = Word.Line()
meta = json.dumps(entry['meta'])
line.addFragment(
Fragment(
meta,
self.resources['fonts']['text'],asis=True
)
)
lines.append(line)
lines += self.mw_def_entry(entry)
self._lines = lines
return lines
def mw_seq(self, seq: list[Any]) -> list[Line]:
lines: list[Word.Line] = []
outer = ' '
inner = ' '
for value in seq:
sense = value[1]
#
# The optional 'sn' field tells us what sort of labeling to do
#
sn = sense.get('sn', '')
sns = sn.split(' ')
if len(sns) == 2:
outer = sns[0]
inner = sns[1]
elif len(sns) == 1:
if inner == ' ':
outer = sns[0]
else:
inner = sns[0]
for dt in sense['dt']:
if dt[0] == 'text':
line = Word.Line()
frag = Fragment(
f"{outer} {inner} ",
self.resources['fonts']['bold'],
color = self.resources['colors']['base']
)
outer = ' '
frag.setLeft(10)
line.addFragment(frag)
frag = Fragment(
dt[1],
self.resources['fonts']['text'],
color = self.resources['colors']['base']
)
frag.setLeft(30)
line.addFragment(frag)
lines.append(line)
elif dt[0] == 'vis':
for vis in dt[1]:
line = Word.Line()
frag =Fragment(f" ",
self.resources['fonts']['bold'],
)
frag.setLeft(45)
line.addFragment(frag)
line.addFragment(
Fragment(vis['t'],
self.resources['fonts']['text'],
color = QColor('#aaa')
)
)
lines.append(line)
return lines
def mw_def_entry(self, entry) -> list[Line]:
#
# Easy reference to colors
#
base = self.resources['colors']['base']
blue = self.resources['colors']['blue']
lines: list[Word.Line] = []
line = Word.Line()
hw = re.sub(r'\*', '', entry['hwi']['hw'])
frag = Fragment(hw, self.resources['fonts']['header'], color=base)
line.addFragment(frag)
frag = Fragment(' '+entry["fl"], self.resources['fonts']['label'], color=blue)
line.addFragment(frag)
lines.append(line)
if "vrs" in entry.keys():
line = self.Line()
space = ''
for vrs in entry["vrs"]:
frag = Fragment(space + vrs["va"], self.resources['fonts']['label'], color=base)
space = ' '
line.addFragment(frag)
lines.append(line)
if "prs" in entry["hwi"].keys():
line = self.Line()
frag = Fragment(entry['hwi']['hw'] + ' ', self.resources['fonts']['phonic'], color=base)
line.addFragment(frag)
for prs in entry["hwi"]["prs"]:
audio = self.sound_url(prs)
if audio is None:
audio = ""
frag = Fragment(prs['mw'], self.resources['fonts']['phonic'], color=blue)
frag.setAudio(audio)
frag.setPadding(0,10,3,12)
frag.setBorder(1)
frag.setMargin(0,3,0,3)
line.addFragment(frag)
lines.append(line)
if "ins" in entry.keys():
line = self.Line()
space = ''
for ins in entry['ins']:
try:
frag = Fragment(ins['il'], self.resources['fonts']['text'], color=base)
line.addFragment(frag)
space = ' '
except KeyError:
pass
frag = Fragment(space + ins['if'], self.resources['fonts']['bold'], color=base)
line.addFragment(frag)
space = '; '
lines.append(line)
if 'lbs' in entry.keys():
line = self.Line()
frag = Fragment('; '.join(entry['lbs']), self.resources['fonts']['bold'], color=base)
line.addFragment(frag)
lines.append(line)
for value in entry['def']: # has multiple 'sseg' or 'vd' init
for k,v in value.items():
if k == 'sseq': # has multiple 'senses'
for seq in v:
r = self.mw_seq(seq)
lines += r
elif k == 'vd':
line = self.Line()
line.addFragment(Fragment(
v,
self.resources['fonts']['italic'],
color=blue
))
lines.append(line)
return lines
def sound_url(self, prs: dict[str, Any], fmt: str = "ogg") -> str | None:
"""Create a URL from a PRS structure."""
base = f"https://media.merriam-webster.com/audio/prons/en/us/{fmt}"
if "sound" not in prs.keys():
return None
audio = prs["sound"]["audio"]
m = re.match(r"(bix|gg|[a-zA-Z])", audio)
if m:
url = base + f"/{m.group(1)}/"
else:
url = base + "/number/"
url += audio + f".fmt"
return url
def mw_html(self) -> str:
def parse_sn(sn: str, old: str) -> str:
return sn
#
# Create the header, base word and its label
#
word = self.current["hwi"]["hw"]
label = self.current["fl"]
html = f'<h1 class="def-word">{word} <span class="def-label">{label}</span></h1>\n'
#
# If there are variants, then add them in an unordered list.
# CSS will make it pretty
#
if "vrs" in self.current.keys():
html += "<ul class=\"def-vrs'>\n"
html += "<li>"
html += "</li>\n<li>".join(
[vrs["va"] for vrs in self.current["vrs"]]
)
html += "</li>\n</ul>\n"
#
# If there is a pronunciation section, create it
#
if "prs" in self.current["hwi"].keys():
tmp = []
for prs in self.current["hwi"]["prs"]:
url = self.sound_url(prs)
how = prs["mw"]
if url:
tmp.append(f'<a href="{url}">\\{how}\\</a>')
else:
tmp.append(f"\\{how}\\")
html += '<span class="def-phonetic">'
html += '</span><span="def-phonetic">'.join(tmp)
html += "</span>\n"
#
# If there are inflections, create a header for that.
#
if "ins" in self.current.keys():
html += '<h2 class="def-word">'
html += ", ".join([ins["if"] for ins in self.current["ins"]])
html += "</h2>\n"
#
# Start creating the definition section
#
html += "<ul class='def-outer'>\n"
for meaning in self.current["def"]:
html += f"<li>{meaning['vd']}\n"
html += '<ul class="def-inner">\n'
label = ""
for sseq in meaning["sseq"]:
for sense in sseq:
label = parse_sn(sense[1]["sn"], label)
sls = ""
if "sls" in sense[1].keys():
sls = ", ".join(sense[1]["sls"])
sls = f'<span class="def-sls">{sls}</span> '
for dt in sense[1]["dt"]:
if dt[0] == "text":
html += f'<li class="def-text"><span class="def-sn">{label}</span>{sls}{dt[1]}</li>\n'
elif dt[0] == "vis":
for vis in dt[1]:
html += (
f"<li class=\"def-vis\">{vis['t']}</li>\n"
)
else:
print(f"Do something with {dt[0]}")
html += "</ul>\n"
html += "</ul>\n"
return html
def apidictionary_html(self) -> str:
html = ""
return html
class Definition(QWidget):
pronounce = pyqtSignal(str)
def __init__(self, w: Word, *args: Any, **kwargs: Any) -> None:
super(Definition, self).__init__(*args, **kwargs)
self._word:str = w.getWord()
lines = w.get_def()
assert lines is not None
self._lines = lines
self._buttons:list[Fragment] = []
base = 0
for line in self._lines:
line.finalizeLine(self.width())
base += line.getLeading()
self.setFixedHeight(base)
return
def resizeEvent(self, event: QResizeEvent) -> None:
base = 0
for line in self._lines:
line.finalizeLine(event.size().width())
base += line.getLeading()
self.setFixedHeight(base)
super(Definition,self).resizeEvent(event)
return
_downRect: QRect | None = None
def mousePressEvent(self, event: Optional[QMouseEvent]) -> None:
if not event:
return super().mousePressEvent(event)
for frag in self._buttons:
rect = frag.borderRect()
if rect.contains(event.pos()):
self._downRect = rect
return
return super().mousePressEvent(event)
def mouseReleaseEvent(self, event: Optional[QMouseEvent]) -> None:
if not event:
return super().mouseReleaseEvent(event)
if self._downRect is not None and self._downRect.contains(event.pos()):
self.pronounce.emit(
"https://media.merriam-webster.com/audio/prons/en/us/ogg/a/await001.ogg"
)
self._downRect = None
return
self._downRect = None
return super().mouseReleaseEvent(event)
def paintEvent(self, _: Optional[QPaintEvent]) -> None: # noqa
painter = QPainter(self)
painter.save()
painter.setBrush(QBrush())
painter.setPen(QColor("white"))
#
# Each line needs a base calculated. To do that, we need to find the
# bounding rectangle of the text. Once we have the bounding rectangle,
# we can use the descendant to calculate the baseline within that
# bounding box.
#
# All text on this line needs to be on the same baseline
#
assert self._lines is not None
base = 0
for line in self._lines:
transform = QTransform()
transform.translate(0, base)
painter.setTransform(transform)
for frag in line.getLine():
painter.save()
painter.setFont(frag.font())
painter.setPen(frag.color())
#
# Is this a button?
#
href = frag.audio()
if href:
radius = frag.borderRect().height()/2
painter.drawRoundedRect(frag.borderRect(), radius, radius)
#
# is it an anchor?
#
elif frag.wRef():
painter.drawLine(frag.borderRect().bottomLeft(), frag.borderRect().bottomRight())
painter.drawText(frag.position(), frag.text())
painter.restore()
painter.resetTransform()
base += line.getLeading()
painter.restore()
return
class DefinitionArea(QScrollArea):
def __init__(self, w: Word, *args: Any, **kwargs: Any) -> None:
super(DefinitionArea,self).__init__(*args,*kwargs)
d = Definition(w)
self.setWidget(d)
self.setWidgetResizable(True)
self.setVerticalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOn
)
return