Source code for musicscore.layout

from typing import Union, Optional

from musicscore.util import isinstance_as_string
from musicscore.xmlwrapper import XMLWrapper
from musicxml.xmlelement.xmlelement import XMLPageLayout, XMLPageMargins, XMLScaling, XMLDefaults, XMLSystemLayout, \
    XMLSystemMargins, XMLStaffLayout

__all__ = ['PAGE_MARGINS', 'PAGE_SIZES', 'SYSTEM_MARGINS', 'SYSTEM_LAYOUT', 'STAFF_LAYOUT', 'SCALING', 'Margins',
           'Scaling',
           'PageLayout', 'SystemLayout', 'StaffLayout']
#:
PAGE_MARGINS = {
    'A4': {
        'portrait': {'left': 140, 'right': 70, 'top': 70, 'bottom': 70},
        'landscape': {'left': 111, 'right': 70, 'top': 70, 'bottom': 70}
    }, 'A3': {
        'portrait': {'left': 111, 'right': 70, 'top': 70, 'bottom': 70},
        'landscape': {'left': 111, 'right': 70, 'top': 70, 'bottom': 70}
    }
}

#:
PAGE_SIZES = {'A4': (209.991, 297.0389), 'A3': (297.0389, 419.9819)}

#:
SYSTEM_MARGINS = {'left': 0, 'right': 0}

#:
SYSTEM_LAYOUT = {'system_distance': 117, 'top_system_distance': 117}

#:
STAFF_LAYOUT = {'staff_distance': 80}

#:
SCALING = {
    'millimeters': 7.2319,
    'tenths': 40
}


class LayoutMixin:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._parent = None

    @property
    def parent(self):
        """
        :return: musicscore object which uses this layout object.

        .. todo::
           Print implementation: At this moment only :obj:`~musicscore.score.Score` is implemented.
        """
        return self._parent

    @parent.setter
    def parent(self, val):
        self._parent = val
        if isinstance(self, PageLayout):
            if isinstance_as_string(self.parent, 'Score'):
                self._update()
                self.parent.xml_object.xml_defaults.xml_page_layout = self.xml_object
            else:
                raise NotImplementedError
        elif isinstance_as_string(self, 'SystemLayout'):
            if isinstance_as_string(self.parent, 'Score'):
                self.parent.xml_object.xml_defaults.xml_system_layout = self.xml_object
            else:
                raise NotImplementedError
        elif isinstance_as_string(self, 'StaffLayout'):
            if isinstance_as_string(self.parent, 'Score'):
                self.parent.xml_object.xml_defaults.xml_staff_layout = self.xml_object
            else:
                raise NotImplementedError
        else:
            raise NotImplementedError


