From 1088966991f32b73a36bfe22375bc6ac0ad8fb57 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 24 Jun 2020 12:23:05 -0300 Subject: [PATCH] [improvement][settings] new property 'ui.scale_factor' responsible for defining the interface scale factor --- CHANGELOG.md | 6 ++++ README.md | 1 + bauh/api/abstract/view.py | 14 ++++++++ bauh/app.py | 8 +++++ bauh/view/core/config.py | 3 +- bauh/view/core/settings.py | 18 ++++++++-- bauh/view/qt/components.py | 56 ++++++++++++++++++++++++++++-- bauh/view/resources/locale/ca | 2 ++ bauh/view/resources/locale/de | 2 ++ bauh/view/resources/locale/en | 2 ++ bauh/view/resources/locale/es | 2 ++ bauh/view/resources/locale/it | 2 ++ bauh/view/resources/locale/pt | 2 ++ bauh/view/resources/locale/ru | 2 ++ bauh/view/resources/locale/tr | 2 ++ pictures/releases/0.9.6/scale.png | Bin 0 -> 16421 bytes 16 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 pictures/releases/0.9.6/scale.png diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb8403a..338c447f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - big refactoring regarding the components states (now it is easier to maintain the code) - some components compatibility with the system's color scheme - allowing "Oxygen" as a default style +- Settings + - new property **ui.scale_factor** responsible for defining the interface scale factor. Useful if bauh looks + small on the screen. It can be changed through the settings window (Interface tab): +

+ +

