1# -*- coding: utf-8 -*- 2# ----------------------------------------------------------------------------- 3# Name: style.py 4# Purpose: Music21 classes for non-analytic display properties 5# 6# Authors: Michael Scott Cuthbert 7# 8# Copyright: Copyright © 2016 Michael Scott Cuthbert and the music21 9# Project 10# License: BSD, see license.txt 11# ----------------------------------------------------------------------------- 12''' 13The style module represents information about the style of a Note, Accidental, 14etc. such that precise positioning information, layout, size, etc. can be specified. 15''' 16from typing import Optional, Union 17import unittest 18 19from music21 import common 20from music21 import exceptions21 21from music21.prebase import ProtoM21Object 22 23 24class TextFormatException(exceptions21.Music21Exception): 25 pass 26 27 28class Style(ProtoM21Object): 29 ''' 30 A style object is a lightweight object that 31 keeps track of information about the look of an object. 32 33 >>> st = style.Style() 34 >>> st.units 35 'tenths' 36 >>> st.absoluteX is None 37 True 38 39 >>> st.absoluteX = 20.4 40 >>> st.absoluteX 41 20.4 42 43 ''' 44 _DOC_ATTR = { 45 'hideObjectOnPrint': '''if set to `True` will not print upon output 46 (only used in MusicXML output at this point and 47 Lilypond for notes, chords, and rests).''', 48 } 49 50 def __init__(self): 51 self.size = None 52 53 self.relativeX: Optional[Union[float, int]] = None 54 self.relativeY: Optional[Union[float, int]] = None 55 self.absoluteX: Optional[Union[float, int]] = None 56 57 # managed by property below. 58 self._absoluteY: Optional[Union[float, int]] = None 59 60 self._enclosure: Optional[str] = None 61 62 # how should this symbol be represented in the font? 63 # SMuFL characters are allowed. 64 self.fontRepresentation = None 65 66 self.color: Optional[str] = None 67 68 self.units: str = 'tenths' 69 self.hideObjectOnPrint: bool = False 70 71 def _getEnclosure(self): 72 return self._enclosure 73 74 def _setEnclosure(self, value): 75 if value is None: 76 self._enclosure = value 77 elif value == 'none': 78 self._enclosure = None 79 elif value.lower() in ('rectangle', 'square', 'oval', 'circle', 80 'bracket', 'triangle', 'diamond', 81 'pentagon', 'hexagon', 'heptagon', 'octagon', 82 'nonagon', 'decagon'): 83 self._enclosure = value.lower() 84 else: 85 raise TextFormatException(f'Not a supported enclosure: {value}') 86 87 enclosure = property(_getEnclosure, 88 _setEnclosure, 89 doc=''' 90 Get or set the enclosure. Valid names are 91 rectangle, square, oval, circle, bracket, triangle, diamond, 92 pentagon, hexagon, heptagon, octagon, 93 nonagon, decagon or None. 94 95 96 >>> tst = style.TextStyle() 97 >>> tst.enclosure = None 98 >>> tst.enclosure = 'rectangle' 99 >>> tst.enclosure 100 'rectangle' 101 102 ''') 103 104 def _getAbsoluteY(self): 105 return self._absoluteY 106 107 def _setAbsoluteY(self, value): 108 if value is None: 109 self._absoluteY = None 110 elif value == 'above': 111 self._absoluteY = 10 112 elif value == 'below': 113 self._absoluteY = -70 114 else: 115 try: 116 self._absoluteY = common.numToIntOrFloat(value) 117 except ValueError as ve: 118 raise TextFormatException( 119 f'Not a supported absoluteY position: {value!r}' 120 ) from ve 121 122 absoluteY = property(_getAbsoluteY, 123 _setAbsoluteY, 124 doc=''' 125 Get or set the vertical position, where 0 126 is the top line of the staff and units 127 are in 10ths of a staff space. 128 129 Other legal positions are 'above' and 'below' which 130 are synonyms for 10 and -70 respectively (for 5-line 131 staves; other staves are not yet implemented) 132 133 >>> te = style.Style() 134 >>> te.absoluteY = 10 135 >>> te.absoluteY 136 10 137 138 >>> te.absoluteY = 'below' 139 >>> te.absoluteY 140 -70 141 ''') 142 143 144class NoteStyle(Style): 145 ''' 146 A Style object that also includes stem and accidental style information. 147 148 Beam style is stored on the Beams object, as is lyric style 149 ''' 150 151 def __init__(self): 152 super().__init__() 153 self.stemStyle = None 154 self.accidentalStyle = None 155 self.noteSize = None # can be 'cue' etc. 156 157 158class TextStyle(Style): 159 ''' 160 A Style object that also includes text formatting. 161 162 >>> ts = style.TextStyle() 163 >>> ts.classes 164 ('TextStyle', 'Style', 'ProtoM21Object', 'object') 165 ''' 166 167 def __init__(self): 168 super().__init__() 169 self._fontFamily = None 170 self._fontSize = None 171 self._fontStyle = None 172 self._fontWeight = None 173 self._letterSpacing = None 174 175 self.lineHeight = None 176 self.textDirection = None 177 self.textRotation = None 178 self.language = None 179 # this might be a complex device -- underline, overline, line-through etc. 180 self.textDecoration = None 181 182 self._justify = None 183 self._alignHorizontal = None 184 self._alignVertical = None 185 186 def _getAlignVertical(self): 187 return self._alignVertical 188 189 def _setAlignVertical(self, value): 190 if value in (None, 'top', 'middle', 'bottom', 'baseline'): 191 self._alignVertical = value 192 else: 193 raise TextFormatException(f'invalid vertical align: {value}') 194 195 alignVertical = property(_getAlignVertical, 196 _setAlignVertical, 197 doc=''' 198 Get or set the vertical align. Valid values are top, middle, bottom, baseline 199 or None 200 201 >>> te = style.TextStyle() 202 >>> te.alignVertical = 'top' 203 >>> te.alignVertical 204 'top' 205 ''') 206 207 def _getAlignHorizontal(self): 208 return self._alignHorizontal 209 210 def _setAlignHorizontal(self, value): 211 if value in (None, 'left', 'right', 'center'): 212 self._alignHorizontal = value 213 else: 214 raise TextFormatException(f'invalid horizontal align: {value}') 215 216 alignHorizontal = property(_getAlignHorizontal, 217 _setAlignHorizontal, 218 doc=''' 219 Get or set the horizontal alignment. Valid values are left, right, center, 220 or None 221 222 223 >>> te = style.TextStyle() 224 >>> te.alignHorizontal = 'right' 225 >>> te.alignHorizontal 226 'right' 227 ''') 228 229 def _getJustify(self): 230 return self._justify 231 232 def _setJustify(self, value): 233 if value is None: 234 self._justify = None 235 else: 236 if value.lower() not in ('left', 'center', 'right', 'full'): 237 raise TextFormatException(f'Not a supported justification: {value}') 238 self._justify = value.lower() 239 240 justify = property(_getJustify, 241 _setJustify, 242 doc=''' 243 Get or set the justification. Valid values are left, 244 center, right, full (not supported by MusicXML), and None 245 246 >>> tst = style.TextStyle() 247 >>> tst.justify = 'center' 248 >>> tst.justify 249 'center' 250 ''') 251 252 def _getStyle(self): 253 return self._fontStyle 254 255 def _setStyle(self, value): 256 if value is None: 257 self._fontStyle = None 258 else: 259 if value.lower() not in ('italic', 'normal', 'bold', 'bolditalic'): 260 raise TextFormatException(f'Not a supported fontStyle: {value}') 261 self._fontStyle = value.lower() 262 263 fontStyle = property(_getStyle, 264 _setStyle, 265 doc=''' 266 Get or set the style, as normal, italic, bold, and bolditalic. 267 268 >>> tst = style.TextStyle() 269 >>> tst.fontStyle = 'bold' 270 >>> tst.fontStyle 271 'bold' 272 ''') 273 274 def _getWeight(self): 275 return self._fontWeight 276 277 def _setWeight(self, value): 278 if value is None: 279 self._fontWeight = None 280 else: 281 if value.lower() not in ('normal', 'bold'): 282 raise TextFormatException(f'Not a supported fontWeight: {value}') 283 self._fontWeight = value.lower() 284 285 fontWeight = property(_getWeight, 286 _setWeight, 287 doc=''' 288 Get or set the weight, as normal, or bold. 289 290 >>> tst = style.TextStyle() 291 >>> tst.fontWeight = 'bold' 292 >>> tst.fontWeight 293 'bold' 294 ''') 295 296 def _getSize(self): 297 return self._fontSize 298 299 def _setSize(self, value): 300 if value is not None: 301 try: 302 value = common.numToIntOrFloat(value) 303 except ValueError: 304 pass # MusicXML font sizes can be CSS strings. 305 # raise TextFormatException('Not a supported size: %s' % value) 306 self._fontSize = value 307 308 fontSize = property(_getSize, 309 _setSize, 310 doc=''' 311 Get or set the size. Best, an int or float, but also a css font size 312 313 >>> tst = style.TextStyle() 314 >>> tst.fontSize = 20 315 >>> tst.fontSize 316 20 317 ''') 318 319 def _getLetterSpacing(self): 320 return self._letterSpacing 321 322 def _setLetterSpacing(self, value): 323 if value != 'normal' and value is not None: 324 # convert to number 325 try: 326 value = float(value) 327 except ValueError as ve: 328 raise TextFormatException( 329 f'Not a supported letterSpacing: {value!r}' 330 ) from ve 331 332 self._letterSpacing = value 333 334 letterSpacing = property(_getLetterSpacing, 335 _setLetterSpacing, 336 doc=''' 337 Get or set the letter spacing. 338 339 >>> tst = style.TextStyle() 340 >>> tst.letterSpacing = 20 341 >>> tst.letterSpacing 342 20.0 343 >>> tst.letterSpacing = 'normal' 344 ''') 345 346 @property 347 def fontFamily(self): 348 ''' 349 Returns a list of font family names associated with 350 the style, or sets the font family name list. 351 352 If a single string is passed then it is converted to 353 a list. 354 355 >>> ts = style.TextStyle() 356 >>> ff = ts.fontFamily 357 >>> ff 358 [] 359 >>> ff.append('Times') 360 >>> ts.fontFamily 361 ['Times'] 362 >>> ts.fontFamily.append('Garamond') 363 >>> ts.fontFamily 364 ['Times', 'Garamond'] 365 >>> ts.fontFamily = 'Helvetica, sans-serif' 366 >>> ts.fontFamily 367 ['Helvetica', 'sans-serif'] 368 ''' 369 if self._fontFamily is None: 370 self._fontFamily = [] 371 return self._fontFamily 372 373 @fontFamily.setter 374 def fontFamily(self, newFamily): 375 if common.isIterable(newFamily): 376 self._fontFamily = newFamily 377 else: 378 self._fontFamily = [f.strip() for f in newFamily.split(',')] 379 380 381class TextStylePlacement(TextStyle): 382 ''' 383 TextStyle plus a placement attribute 384 ''' 385 386 def __init__(self): 387 super().__init__() 388 self.placement = None 389 390 391class BezierStyle(Style): 392 ''' 393 From the MusicXML Definition. 394 ''' 395 396 def __init__(self): 397 super().__init__() 398 399 self.bezierOffset = None 400 self.bezierOffset2 = None 401 402 self.bezierX = None 403 self.bezierY = None 404 self.bezierX2 = None 405 self.bezierY2 = None 406 407 408class LineStyle(Style): 409 ''' 410 from the MusicXML Definition 411 412 Defines lineShape ('straight', 'curved' or None) 413 lineType ('solid', 'dashed', 'dotted', 'wavy' or None) 414 dashLength (in tenths) 415 spaceLength (in tenths) 416 ''' 417 418 def __init__(self): 419 super().__init__() 420 421 self.lineShape = None 422 self.lineType = None 423 self.dashLength = None 424 self.spaceLength = None 425 426 427class StreamStyle(Style): 428 ''' 429 Includes several elements in the MusicXML <appearance> tag in <defaults> 430 along with <music-font> and <word-font> 431 ''' 432 433 def __init__(self): 434 super().__init__() 435 self.lineWidths = [] # two-tuples of type, width measured in tenths 436 self.noteSizes = [] # two-tuples of type and percentages of the normal size 437 self.distances = [] # two-tuples of beam or hyphen and tenths 438 self.otherAppearances = [] # two-tuples of type and tenths 439 self.musicFont = None # None or a TextStyle object 440 self.wordFont = None # None or a TextStyle object 441 self.lyricFonts = [] # a list of TextStyle objects 442 self.lyricLanguages = [] # a list of strings 443 444 self.printPartName = True 445 self.printPartAbbreviation = True 446 447 # can be None -- meaning no comment, 448 # 'none', 'measure', or 'system'... 449 self.measureNumbering = None 450 self.measureNumberStyle = None 451 452 453class BeamStyle(Style): 454 ''' 455 Style for beams 456 ''' 457 458 def __init__(self): 459 super().__init__() 460 self.fan = None 461 462 463class StyleMixin(common.SlottedObjectMixin): 464 ''' 465 Mixin for any class that wants to support style and editorial, since several 466 non-music21 objects, such as Lyrics and Accidentals will support Style. 467 468 Not used by Music21Objects because of the added trouble in copying etc. so 469 there is code duplication with base.Music21Object 470 ''' 471 _styleClass = Style 472 473 __slots__ = ('_style', '_editorial') 474 475 def __init__(self): 476 # no need to call super().__init__() on SlottedObjectMixin 477 self._style = None 478 self._editorial = None 479 480 @property 481 def hasStyleInformation(self): 482 ''' 483 Returns True if there is a :class:`~music21.style.Style` object 484 already associated with this object, False otherwise. 485 486 Calling .style on an object will always create a new 487 Style object, so even though a new Style object isn't too expensive 488 to create, this property helps to prevent creating new Styles more than 489 necessary. 490 491 >>> lObj = note.Lyric('hello') 492 >>> lObj.hasStyleInformation 493 False 494 >>> lObj.style 495 <music21.style.TextStylePlacement object at 0x10b0a2080> 496 >>> lObj.hasStyleInformation 497 True 498 ''' 499 try: 500 self._style 501 except AttributeError: 502 pass 503 504 return not (self._style is None) 505 506 @property 507 def style(self): 508 ''' 509 Returns (or Creates and then Returns) the Style object 510 associated with this object, or sets a new 511 style object. Different classes might use 512 different Style objects because they might have different 513 style needs (such as text formatting or bezier positioning) 514 515 Eventually will also query the groups to see if they have 516 any styles associated with them. 517 518 >>> acc = pitch.Accidental() 519 >>> st = acc.style 520 >>> st 521 <music21.style.TextStyle object at 0x10ba96208> 522 >>> st.absoluteX = 20.0 523 >>> st.absoluteX 524 20.0 525 >>> acc.style = style.TextStyle() 526 >>> acc.style.absoluteX is None 527 True 528 ''' 529 if self._style is None: 530 styleClass = self._styleClass 531 self._style = styleClass() 532 return self._style 533 534 @style.setter 535 def style(self, newStyle): 536 self._style = newStyle 537 538 @property 539 def hasEditorialInformation(self): 540 ''' 541 Returns True if there is a :class:`~music21.editorial.Editorial` object 542 already associated with this object, False otherwise. 543 544 Calling .style on an object will always create a new 545 Style object, so even though a new Style object isn't too expensive 546 to create, this property helps to prevent creating new Styles more than 547 necessary. 548 549 >>> acc = pitch.Accidental('#') 550 >>> acc.hasEditorialInformation 551 False 552 >>> acc.editorial 553 <music21.editorial.Editorial {}> 554 >>> acc.hasEditorialInformation 555 True 556 ''' 557 return not (self._editorial is None) 558 559 @property 560 def editorial(self): 561 ''' 562 a :class:`~music21.editorial.Editorial` object that stores editorial information 563 (comments, footnotes, harmonic information, ficta). 564 565 Created automatically as needed: 566 567 >>> acc = pitch.Accidental() 568 >>> acc.editorial 569 <music21.editorial.Editorial {}> 570 >>> acc.editorial.ficta = pitch.Accidental('sharp') 571 >>> acc.editorial.ficta 572 <music21.pitch.Accidental sharp> 573 >>> acc.editorial 574 <music21.editorial.Editorial {'ficta': <music21.pitch.Accidental sharp>}> 575 ''' 576 from music21 import editorial 577 if self._editorial is None: 578 self._editorial = editorial.Editorial() 579 return self._editorial 580 581 @editorial.setter 582 def editorial(self, ed): 583 self._editorial = ed 584 585 586class Test(unittest.TestCase): 587 pass 588 589 590if __name__ == '__main__': 591 import music21 592 music21.mainTest(Test) # , runTest='') 593 594