[docs]class Margins: def __init__(self, parent: Union['PageLayout', 'SystemLayout'], left: Union[float, int] = None, right: Union[float, int] = None, top: Union[float, int] = None, bottom: Union[float, int] = None): self._parent = None self._left = None self._right = None self._top = None self._parent_xml_object = None self._parent = None self.parent = parent self.left = left self.right = right self.top = top self.bottom = bottom def _update_parent(self, side): if side not in ['left', 'right', 'top', 'bottom']: raise ValueError if side in ['top', 'bottom'] and isinstance(self.parent, SystemLayout): if eval(f"self.{side}") is not None: raise ValueError else: setattr(self._parent_xml_object, f"xml_{side}_margin", eval(f"self.{side}")) @property def bottom(self): """ Set and get :obj:`~musicxml.xmlelement.xmlelement.XMLBottomMargin` value of parent. """ return self._bottom @bottom.setter def bottom(self, val): self._bottom = val self._update_parent('bottom') @property def left(self) -> Union[int, float]: """ Set and get :obj:`~musicxml.xmlelement.xmlelement.XMLLeftMargin` value of parent. """ return self._left @left.setter def left(self, val): self._left = val self._update_parent('left') @property def parent(self): """ :return: parent layout object. :obj:`PageLayout` and :obj:`SystemLayout` are implemented. """ return self._parent @parent.setter def parent(self, val): self._parent = val if isinstance(val, PageLayout): self._parent_xml_object = self._parent.xml_object.xml_page_margins elif isinstance(val, SystemLayout): self._parent_xml_object = self._parent.xml_object.xml_system_margins else: raise NotImplementedError @property def right(self) -> Union[int, float]: """ Set and get :obj:`~musicxml.xmlelement.xmlelement.XMLRightMargin` value of parent. """ return self._right @right.setter def right(self, val): self._right = val self._update_parent('right') @property def top(self) -> Union[int, float]: """ Set and get :obj:`~musicxml.xmlelement.xmlelement.XMLTopMargin` value of parent. """ return self._top @top.setter def top(self, val) -> Union[int, float]: self._top = val self._update_parent('top')
[docs]class Scaling(XMLWrapper): _ATTRIBUTES = {'millimeters', 'tenths', 'score'} XMLClass = XMLScaling def __init__(self, millimeters: Union[int, float] = SCALING['millimeters'], tenths: Union[int, float] = SCALING['tenths']): super().__init__() self._xml_object = self.XMLClass() self._millimeters = None self._tenths = None self._score = None self.millimeters = millimeters self.tenths = tenths def _update_score(self): if self.score: self.score.page_layout._set_page_height_and_width() @property def millimeters(self) -> Union[int, float]: """ Set and get millimeters value of scaling object. After setting value, parent :obj:`~musicscore.score.Score`'s :obj:`PageLayout` is updated to reflect the changes. :return: millimeters :rtype: Union[int, float] """ return self._millimeters @millimeters.setter def millimeters(self, val): if val != self._millimeters: self._millimeters = val self.xml_object.xml_millimeters = val self._update_score() @property def score(self): """ Set and get parent :obj:`~musicscore.score.Score`. After setting score, its :obj:`~musicxml.xmlelement.xmlelement.XMLScaling` and :obj:`~musicxml.xmlelement.xmlelement.XMLDefaults` are created if needed. :return: parent score :rtype: :obj:`~musicscore.score.Score` """ return self._score @score.setter def score(self, val): self._score = val if not self.score.xml_object.xml_defaults: self.score.xml_object.xml_defaults = XMLDefaults() self.score.xml_object.xml_defaults.xml_scaling = self.xml_object @property def tenths(self) -> Union[int, float]: """ Set and get tenths value of scaling object. After setting value, parent :obj:`~musicscore.score.Score`'s :obj:`PageLayout` is updated to reflect the changes. :return: tenths :rtype: Union[int, float] """ return self._tenths @tenths.setter def tenths(self, val): if val != self._tenths: self._tenths = val self.xml_object.xml_tenths = val self._update_score()
[docs] def millimeters_to_tenths(self, x: Union[int, float]) -> Union[int, float]: """ Converts millimeter value into tenths :param x: millimeters :return: calculated tenths """ return round((x * self.tenths) / self.millimeters)
[docs]class PageLayout(XMLWrapper, LayoutMixin): """ :param size: :obj:`PAGE_SIZES` :param orientation: 'portrait', 'landscape' """ _ATTRIBUTES = {'scaling', 'size', 'orientation', 'parent'} XMLClass = XMLPageLayout def __init__(self, size: str = 'A4', orientation: str = 'portrait'): super().__init__() self._xml_object = self.XMLClass() self._xml_object.xml_page_margins = XMLPageMargins(type='both') self._size = None self._orientation = None self.size = size self.orientation = orientation self._margins = Margins(parent=self, **PAGE_MARGINS[self.size][self.orientation]) def _get_page_height(self): return self.scaling.millimeters_to_tenths(PAGE_SIZES[self.size][1]) if self.orientation == 'portrait' else \ self.scaling.millimeters_to_tenths(PAGE_SIZES[self.size][0]) def _get_page_width(self): return self.scaling.millimeters_to_tenths(PAGE_SIZES[self.size][0]) if self.orientation == 'portrait' else \ self.scaling.millimeters_to_tenths(PAGE_SIZES[self.size][1]) def _set_page_height_and_width(self): self.xml_object.xml_page_height = self._get_page_height() self.xml_object.xml_page_width = self._get_page_width() def _update(self): self._set_page_height_and_width() self._margins = Margins(parent=self, **PAGE_MARGINS[self.size][self.orientation]) @property def margins(self) -> Margins: """ Gets margins attribute. :return: margins object :rtype: :obj:`Margins` """ return self._margins @property def orientation(self) -> str: """ Set and get orientation. Permitted values are ['portrait', 'landscape']. After setting value, if parent and size already exist, page's height and width are set. :return: 'portrait', 'landscape' :rtype: str """ return self._orientation @orientation.setter def orientation(self, val): _permitted = ['portrait', 'landscape'] if val not in _permitted: raise ValueError(f"{val} can only be: {_permitted}") if val != self._orientation: self._orientation = val if self.size and self.parent: self._update() @property def scaling(self) -> Scaling: """ :return: :obj:`~musicscore.score.Score`'s :obj:`Scaling`. :rtype: :obj:`Scaling` """ return self.parent.get_root().scaling @property def size(self) -> str: """ Set and get orientation. Permitted values are keys of obj:`PAGE_SIZES`. After setting value, if parent and orientation already exist, page's height and width are set. :return: sizes in :obj:`PAGE_SIZES` :rtype: str """ return self._size @size.setter def size(self, val): if val not in PAGE_SIZES: raise NotImplementedError if val != self._size: self._size = val if self.orientation and self.parent: self._update()
[docs]class SystemLayout(XMLWrapper, LayoutMixin): _ATTRIBUTES = {'system_distance', 'top_system_distance', 'parent'} XMLClass = XMLSystemLayout def __init__(self, system_distance: Union[int, float] = SYSTEM_LAYOUT['system_distance'], top_system_distance: Union[int, float] = SYSTEM_LAYOUT['top_system_distance']): super().__init__() self._xml_object = self.XMLClass() self._xml_object.xml_system_margins = XMLSystemMargins() self.system_distance = system_distance self.top_system_distance = top_system_distance self._margins = Margins(parent=self, **SYSTEM_MARGINS) @property def margins(self) -> Margins: """ Gets margins attribute. :return: margins object :rtype: :obj:`Margins` """ return self._margins @property def system_distance(self) -> Optional[Union[int, float]]: """ Set and get ``value`` of :obj:`~musicxml.xmlelement.xmlelement.XMLSystemDistance`. :return: ``self.xml_object.xml_system_distance.value_`` :rtype: int, float, None """ if self.xml_object.xml_system_distance: return self.xml_object.xml_system_distance.value_ @system_distance.setter def system_distance(self, val): self.xml_object.xml_system_distance = val @property def top_system_distance(self) -> Optional[Union[int, float]]: """ Set and get ``value`` of :obj:`~musicxml.xmlelement.xmlelement.XMLTopSystemDistance`. :return: ``self.xml_object.xml_top_system_distance.value_`` :rtype: int, float, None """ if self.xml_object.xml_top_system_distance: return self.xml_object.xml_top_system_distance.value_ @top_system_distance.setter def top_system_distance(self, val): self.xml_object.xml_top_system_distance = val
[docs]class StaffLayout(XMLWrapper, LayoutMixin): _ATTRIBUTES = {'staff_distance', 'parent'} XMLClass = XMLStaffLayout def __init__(self, staff_distance=STAFF_LAYOUT['staff_distance']): super().__init__() self._xml_object = self.XMLClass() self.staff_distance = staff_distance @property def staff_distance(self) -> Optional[Union[int, float]]: """ Set and get ``value_`` of :obj:`~musicxml.xmlelement.xmlelement.XMLStaffDistance`. :return: ``self.xml_object.xml_staff_distance.value_`` :rtype: int, float, None """ if self.xml_object.xml_staff_distance: return self.xml_object.xml_staff_distance.value_ @staff_distance.setter def staff_distance(self, val): self.xml_object.xml_staff_distance = val