### Fixes - AppImage diff --git a/README.md b/README.md index 822899ca..c3a5b7c1 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,7 @@ ui: updates_icon: null # defines a path to a custom icon indicating updates hdpi: true # enables HDPI rendering improvements. Use 'false' to disable them if you think the interface looks strange auto_scale: false # activates Qt auto screen scale factor (QT_AUTO_SCREEN_SCALE_FACTOR). It fixes scaling issues for some desktop environments ( like Gnome ) + scale_factor: 1.0 # defines the interface display scaling factor (Qt). Raise the value to raise the interface size. The settings window display this value as a percentage (e.g: 1.0 -> 100%). updates: check_interval: 30 # the updates checking interval in SECONDS ask_for_reboot: true # if a dialog asking for a system reboot should be displayed after a successful upgrade diff --git a/bauh/api/abstract/view.py b/bauh/api/abstract/view.py index 2cad07f1..3e3a7369 100644 --- a/bauh/api/abstract/view.py +++ b/bauh/api/abstract/view.py @@ -210,3 +210,17 @@ class TabGroupComponent(ViewComponent): def get_tab(self, id_: str) -> TabComponent: if self.tab_map: return self.tab_map.get(id_) + + +class RangeInputComponent(InputViewComponent): + + def __init__(self, id_: str, label: str, tooltip: str, min_value: float, max_value: float, + step_value: float, value: float = None, max_width: int = None): + super(RangeInputComponent, self).__init__(id_=id_) + self.label = label + self.tooltip = tooltip + self.min = min_value + self.max = max_value + self.step = step_value + self.value = value + self.max_width = max_width diff --git a/bauh/app.py b/bauh/app.py index 5a9f3a18..4e9431ff 100755 --- a/bauh/app.py +++ b/bauh/app.py @@ -1,5 +1,6 @@ import os import sys +import traceback import urllib3 from PyQt5.QtCore import QCoreApplication, Qt @@ -25,6 +26,13 @@ def main(tray: bool = False): os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = '1' logger.info("Auto screen scale factor activated") + try: + scale_factor = float(app_config['ui']['scale_factor']) + os.environ['QT_SCALE_FACTOR'] = str(scale_factor) + logger.info("Scale factor set to {}".format(scale_factor)) + except: + traceback.print_exc() + if bool(app_config['ui']['hdpi']): logger.info("HDPI settings activated") QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) diff --git a/bauh/view/core/config.py b/bauh/view/core/config.py index 2f843b9a..5ffa28b9 100644 --- a/bauh/view/core/config.py +++ b/bauh/view/core/config.py @@ -39,7 +39,8 @@ def read_config(update_file: bool = False) -> dict: }, 'style': None, 'hdpi': True, - "auto_scale": False + "auto_scale": False, + "scale_factor": 1.0 }, 'download': { diff --git a/bauh/view/core/settings.py b/bauh/view/core/settings.py index e6e1b365..dddcfece 100644 --- a/bauh/view/core/settings.py +++ b/bauh/view/core/settings.py @@ -11,7 +11,7 @@ from bauh.api.abstract.controller import SoftwareManager from bauh.api.abstract.download import FileDownloader from bauh.api.abstract.view import ViewComponent, TabComponent, InputOption, TextComponent, MultipleSelectComponent, \ PanelComponent, FormComponent, TabGroupComponent, SingleSelectComponent, SelectViewType, TextInputComponent, \ - FileChooserComponent + FileChooserComponent, RangeInputComponent from bauh.view.core import config, timeshift from bauh.view.core.config import read_config from bauh.view.core.downloader import AdaptableFileDownloader @@ -191,6 +191,19 @@ class GenericSettingsManager: max_width=default_width, id_='auto_scale') + try: + scale = float(core_config['ui']['scale_factor']) + + if scale < 1.0: + scale = 1.0 + except: + scale = 1.0 + + select_scale = RangeInputComponent(id_="scalef", label=self.i18n['core.config.ui.scale_factor'] + ' (%)', + tooltip=self.i18n['core.config.ui.scale_factor.tip'], + min_value=100, max_value=400, step_value=5, value=scale * 100, + max_width=default_width) + cur_style = QApplication.instance().style().objectName().lower() if not core_config['ui']['style'] else core_config['ui']['style'] style_opts = [InputOption(label=self.i18n['core.config.ui.style.default'].capitalize(), value=None)] style_opts.extend([InputOption(label=s.capitalize(), value=s.lower()) for s in QStyleFactory.keys()]) @@ -226,7 +239,7 @@ class GenericSettingsManager: max_width=default_width, value=core_config['download']['icons']) - sub_comps = [FormComponent([select_hdpi, select_ascale, select_dicons, select_style, input_maxd], spaces=False)] + sub_comps = [FormComponent([select_hdpi, select_ascale, select_scale, select_dicons, select_style, input_maxd], spaces=False)] return TabComponent(self.i18n['core.config.tab.ui'].capitalize(), PanelComponent(sub_comps), None, 'core.ui') def _gen_general_settings(self, core_config: dict, screen_width: int, screen_height: int) -> TabComponent: @@ -388,6 +401,7 @@ class GenericSettingsManager: self.logger.info("Deleting environment variable QT_AUTO_SCREEN_SCALE_FACTOR") del os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] + core_config['ui']['scale_factor'] = ui_form.get_component('scalef').value / 100 core_config['ui']['table']['max_displayed'] = ui_form.get_component('table_max').get_int_value() style = ui_form.get_component('style').get_selected() diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 41e54a2e..edfbf984 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -9,11 +9,11 @@ from PyQt5.QtCore import Qt, QSize from PyQt5.QtGui import QIcon, QPixmap, QIntValidator, QCursor from PyQt5.QtWidgets import QRadioButton, QGroupBox, QCheckBox, QComboBox, QGridLayout, QWidget, \ QLabel, QSizePolicy, QLineEdit, QToolButton, QHBoxLayout, QFormLayout, QFileDialog, QTabWidget, QVBoxLayout, \ - QSlider, QScrollArea, QFrame, QAction + QSlider, QScrollArea, QFrame, QAction, QSpinBox from bauh.api.abstract.view import SingleSelectComponent, InputOption, MultipleSelectComponent, SelectViewType, \ TextInputComponent, FormComponent, FileChooserComponent, ViewComponent, TabGroupComponent, PanelComponent, \ - TwoStateButtonComponent, TextComponent, SpacerComponent + TwoStateButtonComponent, TextComponent, SpacerComponent, RangeInputComponent from bauh.view.qt import css from bauh.view.qt.colors import RED from bauh.view.util import resource @@ -699,6 +699,9 @@ class FormQt(QGroupBox): label = self._new_label(c) field = FormComboBoxQt(c) if c.type == SelectViewType.COMBO else FormRadioSelectQt(c) self.layout().addRow(label, self._wrap(field, c)) + elif isinstance(c, RangeInputComponent): + label = self._new_label(c) + self.layout().addRow(label, self._wrap(self._new_range_input(c), c)) elif isinstance(c, FileChooserComponent): label, field = self._new_file_chooser(c) self.layout().addRow(label, field) @@ -712,6 +715,8 @@ class FormQt(QGroupBox): self.layout().addRow(label, FormMultipleSelectQt(c)) elif isinstance(c, TextComponent): self.layout().addRow(self._new_label(c), QWidget()) + elif isinstance(c, RangeInputComponent): + self.layout() else: raise Exception('Unsupported component type {}'.format(c.__class__.__name__)) @@ -792,6 +797,22 @@ class FormQt(QGroupBox): return label, self._wrap(line_edit, c) + def _new_range_input(self, model: RangeInputComponent) -> QSpinBox: + spinner = QSpinBox() + spinner.setMinimum(model.min) + spinner.setMaximum(model.max) + spinner.setSingleStep(model.step) + spinner.setValue(model.value if model.value is not None else model.min) + + if model.tooltip: + spinner.setToolTip(model.tooltip) + + def _update_value(): + model.value = spinner.value() + + spinner.valueChanged.connect(_update_value) + return spinner + def _wrap(self, comp: QWidget, model: ViewComponent) -> QWidget: field_container = QWidget() field_container.setLayout(QHBoxLayout()) @@ -904,6 +925,8 @@ def to_widget(comp: ViewComponent, i18n: I18n, parent: QWidget = None) -> QWidge return MultipleSelectQt(comp, None) elif isinstance(comp, TextInputComponent): return TextInputQt(comp) + elif isinstance(comp, RangeInputComponent): + return RangeInputQt(comp) elif isinstance(comp, FormComponent): return FormQt(comp, i18n) elif isinstance(comp, TabGroupComponent): @@ -924,3 +947,32 @@ def to_widget(comp: ViewComponent, i18n: I18n, parent: QWidget = None) -> QWidge return new_spacer() else: raise Exception("Cannot render instances of " + comp.__class__.__name__) + + +class RangeInputQt(QGroupBox): + + def __init__(self, model: RangeInputComponent): + super(RangeInputQt, self).__init__() + self.model = model + self.setLayout(QGridLayout()) + self.setStyleSheet('QGridLayout {margin-left: 0} QLabel { font-weight: bold}') + self.layout().addWidget(QLabel(model.label.capitalize() + ' :' if model.label else ''), 0, 0) + + if self.model.max_width > 0: + self.setMaximumWidth(self.model.max_width) + + self.spinner = QSpinBox() + self.spinner.setMinimum(model.min) + self.spinner.setMaximum(model.max) + self.spinner.setSingleStep(model.step) + self.spinner.setValue(model.value if model.value is not None else model.min) + + if model.tooltip: + self.spinner.setToolTip(model.tooltip) + + self.layout().addWidget(self.spinner, 0, 1) + + self.spinner.valueChanged.connect(self._update_value) + + def _update_value(self): + self.model.value = self.spinner.value() diff --git a/bauh/view/resources/locale/ca b/bauh/view/resources/locale/ca index 1990d2c6..5880b54a 100644 --- a/bauh/view/resources/locale/ca +++ b/bauh/view/resources/locale/ca @@ -231,6 +231,8 @@ core.config.ui.hdpi=HDPI core.config.ui.hdpi.tip=If improvements related to HDPI resolutions should be activated core.config.ui.max_displayed=Applications displayed core.config.ui.max_displayed.tip=Maximum number of applications displayed on the table +core.config.ui.scale_factor=Scale +core.config.ui.scale_factor.tip=It defines the interface display scaling factor (Qt). Raise the value to raise the interface size. Restart required. core.config.ui.style.default=Default core.config.ui.tray.default_icon=Default icon core.config.ui.tray.default_icon.tip=The default icon for {app} displayed on the tray diff --git a/bauh/view/resources/locale/de b/bauh/view/resources/locale/de index 552ca506..5a183b21 100644 --- a/bauh/view/resources/locale/de +++ b/bauh/view/resources/locale/de @@ -230,6 +230,8 @@ core.config.ui.hdpi=HiDPI core.config.ui.hdpi.tip=Falls aktiviert werden Optimierungen für hochauflösende Displays vorgenommen core.config.ui.max_displayed=Anzahl der Anwendungen core.config.ui.max_displayed.tip=Maximale Anzahl von Anwendungen, die in der Tabelle angezeigt werden +core.config.ui.scale_factor=Scale +core.config.ui.scale_factor.tip=It defines the interface display scaling factor (Qt). Raise the value to raise the interface size. Restart required. core.config.ui.style.default=Default core.config.ui.tray.default_icon=Anwendungs-Icon core.config.ui.tray.default_icon.tip=The default icon for {app} displayed on the tray diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en index b16ee37a..4eb3355f 100644 --- a/bauh/view/resources/locale/en +++ b/bauh/view/resources/locale/en @@ -230,6 +230,8 @@ core.config.ui.hdpi.tip=If improvements related to HDPI resolutions should be ac core.config.ui.hdpi=HDPI core.config.ui.max_displayed.tip=Maximum number of applications displayed on the table core.config.ui.max_displayed=Applications displayed +core.config.ui.scale_factor=Scale +core.config.ui.scale_factor.tip=It defines the interface display scaling factor (Qt). Raise the value to raise the interface size. Restart required. core.config.ui.style.default=Default core.config.ui.tray.default_icon.tip=The default icon for {app} displayed on the tray core.config.ui.tray.default_icon=Default icon diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es index cc530de3..7b311f67 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -231,6 +231,8 @@ core.config.ui.hdpi=HDPI core.config.ui.hdpi.tip=Si se deben activar las mejoras relacionadas con las resoluciones HDPI core.config.ui.max_displayed=Aplicaciones mostradas core.config.ui.max_displayed.tip=Número máximo de aplicaciones que se muestran en la tabla +core.config.ui.scale_factor=Escala +core.config.ui.scale_factor.tip=Define el factor de escala de visualización de la interfaz (Qt). Aumente el valor para aumentar el tamaño de la interfaz. Reinicio requerido. core.config.ui.style.default=Predeterminado core.config.ui.tray.default_icon=Ícono predeterminado core.config.ui.tray.default_icon.tip=El icono predeterminado que se muestra en la bandeja diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it index 07c378f3..6e51c78a 100644 --- a/bauh/view/resources/locale/it +++ b/bauh/view/resources/locale/it @@ -231,6 +231,8 @@ core.config.ui.hdpi=HDPI core.config.ui.hdpi.tip=If improvements related to HDPI resolutions should be activated core.config.ui.max_displayed=Applications displayed core.config.ui.max_displayed.tip=Maximum number of applications displayed on the table +core.config.ui.scale_factor=Scale +core.config.ui.scale_factor.tip=It defines the interface display scaling factor (Qt). Raise the value to raise the interface size. Restart required. core.config.ui.style.default=Default core.config.ui.tray.default_icon=Default icon core.config.ui.tray.default_icon.tip=The default icon for {app} displayed on the tray diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt index 9d7894c4..dced5379 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -230,6 +230,8 @@ core.config.ui.hdpi.tip=Se melhorias para resoluções HDPI devem ser ativadas core.config.ui.hdpi=HDPI core.config.ui.max_displayed.tip=Número máximo de aplicativos exibidos na tabela core.config.ui.max_displayed=Aplicativos exibidos +core.config.ui.scale_factor=Escala +core.config.ui.scale_factor.tip=Define o fator de escala de exibição da interface (Qt). Aumente o valor para aumentar o tamanho da interface. Reinicialização necessária. core.config.ui.style.default=Padrão core.config.ui.tray.default_icon.tip=O ícone padrão exibido na bandeja core.config.ui.tray.default_icon=Ícone padrão diff --git a/bauh/view/resources/locale/ru b/bauh/view/resources/locale/ru index 636fc8f0..176f0166 100644 --- a/bauh/view/resources/locale/ru +++ b/bauh/view/resources/locale/ru @@ -230,6 +230,8 @@ core.config.ui.hdpi=HDPI core.config.ui.hdpi.tip=Если улучшения, связанные с разрешением HDPI должны быть активированы core.config.ui.max_displayed=Отображаемые приложения core.config.ui.max_displayed.tip=Максимальное количество приложений, отображаемых в таблице +core.config.ui.scale_factor=Scale +core.config.ui.scale_factor.tip=It defines the interface display scaling factor (Qt). Raise the value to raise the interface size. Restart required. core.config.ui.style.default=Default core.config.ui.tray.default_icon=Значок по умолчанию core.config.ui.tray.default_icon.tip=Значок по умолчанию для {app}, отображаемый в трее diff --git a/bauh/view/resources/locale/tr b/bauh/view/resources/locale/tr index 3ddcd7f2..8e6e74a5 100644 --- a/bauh/view/resources/locale/tr +++ b/bauh/view/resources/locale/tr @@ -230,6 +230,8 @@ core.config.ui.hdpi.tip=HDPI çözünürlükleri ile ilgili iyileştirmeler etki core.config.ui.hdpi=HDPI core.config.ui.max_displayed.tip=Tabloda görüntülenen azami uygulama sayısı core.config.ui.max_displayed=Görüntülenen uygulamalar +core.config.ui.scale_factor=Scale +core.config.ui.scale_factor.tip=It defines the interface display scaling factor (Qt). Raise the value to raise the interface size. Restart required. core.config.ui.style.default=Default core.config.ui.tray.default_icon.tip=Tepside görüntülenen {app} için varsayılan simge core.config.ui.tray.default_icon=Varsayılan simge diff --git a/pictures/releases/0.9.6/scale.png b/pictures/releases/0.9.6/scale.png new file mode 100644 index 0000000000000000000000000000000000000000..ade3c67fd0dc2898e083abfad24aa7c8c6e1e6a7 GIT binary patch literal 16421 zcmdtJbzD_z_b<9Yx{+=aL8L=Mq@+PqM7l$eEyPlVciMM6AUJQNB=sH`NXjzXam!=E>B zu;9NtHu9fPD70W}Sy?q@Sy^T$7e`BLI|~$wJ=1KZkg&%#_p(=rMmh4`Pba#HM;;$Y6kZXX{e?*Foh$d6iRRbt}@v z7H6rMY453qOQ(IbkNQ&hrAN3}-rdJNYK(yM$l#a z)&g~@+NN1elx~Mw#PtZW$h?P*Qhz?TdPbG$<(KVC9O1Lb+j~Z=-wb=%iB5S%CLmfY zFQ-N3l~xQnneM~`&qHj6CMBL^15G}cUh_*F*Sd($v773I?vgD!s9(d}Z5-S0;v<>B z)FYfcm#q9fic8{wRa749pNXsL|7vE%DuRW%me9b!{iY{z^%UB|gWAVRvE*s1yzv1C zg2&nDd`|em+S4-=C!)S6m+zWOSfZjB3f*rSmbNz=H)qZzhByCUqaV6>88a*n!LneP z-BpxBT_FFYHGFyv-@$cK(tV0T@vtEOqIqOVJ%evzyDHz4$Nq^$O^i=cv~z%bi^f%6 z$5qzR-rmB&6(#FpVd`q}nAzRh)rwhB`JS3~AQ2S`#f(yxyQAS@^n2P}hkW^5cn@84DqW@u{XXD}`2(n^7Sgzf#9ZUWr&eyiNA<-``n4hV^fqzF%PEJGF z%M4SasiVD}Sx}H{b#>J&YgbB3ON&oXFepDiU*`tt!>p{2ADMZ1iD_tP%(76ZM~@y+ z)6!-*FLu5<_+@s&&#*79=4obT#{By0K=wVlQgsei4h}rj+|m-8y0Y9g9#&RNfn?Rc zlB~kQl*CMua&4i6tbclVjT;H1Py7m0J(QG`q@O(#{`~oKH~9~W&dyFgVd2n*2Hyr9 zqj@Dh(ukEky z#o;6mVROHmZ28efdFAzN`%qt;l1d-Be*GwWqm2C8qI}{==zQQ>bO=6G>*y$@u&{8w zdT>rk_2n}X+XKUvNZm`OKgKkY945!edn9SXjh`vbM=qFRa(`zuW7;Jk*08U1oQ-Uh?*DMxeQWQB@#0X{-D>}f#-DX1 zgHrw9z7e@^PLXovlP2&Q$11;Oi-;fbKz_F7>2iCwO+mw_%1xeizvAxi@AKWfNzPduH{-Dw<1+i;#*G_h zQ9qQPE`O6>A1#baP36^6p}-*^2rew--uV4{erYLmbGpv__e6zR)QB!-NQlnaB&&{2 zlId|0kClyW;5Yl}lCuF;a&iS0Cqz~mZSzcEi-%wP2PiVbQ$ z_nz?Z@u}I`%U3!~LnT{K?2(B3ol?WLf91uv|9djb_hi4}I3>)n>5NiYSy@R%<#|(+ z)KYIUi#XFwtCwR76+?ykRRd*D9;~dbnN3#Ohet%TY%g~4Kikl`X+NQ1x9zn&)8Oso z?A+pizQZJX`*vGvD>}TXyo-y#cN-zE{f!80V#c&bpYFqJ?wFZfMVU<3x>=1D&~R~c z57xOmPSv?TuqyW4Svp*NAq71IiFK)h1v*OH=a2*q4ee#mFDF;m_DBlO?tLFc^+g9CwVmo` zI6iRq#X0nbGdvSKgsu@~_I4?OPbdlZN-Ly}i zu1~3#A091qM3It{e|&f`qHR1s?LLdC_MNaCuFrLIie__j69)%pVCA!_vhp1@H4<@g z@qs)|&ePMK{?_`E5qFrZh6rVi99)NASK!GDyk8mN*b!3H>b>j6`1J&JxqMt>0C%8BjFgQBkr=N;vQo;f3Mv zL>w1dg0QWIKH^ex>s>1+B_TnPk&(4`bj-tsI-Cn4R@2c5H*E`TX=y?G`t_@*+Zs6~ zuTivY7*RygQ_smec!q|C^JkBaJ!NFj#avecV2h?3);(JpD`i2MjO1yqe9tATZ)lib zUJfI>ejgA50kzta@Ajy^<2-En_WaN8e~bu!N-`8T?9=-A z`j+~gd8K!;-@IAgdm|7NpVH*?a2pknsrVw}L8|1|_I6-#F>m9=xp%t5M7gz2sTrn1 zEPaG!-`jMz_0h3%>(_i2M?1?nxVU$sD7oi;{tQODgoPu*#_>l2pWOF>a+Sk0?4Qsp zvY~TdUtd4`bkk}8)!?=FJS;4XuTs&DiG+qG!td-j-Fi5CYk$8Jwrx+j_uiUjx!w3> zo~VPFFg?Wysf0W?>O$cD|TM(THL#9#wyhYixT|qSG>kdr4~E6$HBCFh_BD~>H>Vc z>96j1YB5*#w?ej|-#*-4Jec*TvM>JpMkVX#Fh$ny*(T!^p`rpRJYou(+t0Y}zZbpY zT181oc@?k?i()Lfjo;BPs*UM7D*v-RDjy#oKIa9wbnmAN?GZ=~L6MchG8E{PqIbPu z3RB5yRb=uFPW3y+t#X|0gbG87xzJzM&HL|9+^Z}sAs=)+@25(}AnUQS)K~3$A`DBl z4UhtV2a3n}S2sm)SlD8})CE9@^p{%0!^8YuyH=_n#3%*_2a|5Q^K{10g_W1zG(F0F z^vUeo`&(-42j)Rf8N-GexJ>$BrMSpZb& z`T0?ez9$P%)~Lu%OF4OY)1$w?N=ixsYHDu1|L}p7jxGv{*RsB)T1|v{}MkxKkS1Rm|)uv9_DI@dhc@q z?i(o77eVDXF}-~C>UkWC;#EFA5?G?cvxC{e3R~?MVG2AksEzE*UiW zF(B9NG>^WpxLE2mFH_o)7ZR6_HnX`I`W+N*Vvzf`T2M!tQoQh!h!# zTYk>*}d9Yi`DVlPSaiP5M&ouWxGctYE z&Wk1%7MQ-izP%|Dba30@F)# z94RTOn>OEZ#Kgq-`1$WFfBOIhiZ3WASZ@1+XnT7*M;Xv^a4OwRD^!J^p@Y4;`sL9g zL;TyfZ;y?SrvtLG`koV~P`kgi#qREjQo4V?6Pw8=;Qj}(bn~uQoqCVbEd`Imtt-8~ zy(X5H*`=!6*e??k6SwO8PkvX??r{6}r%HAGF!m>W^5h9}(I~(K*k69RpyZ3=7cNek z121i_46+#1xL`^TrfK;5`={?))WJT7B_FJEG)Ar*X5)7pL4Ydl&@C_kOi)u(n*a`K z6^Hp*zdvEy`b`9CoFg7j!h0VB1p_h@#t$k0=1lX%gx=!b40M(GNo?3ncyQlWL_aZ!eL{ya02<*l%NB=o0+ zDBi|^vNA!GMm4dh%TF8w0|OFratv8n*|EtA5ze&!0>}y&YGvKwWqN3EMrl!=wcGSCjdm%@x z?vE7|6!?UMI$#qAJLp5bm^K1LfX;B`^;f5@xjO}V6?jnB3SmSHTfY*GTLJ9z>TlB_ zY%)7rJ`sc4pqk|AQf~|Nr??Lv2-8-A*0<){XcY_KeLbL)N5{mVNiY84J~Dm#{yjm6 zOdzZb&JHZ35Ei+x?I;1OAuNBscCi9rP*+!1r;Bsn!I?Pid-?}VX=g;1P|Zog_Oz5- zI%<|i+hf?J`s1-oQjAcV9T8;LO{}cKQ~l3%MfJ)nD?5ijJ%~$A#)aWvN~5o@zp}b| zg@>o8lc<)`hiX zvoK2ean;ABO8FB3vF&|-iwt(KlCtt3&KuxX10y38DcD$8X0V~Cg>1;5ZB9i1dci#j zfg&{NN#Op{+Zzn)Gc+`GGxnv*AAFU@CiO>;g!~*!BWKE9FiLQgMK?5@;S>LjU)$-I z>iVAhC|xTbC@`P((6z@F7W2>>lid&R>^$(lbyDIqA<^x??9 z02Wc?_BpT>=ey%Mt){B^v+sY1)V{-$*fu=;XyqPSBznk)4<9H8lrv}dEd*q@u#i7vl9!(@TiV38=Jynhlb1ea(}A%Sc&P%c-eI#2C-m3hv|0{ zc_!W##Sst?n3$P8hcZHE_X>PLCqg83aVmKA>Qw|+zJE_|Q0;W*BkMgNcvgUs2?zgs;^`n3fbJ*K<6JJKEnEqkRE6tM8{@tyz7NDO91YUlpE z@PoM_>aoQQZ1SEw#MqbGG+Gr_tvYaDkvZL0Gt#+@e4dzz19oFgFY+EMJZTGzO33-5c;jCZ?u= zu(q}Y@AH+R2O#=ajX^9-3!u%i=8f;((E)MU?w~TZqOWPP>sa=O5_Fs2eF3{;s}U86 z<>+^Dc7VK>1_uSmD@B8$fFzH1SA#%YY3=Bc7rhtv_AN5n=F3Mw0AMxj6oV zhk`{!uamuGAxcUU)bPn6L9mL1Zr?Ceukegc%xy34y!3iyAvZTSGWI`aXWRJ5t^ghk zzVs0$%vCWlTKFHOJuy38y;*}y2Zz449W)^`)?Z$$-meEU70t&#Tj=$VV_;y+eQ=+- z1A-(xy3%LA_0qk)0e>fnI5JLD+DAdXiHeDteSO7R^7(UXqOo7ZWR+tZ6!-o6_o0fc zlPirAiVW+_eiRuZTn;FCP(c9~H62~1Zkc7Pbf&{}ErQPxy$K3#u*6h$cXjxa`lYy^ zF1Ma-j0Fx1sQH{9Eh7jIL?>apu{1ZQG+30SZy%!O+rru=CTMwhc&K40CnOk4LN}Qw zR99bRNrGNr23)9h2k1z`sst^(U~~^~$f#F;0MwDS0psb?JI4jU&a^_T-i}Z^FHYs2a5Oa6CundwjJ$l}HexfFA>TUo z0d^YF&Y*1uG&cH$oS*T2Ur0lqgYZF1uFvA(H}p;fOms7M=0)pP7kHgxt!`Bt37`qgSC)p5k`8kb>q3B z>*ehAl@*iGx({k=2y7U=RJqZt_)pyuS;G^F1JB?S@9 zV6Hr(zK$X{bg4G$i1aUrGSt?l0=2tvf3J|!)y>WM?@#6Woo~0zvNU2rp-5MJ!FaK< zZVQ-Ttk#W_h*6wfE01FK`bd6}T|ekM_2;#7H7H`&-w#ZbTdBSutC<-j1?4AT_eA7Y0rx5l!GJM88@c!>FrUt``cc>`?xq%#(>sk96X(YZCm+3> zy%Rd@)jc)G6?i}UvhIlko7EcI?ODgQtFt2_yDa&q$U zcoI_5huqnFoHbc0N=gBGg$m#;eYdI5xdoz213K{6;J7$a)qD3$ZEQHSjI2R+1%)CM z^n1V$ci3LH3=WcddGUU~1~1oq4Up>blP4@JESI26ttQHE0DI}6B-@io-39KwwYNvX z2~tOfzclP@q+_E3)N|Wml4^3NPHGYn!)ylnefgi)&^Ch-Ey5KXMD zacc~S0GY+H+I~8&Y?O1OXp_D5S>tHTM}%SdQQ6kD2ZT>ANw`|sMf3BxP_LJ@g&L{C zfM{)NoB#cr5ICrgt}Yr1_|D7ZM=255i>@^oHGS+ z)huhQ!CR!#Zu~i5FfAQ-5kaabhifd`<4;W|+UP0v+c}+YNYJHLS)o&rZT`%$adB~R z0==*PXz3kr)sdv-l@%Nmv}{EW4^bHT_?IqSvhVv&`_D%O(LTkI0N@P7HLrJw@a)ri6E_FYW^ln^2|vu`wMFZ*N7Sz20xSR`j)@V+t| zg+j(FxC1YflGs)82?;xXetjL1dF=nHa*z9g#b{L$@LnoopDS7la=glg_{6im2Mi$1 z&TVW&gLx(mOP*BawysrwFynpr^Yw$dg#~n2O$0>%*dt#$0LWOMjfVQfXn`l?{Z>Tg zu?p6Vm=jIsv6n}~_U!HES8j6g@d*i7K)UBW!T*4XIk&k<1gOWL-s2&Qbkf_mHoHo1 zmgeUJ0k9+X!aq71B8+xM)1U%C70v_|9XeF!TuTrs6B99tot?e;^k@fA1%7pH?E)Z} z=48Pq-DeV-nqIyg;f0Q+fYVGtp#aO!wMIz0$lwlGeF(6ID+l1KIz~k@7?rCzKM(Jy z11c>7+Tc+lAQm*SfgD_XeA$!z--s3qBYOT*s$VPUwLR9Q1s|Vyw)QVGO18YZT^+-X zLNRP3S%4mQ{?-QE-N$0p?~mO@R)a3XJ!fff2w~x;Ncl5<`}VEX2YLl?JgsWlmhNtq z!y56jC;gp(`hka#T)zCgU-E>2jg1YlH4thB02{22bQsb}e5UBAxv83`IB-cItu8Dq zl)!3$RiSa{otN?RSWCfTl#0Dp#T)=B;=H~5@B#c&*kJsRzmP-&&xP(M>NJO{w4V&E zu>E0U9TWaB@8P^DfZwgHtpSgzYG>J`CXkUau&`Re0Rk0+TGB@p1pe&7P|zo#aH~!X@G1uQhFk{K zCB3KM7$7bzu;?&VwxFmee96PPOLpU*u&L*#+BA5@U7t$A6E1YefExS3=QAxUxHyP1mUs0~_YIZ@>&*48&%dwPDkxUPN2 zg<>IFnr-l=g7pU4gNb66kT5)~xe6dw>A{1p-ekd$N1ia1$Euw(N!sgNSJ9!}0|TnA zshJ0Hwc6)M0I)DWSdTw{Y5F_recdaJ?k;jH1y%MR^EEm;Sattklk3Kf+}=iHa)Hv2 z^^Q4%Bj@3K3ruXF)wIMC4Nb*t6_u3=E-s%t1;Fa1?Pn7ANZUSz0bf#4A#dSUH7G8% z{&uw{U~A}$HRo*fT+?2D^lX(&}s~d3U3fXqeY?` zSy@?peIS|ug(aN;YX>onp%n#WY+r6V{fiG1-D$oR6E;Xe{Zf9x?r~)9#xdnnCnsqu zEB5C>K|-B^V7S^6T*VAHJoEsi{Efr&@VqpI5Nv*+!NN#|AG|xpdb?{LULtTUkT9&*c#3T6 z(L#M-wYhxuaBJW}$~}JkQNc-#BR)Jfw&?FPm0}ES2w;Rc&;+0n$vHUi@EF!&pa3?> z+1YUgWH@8Nz5~T3H>ZYUu(I)2?%HuIYzO!SJSB#&(Es8*{Kbnd5Yk_!roQk#*aXMN zuNf2y4y%Z8j{MP(up5uEO}6G*oZQ?xz?S0&mK*aHF}>DC@(~*xnO30K>Zp4RB!1np z#@82pBg(z48qa&WG4KUd=4+SEo3HsU)`kQmQ}Cf|ebV%FbgW!|aK@{^#7_f#eBy1+ z8)W=IBRkw`Bc7j|LxI?YP$KEUfL7qMZGC-&+kv=M zNC|WT5G4x?>nMRww$3SsVrQ(SBfKPN4c`6=Gjp2ybTqKopyJ|UopH_z=p1$v<+z~K zUgo5T&*ErasoH5+Q>+l?;c@jZSX1vP%o#$WUUyO<|+=fgV1U>Ek{r?P>`u`2uBTUTd?pmr_6-|8`a^2G=iEbrb*4#YWIj&77 zl|ivW^M3{HiG_wGGD}J_Ha0ebYZ#JZG$OJ+-D|*A8yDUFq(8)(_^HMiS#f*TuAR?J z2Cji>by=bP{~E+|UpXFslG-vkNq2N~WR^t>Sutl|x}~4>0LBFr-^IX|NG2*E0EZ)@ z|L;~`%;Ow9yl&g)HaS~cP7ukc>BY8IMsU>5P8-;NJ%wlipxl^)M@&2HFxyB7(#5rF z*WQ$U&(E^={Vy0;ym5@)3=Y=(uU{e2(F8H8h->;$`E|E$WO&{q7+5tmH8976fa}GA z#IIFk3ULWH4)Kk<9?wrM@QD&akH^j}@7n!6Xi1onEIBm}uRhqgwa+S^n?sXliYSi1 ze;b-QvBPy?!QKRj5U*bbGOpcN30g!%L~A(+Pax^)isoHhx7MEpL+9}52!VwF2dJ{O zEq$YMM{ImRN;I>O+kN#tXR8E}cpyZ8*vH3v>q1L0)cojh(L+g=j*c%t6kq}4-q`Pm z_1W)<5JV$^lnpB2%^Pyn_oAdQ*OBxP_NT-{-Ziz1JZyNUc93TfGm!q0t^bFRz*+S0A9C|u*eryzwP?Pi1Bdfs-Oanv1?M^z!Ihgam z&QtO@8X5t?fO_@3uI@J2e$L?8#-OfLHg-U!BV@YHeG%^E*C^skg3lfXCOx7v+Kmf< zLz1BXKrUW8v$YK#C=#|pv>xP2UcGwNPKkyhpb@};_x4!+;JyWFmDzl22r2+%*|w=E zdg#<`U{*yy?Wn#JF6{ja?x-TDUN8y6pn0BdcQLK}C}u(-0|SScxC<)M`C!w~2lQV+ zSOY(PbV2cv($L_7iN<#C?cAhe6Zfz%%4erf1o6pv45|ZQO&4K?9YRj4VwCc(SHksE zpUrtPt{NXImZ^8r53+-La{aN>uvyn~mL74$x0p98?S4ew!(o#Ip#87NXmfbn0y5JzRs6dWceJXMBCfj=fDge ztF)&B`#C>L=<2Jy&fqZpDu;RSA690Yq-cd~f&&6jFgzdHmAbHAy9NRUItY~z;0uQ9 zL{K$Me1QgHa6}NJ9RMycBAglno{c%bvpsFZ;(;NGyu7ZiuEYR#rm2+`4#bLf*GBLW zOGFq#bZxNiNM;SDp=uHz8o=QndisoISY96eJ!r4*&&cT&sT90lBLxQr0~?zK&@b3H z^1ylmGJY6BT|)Q^!nb^GYZ}0BuH3ps1G4FtFJF-AjEj%Qgz=a3?p@Sr_FLIPEf&Zk zA!EHoMp-eIJ`lm;#7soVs%*RB4*v}V;}XM)cESD)DLMnDYzA&J1=0J09DIQrH*mAF zvuO>pV`F21FiCrP)hZN1Mg>$o#A`-y2N(<>Eux{Ag@iQMYZhT1BgQwP@+0yuQqFG# zJ+`6BaEORPAs_))&u2Tz1rZ0ULT<}hrKjzl*(x!_4`zo};!vnM1^Q&8ci<~)<>@!- zczKC&adEYGlK?5$-F5x9L5d3a`SUTdMjq2{t3#OJ00W?NdioT(DyO5r7Kg{jNRRo_ zlYeeh@M@hiv1c&%(e;>NZxET~VS9wQ6#yjFuW?~T&?aavUmpAa`376d1YrL|(0Ril zA~ST4DS(fmpih^m z-s6WgFVL?d!V5u%-@v*K*NdCF3$X(>h2JiBd{CubsN+c)AkEEudyglPMX>Deq6TOH z%!xf6-C$DVn46o!qr)4904twGH~GBVCpC5?3Bt?Ei;`btMS@d+d*(os)~U49_3YEu zUEMQm@{<6u;QFZqgP7}pXuR6^9{1ARTmXy#s6JbI)qlhF$e9a(z5k*cr1Pfm91(c{ ztS*x$Pp*qA#L%)5ymxhX2d$*q$@K!FAvWcv78W6}yTEiyPfL@g#-OC7jhvb?fTv{& z9Sb60R>|vM!2FJdHTb!_+y)O7fGWxJufV9c|M*|f{o7jt%QhgRC8N{s??P@@2b zYBruM6 zjBpMhomZ}0L1XgSLPMoN78BVnpUcW%#$EXy3*!^|3lh(iJo-t7YKFQ2|2xFwYGO|X zkqsAYLntU1DD$hU5e&DVnSddQisbRO9skSCA`O z(`KL-D1ehW0I3%UlVk#4fhOuxvxiFKeLX)+S?R>?thbQ7_9ulZOaUwDw>iGAZpYsxmQtAG9W6k{cq+m9OqI)4=E9yx@X+rLItg&Z3^BX8U2zL}kN%*L;1-xxfUycm~J5H`)K@%Ir!6g@zpO z6*RBXQb2EV8n`whA~_5U9o^yQ&b*Y2X6B}Rt~ok1Acc2+`v7@;&aHAxL|^_ZBSFlX z3xRdNe64?e@cHIQ{=muKyQKmdv{@72Xyj{s`8{KZ&?^-_Iibz)2qV&z%KsPgzK6MB zh7CCxSx|4UDiFu0cy$J78<=1~1FMgWzdT}t6w5U>HY`Z40RaFb?CyI}XLQ+60#eRl z5(eUe$Bnv|jctfvA`gI=wuA*MM>|iTGN-F@8S-3sA`ugTW zAC(U`%KAai9R2$i8}%P7=&+`aDtgy7z4y_4n_$(psCI~Ve~P(rD;a_F=YkUMydy-g zeIR*+0ds{zkoVd(3>di}Fd^A>$UJVr2s48jjVN~jAh&vWo0!3PhG4gxv@~jSwh5Fr z+U?Oos>rqgne>3v)YMVh5E(QGT^8ld_Z#Flqj`+lR) z4WjWf&?4tIHV9B4WrczO2jsySJU3J`M3tk#ff_s>#LA;{@7v@nrFowcC=(~z=QmjFpr4`8^ zBPkY0CP4b4w)YefrP|vu{LhcBBe)2%1(0-}xB%hV>Fn6epw<-^38W)pIag{zLMULz z#UNtIW|*po%*}7!30Sj2^*88ggK#uuzNV;%2f`u{!omT+6mTMfp!(hl$wKw>o3>(r zp>_!cIC5@%Jqp4N3D#8~|4#+}KUDX(S9-nGGGc9tfeqL;OqzfefkZ^1zD?i0ctH%3 zXh6mzOV(wI{w7F|hKHXk3tbfloVPJqMGO(LPB>VGgv;kIi9%a~73%<15yAP;wBWD| zx%42ZAzaaaMPnUjjnfWe^;Xo9gQ7CN?&Dme=vfr)B-11t4J! z;4{vrht4rAmL0ENzYbATJ4JH20s`lf-Mk4P6eIbdqwA?Y&!G&M;n35c#uFaM*l)w` zN4f?afm@lX5ro4d9N*t1CtKTBT{}_0qU{&dwDIvcPEdG(ff|d?%dG7H>l7@6FTJNH zyBaXY;h+qJ>8>R92rW_cI|A%m&0b->jE@HsifJTiOHCER5iAPP(r^Rn7$Kzilw1KtMMZoj_n`32pFmn1jg0KI!0&|b(+W=;Un|S{{OBOdDkkDJRCJ3{ zWs@zl>_hU7rW2r8&q46-U$#dKJo$$DdKD3Q-Z&jK6OXjx9EIR8aG~Ez`qIMO0gVGO zW_XP1!x8yodYZw=$mkHFz*}&HTDU@oe#H6k&_@2Ub_Ksz6?$@;>D{VpnSgKhY{Ht zgpfjjHYVyeehiFVa&q`C?cpcFf{hkdl<||N>mM!sCT|_z$PG{%Ac)NmgJ>Jv5H8)% z80MYPAVkoMk{~Pv&f}!1=ax83i+%j~F%8t1tA*-r97IS*1q*EW7!qKxSj7mRijNQB zZ9GJW4HF6}=kvW{e^by;Rz~uvP)Mc~Nh|!~PIbaZsd)8+dR=xy!oCE(O9YKO*a0*!o#qk#atiyzd)n!RmpvbzC+ zIYomHIpSqhB_W&%hcUX}KN(p9+T|ge8{9IGjtZ#%a64|Y!Mg*01Rd%DIeG)GcSvAz ze1+GZ&3iF7yr#4LcRDvv%y%^d`rL<|u5jab8wha`>`CBS zD3EyMOcCuc0N@Z#vvN#qXpGQmT4raN;!Qo)G(JqeQ11ghP6)ViWPA%W<2v_0qsMwn zA1qbkdVV2K4jw7DSJ|gmfMm9YA4nl8qgfWZxi3xj&8kj-%Vt?}yTmtqX*ohOQ9kBdQ#dEESEm}j28ojl8`wZ>S~7AP>)Zc=v;Q}7 z33YpI{S~7IJlRq(vWC*U_wsabAI3bgpT7fVnDdMI;k$XI9uv=z{EBD#H}r~kbR|ze z{R5ewzZId$)LZCi4a#Ks9vh3`WI2tne