././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1640626112.175523 guidata-2.0.2/0000777000000000000000000000000000000000000010024 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640620179.0 guidata-2.0.2/CHANGELOG.md0000666000000000000000000003442400000000000011644 0ustar00# guidata Releases # ### Version 2.0.2 ### Bug fixes: * Fixed PySide6 compatibility issues * Fixed remaining Python 3 compatibility issues ### Version 2.0.1 ### Bug fixes: * Fixed Python 3 compatibility issues ### Version 2.0.0 ### Changes: * Removed support for Python 2.7 and PyQt4 (guidata supports Python >=3.6 and PyQt5, PySide2, PyQt6, PySide6 through QtPy 2) * Added support for dark theme mode on Windows (including windows title bar background), MacOS and GNU/Linux. * Added embbeded Qt-based Python console widget * Dataset edit layout: now disabling/enabling "Apply" button depending on widget value changes * Code editor: widget minimum size area may now be set using rows and columns size * Test launcher: redesigned, added support for dark mode ### Version 1.8.0 ### Changes: * Added generic widgets: array, dictionnary, text and code editors. * Removed `spyderlib`/`spyder` dependency. * Added setter method on DataItem object for "help" text (fixed part of the tooltip). ### Version 1.7.9 ### Changes: * Added PySide2 support: guidata is now compatible with Python 2.7, Python 3.4+, PyQt4, PyQt5 and PySide2! ### Version 1.7.8 ### Changes: * Added PyQt4/PyQt5/PySide automatic switch depending on installed libraries * Moved documentation to https://docs.readthedocs.io/ ### Version 1.7.7 ### Bug fixes: * Fixed Spyder v4.0 compatibility issues. ### Version 1.7.6 ### Bug fixes: * Fixed Spyder v3.0 compatibility issues. ### Version 1.7.5 ### Bug fixes: * `FilesOpenItem.check_value` : if value is None, return False (avoids "None Type object is not iterable" error) ### Version 1.7.4 ### Bug fixes: * Fixed compatibility issue with Python 3.5.1rc1 (Issue #32: RecursionError in `userconfig.UserConfig.get`) * `HDF5Reader.read_object_list`: fixed division by zero (when count was 1) * `hdf5io`: fixed Python3 compatibility issue with unicode_hdf type converter ### Version 1.7.3 ### Features: * Added CHM documentation to wheel package * hdf5io: added support for a progress bar callback in "read_object_list" (this allows implementing a progress dialog widget showing the progress when reading an object list in an HDF5 file) Bug fixes: * Python 3 compatibility: fixed `hdf5io.HDF5Writer.write_object_list` method * data items: * StringItem: when `notempty` parameter was set to True, item value was not checked at startup (expected orange background) * disthelpers: * Supporting recent versions of SciPy, h5py and IPython * Fixed compatibility issue (workaround) with IPython on Python 2.7 (that is the "collection.sys cx_Freeze error") ### Version 1.7.2 ### Bug fixes: * Fixed compatibility issues with old versions of Spyder ( See [documentation](https://guidata.readthedocs.io/en/latest/) for more details on the library and [changelog](CHANGELOG.md) for recent history of changes. Copyright © 2009-2021 CEA, Pierre Raybaut, licensed under the terms of the CECILL License (see ``Licence_CeCILL_V2-en.txt``). ## Overview Based on the Qt library, ``guidata`` is a Python library generating graphical user interfaces for easy dataset editing and display. It also provides helpers and application development tools for Qt (PyQt5, PySide2, PyQt6, PySide6). Generate GUIs to edit and display all kind of objects: - integers, floats, strings ; - ndarrays (NumPy's n-dimensional arrays) ; - etc. Application development tools: - configuration management - internationalization (``gettext``) - deployment tools - HDF5 I/O helpers - misc. utils ## Dependencies ### Requirements - Python >= 3.6 - [PyQt5](https://pypi.python.org/pypi/PyQt5) >=5.5 or [PySide2](https://pypi.python.org/pypi/PySide2) >=5.11 - [QtPy](https://pypi.org/project/QtPy/) >= 1.3 ### Optional Python modules - [h5py](https://pypi.python.org/pypi/h5py) (HDF5 files I/O) - [cx_Freeze](https://pypi.python.org/pypi/cx_Freeze) or [py2exe](https://pypi.python.org/pypi/py2exe) (application deployment on Windows platforms) ### Other optional modules gettext (text translation support) ### Recommended modules [guiqwt](https://pypi.python.org/pypi/guiqwt) >= 4.0 is a set of tools for curve and image plotting based on `guidata`. ## Installation ### From the source package: ```bash python setup.py install ``` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640626111.7849116 guidata-2.0.2/doc/0000777000000000000000000000000000000000000010571 5ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640626111.8005345 guidata-2.0.2/doc/_static/0000777000000000000000000000000000000000000012217 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443946233.0 guidata-2.0.2/doc/_static/favicon.ico0000666000000000000000000001731600000000000014350 0ustar00  6  hf( @  |||S///t&  ,'''u:" ...-444B???^WWWUUU###pTF;-999VVV@qqq됐UUUhH3  jjj=ggghG.aaajQQQT8rrrY9Y6c]HIP(e\Q?;;;>pbaZODQ"bb555z.̺jF}dEcbb`VK@9hhh5zKlAgCcbb^TI>@7v=q?kBtRG<3Gu=o@jBeD|cE|cE}dF[OP*¢|?t>n@iB~dE|cE|cE}eGa\Ğɾ|cE|cEgggg&r?lAgC}cE|cE%ũy@kAfDfIl }Ip@sMķGGGYYYĊޛ_(ũޮ^ŭзL?(0 gggl1  WWW+!!!P+++ 2224aaa\111p A-!... PPP*uuurഴvvv555oD%TTT{{{1WWWJ$iii=(((Z"5H%ĺTdFmbZp]]]|&b<|hEmbaUH<===SvClA]En\\\Y|Cq?rKottŽPw```Q¥norjiLuRRR8ˉ̸oRs8886[xfDuކL Usv񚚚cd湹@@@ QβYgμɻw Chz~~ykCAAA?AAAAAAAAAAAAAAAAAAA?AA(   IIII# GGGqqq^盛WWW{ ?YYY^nnn48Utn!bG^qbU@ffftx@uNQy̵͸z`gggMregCӵ̸ɸ쉉4ϖ͞04͓ϸ[{{{?A?AAAAAAAAAAAAAAA././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628151427.0 guidata-2.0.2/doc/basic_example.py0000666000000000000000000000120200000000000013732 0ustar00# -*- coding: utf-8 -*- # Note: the two following lines are not required # if a QApplication has already been created import guidata _app = guidata.qapplication() import guidata.dataset.datatypes as dt import guidata.dataset.dataitems as di class Processing(dt.DataSet): """Example""" a = di.FloatItem("Parameter #1", default=2.3) b = di.IntItem("Parameter #2", min=0, max=10, default=5) type = di.ChoiceItem("Processing algorithm", ("type 1", "type 2", "type 3")) param = Processing() param.edit() print(param) # Showing param contents param.b = 4 # Modifying item value param.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/doc/conf.py0000666000000000000000000001573000000000000012076 0ustar00# -*- coding: utf-8 -*- # # Spyder documentation build configuration file, created by # sphinx-quickstart on Fri Jul 10 16:32:25 2009. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.append(os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["sphinx.ext.autodoc"] try: import sphinx.ext.viewcode extensions.append("sphinx.ext.viewcode") except ImportError: print("WARNING: the Sphinx viewcode extension was not found", file=sys.stderr) # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8' # The master toctree document. master_doc = "index" # General information about the project. project = "guidata" import time this_year = time.strftime("%Y", time.localtime()) copyright = ( "2009-%s, CEA - Commissariat à l'Energie Atomique et aux Energies Alternatives" % this_year ) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. import guidata version = ".".join(guidata.__version__.split(".")[:2]) # The full version, including alpha/beta/rc tags. release = guidata.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. # unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. modindex_common_prefix = ["guidata."] autodoc_member_order = "bysource" # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = "classic" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. ## html_theme_options = {'sidebarbgcolor': '#227A2B', ## 'sidebarlinkcolor': '#98ff99'} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = "%s %s Manual" % (project, version) # A shorter title for the navigation bar. Default is the same as html_title. html_short_title = "%s Manual" % project # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = "images/guidata.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = "_static/favicon.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. html_use_modindex = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = "guidata" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). # latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). # latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ("index", "guidata.tex", "guidata Manual", "Pierre Raybaut", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # Additional stuff for the LaTeX preamble. # latex_preamble = '' # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_use_modindex = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/doc/development.rst0000666000000000000000000000100200000000000013636 0ustar00How to contribute ================= Coding guidelines ----------------- In general, we try to follow the standard Python coding guidelines, which cover all the important coding aspects (docstrings, comments, naming conventions, import statements, ...) as described here: * `Style Guide for Python Code `_ The easiest way to check that your code is following those guidelines is to run `pylint` (a note greater than 9/10 seems to be a reasonnable goal). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629181421.0 guidata-2.0.2/doc/examples.rst0000666000000000000000000000425400000000000013146 0ustar00Examples ======== Basic example ------------- Source code : :: import guidata _app = guidata.qapplication() # not required if a QApplication has already been created import guidata.dataset.datatypes as dt import guidata.dataset.dataitems as di class Processing(dt.DataSet): """Example""" a = di.FloatItem("Parameter #1", default=2.3) b = di.IntItem("Parameter #2", min=0, max=10, default=5) type = di.ChoiceItem("Processing algorithm", ("type 1", "type 2", "type 3")) param = Processing() param.edit() Output : .. image:: images/basic_example.png Assigning values to data items or using these values is very easy : :: param.a = 5.34 param.type = "type 3" print "a*b =", param.a*param.b Other examples -------------- A lot of examples are available in the `guidata` test module :: from guidata import tests tests.run() The two lines above execute the `guidata test launcher` : .. image:: images/screenshots/__init__.png All `guidata` items demo ~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: ../guidata/tests/all_items.py :start-after: SHOW .. image:: images/screenshots/all_items.png All (GUI-related) `guidata` features demo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: ../guidata/tests/all_features.py :start-after: SHOW .. image:: images/screenshots/all_features.png Embedding guidata objects in GUI layouts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: ../guidata/tests/editgroupbox.py :start-after: SHOW .. image:: images/screenshots/editgroupbox.png Data item groups and group selection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: ../guidata/tests/bool_selector.py :start-after: SHOW .. image:: images/screenshots/bool_selector.png Activable data sets ~~~~~~~~~~~~~~~~~~~ .. literalinclude:: ../guidata/tests/activable_dataset.py :start-after: SHOW .. image:: images/screenshots/activable_dataset.png Data set groups ~~~~~~~~~~~~~~~ .. literalinclude:: ../guidata/tests/datasetgroup.py :start-after: SHOW .. image:: images/screenshots/datasetgroup.png ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640626111.8005345 guidata-2.0.2/doc/images/0000777000000000000000000000000000000000000012036 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629144855.0 guidata-2.0.2/doc/images/basic_example.png0000666000000000000000000001422300000000000015342 0ustar00PNG  IHDRksRGBgAMA a pHYsodIDATx^}l[yG?bűƱ㦃6R ,ԡ.0$f+V!"R)dq [ M\V(i5d a՝ܭMԑ]Em{=$Dŏ܈rx{%yNMɟ\eBvM˗aMDd0P "C "r gi4lՂߒ$>s>t-]v+eLήBκxu 7Z[?csغ46m8++0yZo4bX!ٙ8禗?77#Q+8^go6F<;;8~zΝo,.`vv4^m7>tr7wl*{$ _?T}>5~%ٱ@mA׀<-(M^ܙXwKbzf%64M{3/6'?~IOxpl<s`RzW;gFGFa_}k٤ܧQFB|$l9zk3V4۟ŗޛ2 {}yI[f IM9$ 3 L*.}osًXBᆙXϬ8NYuMX][BT'ǁ߫3Rv0,b B͕Mt~ϟٽ&eqρ!^ǻ}Pct Qꅇ!np_P<$eo[d`X@P?5؀O|%|oo{'CzI/"۹wkܚ[mI>6É/OkHV<56`/9$? Bu.{\G~@6h# б*(6u8&_٦pc;fvݛY<۶$5i4bme8_]~_?*[?"?Kc?&~8Wbʨ3U=_h %AA0ltg66L^X vhZ6{OտƟmi~,;= ߕ&O $LcM{T[*=N˾} ujZu^z^9o^3^.B'[_߲azسďCgbqU6e#]Ga-ypPH}{E]T\`(8ņe}s;w+μW]—7 Z61? 2u'. !Kܧ{iIݿи]˾W\< =qeQ yls!t2I5㐼~ݩP?7|6̇ʨ>wE\ь7mK+4Bx\Rtl?f#>?Fj{pYI.54ߎ\Msߚ$ٜ(ٜ(C "r0P "C "r)jw}="r1P "C "r0P "C "r09ט _ǀɾя93j•̽/Տ/P "C"54dP "CEB{s3ö|ɤޏ-Uzntq.c-&Cm#Bl9գ{ >NNbRATW#W é:c%:;mHWF+Wt;-ދ0$!;GO_Bii5 =vlkݙ^Et=93{)~uO[B׮yن}RnInӻ?7>T'v]6`kQz'06[ѕUR(1bMЃ߶&iN-xԲNi݀[C)7uS;s!{as~yHU}ЍZ1mؿs\0԰Ç:V5p s4<[ ? 6템Z̰E 'K5Ӌ{q)hH vaݥS50.R29 = # S/J(lT'Lyą ahh+nTw 5e)Vh^E\ 2Ǣ!%A%R35t"ƞVmޖګZoI֘r~KRCBߦoIQ P ")NF8-/4Q9 D`(@DPcq{C[[5HC{Ty䑢C"r0P "C "r Lflm֗ Y+?qf*@=lBTʮIagQvY[s/|J> QEab$QB.WJ ˳LS_2Cf2ЙVpH %-SB!B3+0)f&./nm LVuu~>9eؾ9.y.QkMD\eD#ο/kEGB]Kek,z PqW#] G0۬ bFz?q<1aN_%K1(C^^yzmmh7szz(&S4`*WC/PG0#٭ lcОD rV먷 lQ>*Rg]%K1F^dK_;ޓ{[}[^##st.d-gB:c(^߅.#s d;5yu̲zӭ^HK-5~!j Q, D`(g^)3/U8Gc DT@D9 D`(@D9 D`(@D9 D`(|&?YIENDB`^+)|*!Bٌ= %D,.urg;4("2C@uuG_u4'BB@bj9!v+uife\󩻾&ꬂC u 5~/=f^?en?O eс RK{1*2ȝݾԭX[AH(Y"+-^*+CywϏ=#BlNGeysGӪ3ZD3SK,]0QB!7]j7M!#1Yj ʕ+TnW^, 7`H B<#@  x:D_~r>`!"OGO DA9E5 u "$!}l7gΜzkdrxTTT5 u 8(k֬鸾1 { $'BB&dќ<Gn޼'WG˟DHȄ5'S{%w #]pQrx9NCȽuIIr&}  Pޭ޵5;<_΄$, ~!n~{ kH3!՘a%?#EIwkRx: <΄`۵k׮%ߧ"B$ύ$ʍj{;Wk5|Gqe:\iߨKe΄߿qL.#EIH̦? 5 <^L޽{Ed#EF~}io?M/"cL5BO^VVdJ~͘1o 㻒88N1 GII r0.'S^'3gtă!m<8!$ЎrOr?@JSB9@749Foe By#D7^g2!F"3yn A+EG ##cٲe?pN"@#` 'T(S1VE__"IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443946233.0 guidata-2.0.2/doc/images/guidata.png0000666000000000000000000000655500000000000014175 0ustar00PNG  IHDR2"gAMA a pHYsodtEXtSoftwarePaint.NET v3.5.100r IDATx^ypTU]˚jfjfʩbtaRIbX&%$A00laͰddPӝ@f$@ MQD6!Iߜ@wSu}{w9wy{-E-$-$ߐlfC7B/T4.  @Hऐ$m"{Fe]".T$G-Ĉ[ٌ2MW!Y쩷g0`>)v H?p˓ڮTp+hsƣ6z8WGkp7`B[}\mXlt-ɓņCپ![s${LMFw˺~*\~z$h $Á$׀FpN&I!Iō/ 22mkhC4HKYkF޾wX;W3!P Ϫ'bKt ‘3Qsi \s#p&Mcn\ݟvzh'x+즧B!oC* {p}s%Yr}jk8;`6 X\>J1n$΍.N`ݢkh05PԋwG`̓\U;&la\A<ex&6`qg3skma  Р`\ȹNDf CU`s(yhUx|_F,Z9z%侍o;7?~9Iz]CU뢃/`}VkLVŶ9 {Y@`dQsx*p~n~FDs,md`s,ǶXy;qث^uH"F2 z_{yZ]82V`kb`/X+gyb9V|ڿ$ö.WQLLZyeNHvՅP(kh6YO: Z%X ,98yg`-8#:ʱX(ա,k-XM%Ih?'3cN [T汆{+br+1xG|(H;rQ+nJZRVg,hU[v^kzz:<>zקN^ _SiG`]>$F^\$DydB2$/?yewޥ`YL8RYd`UL=*Sх_hͲn4pWDۑH>n7\8fErM`M)Xux+@o^Μ[DEL*vߣ4r419k4h;v.#=/ %n_bvA]i*2p7.35xCxn7BCaDK(0ɟͽW]K1 "'yU݌0"ړ:yP ("lj@4Xl|5=N^NXl¥JڎK~XwNU/l,b!2Ӝ_W0:bp5$>B9xs5w \+ VTV -c$Ouf}|'KhDjj*sIzh45ՔEWsO7޶?黜}ՁfG"3N'Lx8HQO/Oty_: "zJ!m\In6]EYNX5K1EFGG#.."Ć`fߜA jW|j2H( (BV*m"HMW +`7S>y`"ӫ2wi[b뺥Ź P7{/ooea;ne :LXcƌܧO &F὞, F`NWΠj:W tDNQT\ > HjVut&#`1ONHB8Iꔫ]OtKU)Ph~`NGD, R-@Ϟ=2V+RRRP ɐ"} 5%ՆgQz-RD}ߢ ,zCڒCrENAg_/NJJ=--uF/@ޑ# m/hD:7X%[wCkn^L(I ';PM~vTMya0`r`y:Lo$#D`m' cjOMy=%1!db}n EaVNkIl++>s'5e`֧`پn`y*:XރV$l~E)w젫Pl"ޒ`ފ-|Y5SSF)um_w<ODW> Q`DP_n!U@6r,qG^Us5RXb9@oOZz̋to_=N>P$ևa^ P t`$/U24 }o(z5-PTVUJ6%Ɩ;XUܹ*lUklQ2I:قy+!#w8JSٗxfN%_uKyNi|]>;+T:9J}'am!% HMRIRS| H]|͖bKl79T[8 4ZNP0+.fތ-+?@a -@a -@Y=̣7IENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640626111.8161614 guidata-2.0.2/doc/images/screenshots/0000777000000000000000000000000000000000000014376 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629144897.0 guidata-2.0.2/doc/images/screenshots/__init__.png0000666000000000000000000017303400000000000016653 0ustar00PNG  IHDRJx[sRGBgAMA a pHYsodIDATx^} Z- p[1j3'6:qƷ zaP};8?x=#{{&@7Z0'; RUr%nIEFfFfeDfeW/љoƿ{a(?̓}@@(@ %1 ƦK=/ަm:Jܣ~G?z7>J`~YVx?u =uQ>cOfT{LAY[4Ǩ}ާ#zѧ ota`gI >OvbW'C=fw g^8H_gF.}#-l"&\NE҇=Aݔ>I(}eO-k?MG>Coinƫws]c0~/~31F=,$䴮m3? \==1O-H?koSN2 #?z>4CzG^nMtR:sL=?/[N\, P$ ҟw?Oӳ* L߿#%#jh?jG.wCj\w/]c8{L(E쌥gI؀xow&bDґs?.W:1?9BBzL"K5糜a`+~C3Kye`/^g8@0E LB|sB?QazCOᯞRa{Cz-%jdUAT!"~,Nw !$V"J߱C 9I9K}&PCGcyQ_7?Lo _ɃJ_zo|;0=Scb?/+9zܓ l˯+I"bv6PXx^0q8RO'AZ%VBұ!S\ ? zy-0@Y( }Ft>AoOƷH_}k?2Nq~c'ԽbC\y=v 7"L/*x&/4vD~/I',  ")[b%.bsVnH:v"OnŒp = GC=8] ZJڰB?z =-w[l ?)6գdrG;`@>d CoFz&`/wPJB b@(@ %1>J< KP(Q Wvu JyP{߅PJ1 PN,o~G_Dl ~kQ(ń'4O] ; NtzP]&;K2M{N{;^HLuzOi0(ONC`1D.~_#s XXmKҔ~7mJ[:_ێ<%3]CL('|_C" |:b_.i _4?`PM->Y*~%"~2L8)jG~D=Ջm,v!؅ D_Q$KZAUKuϝ@4I#KBV;l+EfRt!:dnh~#iL2,](!7?YsՋ\YO6Cܒ9s,d([&z,ރ"h]zvwi=m󒂸ːhzۋ_oMoq-^Xt8q}J](\~jޢ_lO,%OFYA{PAo[ŢT!o4$zh[$s{T+jOhKb *b _˓`8, Am)ʷ )C)}'^$];OPA bz.:t{ tqf|g%}w 7;{a_'t˃lһToJs_0bgrկ]Uu*,XHύttCJY{X,%~8:y/MW͌2W[$<{6]Do\ >tU{qݠvr}i1sy< G3;/5P_A %H4Ȓ"ѯ^BYMa$ 8zEyLDkJ{%=<"c_E}qH`P35"ܫJb (c7$z&Yn_DY\qOl!H`2JyII2/L}H]l.Ѹt9킚$IϱR),ܸA79 $BM*bv,%= ;xs{w E׍%W=xs|M4~6ݖ{O_/#ahdi cnU? 0~?> rzzvAs`s@(l$ Pj֩)*!,UiiؗQ~4[ЬC%ͳlE(QZ)9TcKC`!S'Q\P9eKҔf7Ha+*dO3Vy2Qc$]>=Nz6TSӼNf=4YHU[5bm̌ *ߤfKyCKćߓ3ݠfMpc!YwuaESKTX{<؅ E&U=$TAuªR4zZXȜFZV#=D^/˓Mƣؠ:Vл8f,BōRrz+ T)52 qR:C5ao- E׵ŪĵKa{~Ô4ϊ Z{aXPT2LS,k^4X2(44PMwf\+SY5'%1. B8D!#8$CR>: me^x/]jgwNyRPDb[bp]׮:U:zǏAo=z.C+~|Cta: )wîD ײ;fXġ/@to.CM8{P(ePGҰI'Wyat}h{huȹ#J]T{P(TD% 9_"C@:?{ӫt_;z8mHEK>D_*!EחW{y xGohTriV)[[X!3@6 _?>I?]3J +qhy~ QڰQ㕻àx~q^i%.!}m|<&^Rc!z\ƽ6F6йӦ8 =]_:0V07>H!M}}n~#B!8y=P(Q#^S=%wegIgҰƩy+clí[n[zɻ.d;aɻي_{ʞIgPyST?ۯf}~FHB jd13=I2W$Y>Do:#:  _K3z@CWZ=$A/3zާ)uጴp9n@Qqs>}4ƿ$ƾ+Uc%S‘*_CDDNwz.y?̢=]_DR|E)G_8~.\#DPYWQHnOgpqH\ĝO7Y,=$郺Ct_!Mq#=%?s.ׇ9cbSiy9A% {bO9<{-V|ӽk9L%%NOIN?C~]_l3@BI.S?oZ6zEI~Tq3$N;?^ooG˞@xlʟ 1u-9<^דb=$|x='1?=Ă!-yQϛΞpi5'{.w`0%R n|31odg? Y.~u!qIwn` 4På!1=Eh;Ճi|]äXIғg?dρpoJO'K{ɃzeRQ)qawjX;%^Pyp1gd,>'8.S΋v.92JaXE?L+!TY 9K=ϻ#<=ɏK&??C`(-Z4s-El!N[Y)=*/\|%-spg4|_~:xvhLtC_TficVm^:eܻ-DRmf'{ yvE𡛙^-YQMLK6x-SMRIoV^XvRU~VX/|޲ݻw_TVWWѣ J_Eo3ٔE~ug_+Do!}#ۄ c!W0lYۙ/5=Jj77D`4=IjHRnPEm ;hxOѤViƋÖ]+RiEOjfQSjcmMWI5C̞lQKDHYcAMvS%UjbKXUv>g3ZT4%:Vsq mmO]m`CDzE~qI7ێ`Ico ʰiY2OuHOCt2->|>C甀 YiRKk9ZkfNY&m#>!k%qJ`bJ-uשӰ; "dNHޝliiJygEⴥw(^"IY>p{cӧ>A&V!qI7ؿ؅RJ5Wak2bThV)avސ8{Z"P9 MԻ1KZ!*׊_RX5r2I#C,Њ:i[9tG~y$.Sw]J~"B`VAߧ Ov&Ӫw=.;۪wq{mO @4ucwС :0V␸ @Rlu kP_>3?{t~׉~vOgHHXCOBi/!/i]jO&<}fy0YʊI|#]_3:yyHV:9O{%8 Bb 鏿M\?T=r/8X\ƏAo=z.C+~@m&+$r{mXO|W}fAuȹDC(Re7 4Tvd=J*ĥ*!`p  c]{E|դlǥi4B?Y/n) q_^i*q ʬl*,UXl6'rTl:[a71U&FT7PN4+J}_4T+j7HEۅ|vQjmJI+*[︇I/{{HE-1~tjFJE Y-a5MU,Qm {0*UZiJ^<,|Vt!pC_5zx`Р BIMͪޚy Ez 'b4C,C4k1NZk^fNYOLIn\Hqq_'СtyQ;d]8t \i-哇~9zG #1lG~^/.v=e~i~gˡ{~wx>2}}Y^BT7SߓwJEa O:OZɟ8j\*LR )dXIiq'/ߦH7nիeKߦ߾J='u*oKȓ7贲U: ,c32۲ …x۷Ҹb~ϟ.}PT"2m@1sЬ3.!*׊T!b8 kTRWlP%--¿a?G5/d]z {${ZA7G{ㆶ?$yMl{~)L7^?ۉacB>㗮5(wy`18δKU@eTUgHP{}RKAЍ{1p7+X4T" .F,Q_A.]R_ꜿ=2/䇁[7Od#T28^ ɏ*/D&β6ʅCˆ ᄔnnm ͡6q3vಽqE_\7.6wy/^Tl_ImN1Cy&~n3™r~ :F&Fjpa/S{pX|lq=Ma+O>jX]vCXOm@}Jҋ{xv"z%Y_>#߇GF1zEMo7XHȗ2.9Q?va-пi}c\#* onTب쵬3ڦģfܳ'PlGy/JbGqLD4`.G*y˳a!^'\z'i%x~?c.rOƸجH#U[?<}HN{?0M_P?9On0``,JaJVccP=:9\oegdy~X~pdeFF<[) MQ0$ےwgrf2cYKS[ʬ/ YHecCB?A3.B:t7\Dꄤb4c~%UzN0˦̋%6[=3 \?c">8܅Hzz'+zt?[;ѳS꓾Vn a}a}F Jl弨dHIؓ/-K~\ Tgi_Nn6>vg">}U"odsKteonT@ZYJZ2o;MWq...H#J~G-;™CΫFĊ4LK]/7 GjE;Ig[,eZ'\K^;]4?-|ߟ \؟ef<;9Hp\w I~i3bÑ?l~> (;mh}^_|;{z6%zKܽ{Nauu=q1/NEMȏ7`|ߟ ,P:dmgD=d!$)A|I`O|'eȮ=JG Dԣ$K~ԗ}q>ط9@d`=>Ӣ> Tx` VCC4GirL7s77MY)IߤXmz˱x3zewcu$lˎ!+ɒqlp)VofϨlubYoR:m׈jp5geVϯB"/يrTQ1KTv25*F ,UD5_%fsZpqʨDj,|Rr` !vl O۱fq ʳg[F6=+%3q_RҤfKY(Ԙ\I|GzmԌGfy y»l.Q-KqdGT⿭ Iۡ<۔Ŭ.፮h4Δރ-'36kGZ :ϳ)LGr yޛF6m9f/(32t?[kY᪟R>> 30R:(OVsZ. h}韱E^n&-xb઻)>&Bc~cf\l9Y4f@TV^V'g\v;ew,΍ID~BFq)󲟮+R2{ 4'[l87rmʵ2G5YLAY%AS*]4ܽ"f6aWdVIa <߿Uҍp,ndєpF#$p¢~,mrQO]iqc`dcWϲa `Ȕ.ycIuq>!w 7m΃7ҝPy;jE>K&[Q ۗ<(uūpunōCP嗡촡±PR |.-.H1_e.k~x)JO% T{<I iמKV&sw[=sRuߦ50=Q'2|Me݌ "sjL~,oeޅFKEO6SK# } !\+7z ~-CЛ\چ@rai?Wz)ul83ku4?x|vȄ\ssE帱BP]eh}OgFzsl# ;C>/DO|K-K^kqݻw_TVWWѣ iHq#+o"\.mq-zߑdAգW %Q!|y8! @8)CX,-=JG DУB b@(@z.`4G(d2՛ϸdi~8eb|t ovYة F/ig+qel;0{N=J d| } ?Η/ӥ~%μ6a[}6)3`t7*-[v/FKF\dH9"qZLә?˗iR|#Ьo?o{S0' `3.R1m0r򳥧6K7G -*-3|#"^:->Mg$x6as\G赙<$Ež?tIiX?cNtKJ,#9iّ<Ů9°y$E(ƩC44 ϵ1 iTa9{\i0lޛ;Chz"/~W.윾{]hSҳؙ5>t9}tw?i vSO{=)weeL,K(Lm|%/5~A8#=l]M(d,sis+x.>V\y8h/JW}q3ڝw_*fzy`'͖xzꅅrO+;IXN -n;=<~.Ң_w~%o\wg#oδgϟ~^>\+3#N_?!DDQ I6ZP*%9T}bf02gmRmqهhʏ>[3IO7 \~yqIp79΍m 7oݿts]H>Ng}1f旖4$P8lᐧ9YtapoeSe8#TQꋳ{$>wvy`TϟNRæ\XqSÍ^*RO!(e!? y%9t` we |V}v+vԼNfDd^33M]76GJJ-+mqQ *=UXlTX5tUc?u@n1T?.?%L ?Ϋ0.K4tNpt<Oȟc.o)IOwHcMv# qn4l`ǭГ_rֳ|n]nYQ*R1.B'7赬3良u.CwNzԬי#7TI-_$KPbXs3tNwq YEuYzZ`%'p?v!6sW5qW{|嗉o3F/1\ZU]Ry vJZތO"a݂ip Qq Ln\'o'뷷6}-`@FJ6:-N2JR_V2 %H%BEϫGbNv*FGi8?&y˥lgJ@lKH8ܧ >vT:KRC -\(ԸoԽE鵱eK[QdU1 ^QY4'C\~iqSwe4q2~^ZKӓ\}) C'学 Sc7Ѵdxafez s'"(x*:όC\pN8#isem/3iiח#T [! -.;Y×#搧>]HM+k[w\Rlś?TQbA-*pSi>oVB^ qDFgHK\3O]Jv5~ZLc%gϽ6+ND(9ޱ`QdaA3|Foiԩa,jV#Y(wCTQJxu-v"q*xݘQk:0&XspT;g t_UNZ&IBbQ#n+J;GC24ćD}ؽl*IODGYcAMv"."Z8+To(U9\S3lF g8}hőg ml- ٦fecjn},(m)Q cQݫ`vӤV&qeWjEuzZxBTbFu-b7K%*y:5lCR+UiSSq ",슣 Fp7"JF/QӤum'{8|" 9⠗ q(UhvV)!lڇ*U ;הt zt QVZ$Ss|ʁ#g /ΖwH́KG݇^RpqP" "N`;]j~!FY$OOXb~w`7Qv28؅BI`n𺝶춶Q+v P(ﵤ~a6Nۘ%-,^'/ӢdСCq`s@(],6ty .dn"t*.Z=MBd" 0MJ'>Ͽ/_0/` <yp૫,E"]z;*IҸ>K'O%Zݶ psQ}fGA({]$f] D&y€@?'/rDZ hi?K=O:IH%ɿRJD䔩ԗq_A75,m6:%LD"C@-`+% 9M.]>,2$QXqO1q$v"AMt./rXK:B=t˽tPj֩7v-[Q0{4ѤO7TEˌn2RaLO[Nu- /\4 Kb,= g[T&骚WY\{Y#> ',,_]a:qhndl 9J )SFTSsOjhhThCn 0G gRE- U-J*%%|"=<6DN+W6}5٭#|v5%hJUΪ!6, %9ː,ThԮ&"Z*C4ӨP֢:fNYOL$vT*RY"\ڢTZE3w$YD&5{שѬHGO=vp;su>KL,4<^*ѣTي#=0:GȐ7>ʵ"UD! 9$'Yt!>M4TQqV7 'ǖ#C j/t{6Nb 6 4C6؉ ~HNu,B),$ r؝`KDyYMov QJB bnbϡC]oߡw*É0)S|`׀9J-u* ѐ:MrIMf:d$\qMOt"iEwOyf:./1=o4 ǖ>)87|#lYj{ _:C6oKTvK פVi$\ލXD5O2,D3:l~dY*r vzhvʹJ")n_4œyj fZ!%nPݔ{FWѯOݤ+Q嬖* UJU 8B+ϙ>C&'ZH+,<[av&)sgHȡzvMjYŕoqWZk޸fNYOL,ܢg?lk R ĽYM&w NXk9SR% zk|Ըmn^F)9Cd!i{d=Y>iH,^3=G?J|eQ;8,Э9 ZGО[ }C`Qymi)|rܢ, D?3Էbى"i;ĒGBj,SjL{n:!D(9d 'xiCj?MD|(w s:;2lb Ԫ0Aghں#]Wd!VIW }N-9vls:5R _}loNg W93I#zG^vll8YeܤZ_H/vl*Fev go/|G/H/~y\to8k(L7w74޽ "4ޠi?vB"pSw&FbF&̤Z7 Fl\(dq떶! [AIoҭ΄ϳ=ҏMߞ;o8kYul45jJT{ʇE,h!:YzDhLomgA [(= fBĢDMX/@02 (Pg]ns">ty=ڧ=牊8{Hn:u::وVr~QB*z lFD(03*md7ZhW}K[0@醹uN %Ny/q*%|)BNhFVfaaSQQk]6^u%oмehvsyOFZ5"uVWސ>zbm]BW6Gkيަ֜=2n8 )Bό% TG-Ո5%ۼ#Ҥ$dUrMIKő/0]FsϮŋtSoӳsn$̀d_%ƣQrS,p=}ZkQ©^V,z@S49Bkko8&:rӷ'JE*6kT#D4_!8yM}.Kg!aY6L筵5eSR 4u Mhb KN'r(UhvBios2Nt#ȻoHk'5us}22ؠFB'M6wnu&m:Ih 6P `_J0)jbוY@,P59l+M>C T.&r""??IG[Yzf {FqxX`?-szFaIE h DF&{.} +%jvD욀rw`Q4 !B{S0FAqu$KI؏쎡w@(@ %1BYPM}mE6riz% de^x/]j^T|Vw|`{.}kWU*=zT_y'Wiܵk$+{CiPpk WmP;2Ig*zۃ[ h2^P;DOMPA;Eh{K瞡P2.dEBӮ#ܶHrW8'zֽAfVAY0X^y};ǏW§zJw`p&K:LwQ 费Y8 ',# bnr Μ6$01ۆ(؅C{څ塑BAdjbpH7T)Z41e ;A, pqIRʃ W=th]DG rؕ$MVXp%sH>OА:qr->৆yCC7=]n~ZWTf~-&Y %cm#48r}ncȑ*FF4aڰ:{{o"[F|~/ݨyxz8|OoΖFrx0=:;-~qĸcs4Ba,@Z{{~$ e;p/e\GFpv868kGtCf666hcBZW'cQnPEm+~ƗMjf}*xVKT0rŏLl6)S2m#?59NDj0r^VZ;XW>-e]"zH~+?9"ϞQ][S!=Ou$ [~`m>3{+ v(MzGu&8-n1bonQ{ަ١ШI-AyxC~#N{J{.2κhsk+48+jK #,SR]/X.pY JLZ`-L&L7n%(1&bS:5X@U㓜)UZiJP/ Glj.`NC&ڵ9 ,iкzߦv(": !,f,DE ~ozHz,h\Ē11%ږ2۰-[P=_$H~l܋{rᯆvI݊I:Q\~"՜3-"k3"0le -Vn% lUY(dDh"sOOق9JK{pT/NBMcj^iR+R,j- լשU9)%zK)qcRsmjqݓ{Ew إ@DѨaT8aAhJܜuB2f9Z&b: 5 /bكQ"[顅áߨ'4RPUgTʨ/dؚ.4?YcV0K-e jAzt砆@2J"aޖRaTOruS:KRBV{IBNU*`ٔG u2vKz@;Sp sIiyV&mC߂#${"02sy:.5-!-AzRP:lPfJv(m S6-7إhHJd1Gi/ЬS8aH@ERWEeH`/,/K+`>$ysU =o@(u!% yO{B b@(ń3 g5Y"&-i+"ZYV(l"K3ˡM "ju}67HB +枥ۇhL_费-ɾJFtF&d;Q-Wxa82 ʰBџqj\ˢrӅqDӅA9X"8G;ԙ0+.XtM!]qӓS~"JT9XT<^UV✌ب8u;ݠD(Vb'ۅ5d3Ztb KB.FzUz#Wц|Jx:6-  0p/+Nj5#2nD~3DdȃhW*|70r TWGRb0sdh4Ѭ陧EztAidNID*TTT^V'Gz:9Րޥ =Iy9P8SL xG 3o4q1844^6szWv!riGfWQ?O4j=JمRHE5mXFIC3԰lqv }f.E$J9iRvP~V[K\L,Ȑ?60}`!ޢ;D ;GBj^Ȝ#ϯR1iTT C>';sl%XᜄScXX@5i[eShkr.lq12>$蹂Hb^x/]jg"Ku*[Fez{.}kWU*=zT_y'W9t>+z]SOi;Yۙ/54Gi?!C{ x(!xd ܟ z!5@("+% +QUdvk4wM65ftZsZ6(6SKU[-Iϲ<[zq-ʰ<eGPb|D%i..؃!KP#tx{6Td_$W,vNΈs%oˎo7joȖ'&mڛj3Ux/ (Xv^6f^.1m_7^m./G+#K݇K>حڧ} 3wͽSyxwWv^xg>x~hS4˛[=L ˫UZf41w%¢jquJOzJnݡ/hЈ6|U/C=z*~MN&!K~ߺ[7>BmG%ivӳ)^:nv# o uL%Nytڋl0t'NW%wKyMo>O&=f۟ 9t,k t|2N%\t²_wNtUpWޱ\??gs¬7!@ٹBinќ:^!-}F' ^)cp)L~p5ӣNpcAh;11AFG  v7$=N({L5߽GGuc<-pF#P~\ ؉qڋ07_ùtSˬ^˄6}Q-, $nS 71688+N-VR˕UZ^WWGi=z.Ė93MFm!-xs:Vua5A&ئ;\C;sC ;Q(a'ȰtMRj74ڴ>@?ac(V>"M4076K72M4Ҙ7DhdR e;$\\W>ACp,C"vsX yy8$ʅ?AuN8mIÒ^]H~xbzF.|swÔ!o]zd Zg%3F%ߡ85L4]{/L&6Tdlg(P疁I2/D|17XHl#4bޒ# ɰqg6𔨘 FW!cLwk(IAy$ʖ ӕNce DOJ,3N[Ұ3]^7)Fl݁U5GJn'emC0ʨWMpI!p%C8fJUVyRX*ĔLRI[hIƘ40P@ܸQt@:I ZW&F\erK{׵+NMr-]2ejؐ4Όe- zHzE,Upp:n=/Le-l#U!  =0[!qQjQ1픙̈́.l6Ƽl/;sݼrn9JuQ9 #@Z' 7T"cImn`~HL0Izo:u̩'hjnF꼃XB;ӕμ?a~^--.P.[Ұ/]ë^sEn\6Bw莶QE@v 9QZ;wf8)eBp"eeag.H`/|@Nv{a_'t<eTfic I+\fxL5R%N@cz$DOFӨP#r- I5i^*f RFZJғPrZ9nvB764牓pۮvwޥ~SyX]]G+;O?4=*?g$D0ܡ3zW^gsq.|ꩧ񗚶w(QM6UU-xnTQ7eҏ"qbU@hջ~"AG|>!vB]52w_H`tD%!p;mni{nhr$n&oiA61epCv&͒$6JݕMغ:UuzJ?FacS{ K%/'Yczv3{h>mfCQ2ߨ&h*_7ӱx7ڳ?;a@gFCns =hnu M.n{k01·h{G&h '=y+D,գDY2=©th"#ݨ^oppV{]eKzL$3]zc'(ڧ} ncwQ8iG*{޽{4N!Yw`V/{?@|qsۗs}jz_Qnt£tӦщc<7|8ѵ gyZ,| pWxyZQnt\IǦt8=ks_<Wio}?=G Rn8 nnR =6yлyފ7b=Z]^a:q1`nU.%G2-/ljMޛ{#2 -*:gˁ?J&9o:-iw{GuvuRKz}F'Np}nCs͟ɟ9|pvav?[1mZ.÷5B##hjJ'+$/z-y]Ív?Si7.?n}"$ӡ`Rƈ_~503=CX|Bj8– rݛ6{+` vF|{, paGPJwѺʷ ꤜ^%gSR_RX?<J`p$MhllҗZō3ny 5dqqstEPik[.?F똍?5/kFLGW.?E@㲟[' >?U. KNa}Gl!y.3T)?)W DYꋢCvG 5WO4I\$#,BzxZ]ruϭ^@(O4Z׃%ZZ:BGMrHތ|NXx>khKI\4X%CzGad4RF׈h/ܡD/=硄pA/ULtK|κp^LHsk q!`Q;9G{׾A ˹%:r];Z`:]e ӽEZ? oqTr ߲>FÝ;4wSܼUs<\~. CԹ3Gwhb=~H;A6;v"6gcگJ=tQ:.~pSܰz)k^22Nf+a!=JܞEoSȼeZc1/^evץhSC;ֱ,tڴ:IK!bqyg̺{ZS{or|yQ1,7Gr;q._.}oqQj#ٽ VQJ{ջ!oou& :v,fnwNߑ^դ-pH^@=?[Cى9fDH? H\[~JFp=w+b#c4^{]͠/VH IW JYPe1j!nx:Z.h%l"񰾳.~zK;jc2G"*416AvㆾFT>7kYύJR$D Q7ŠSNgF-JSf:|l`OhR4~. :ьGsDj,0}Rr M7nT,Q1{Gzy PTGJJS=R}j(FlmqPYSr&qXphd*,<[]}E!=,:Hdٖ`};iR+W,k-լשU9ޤ~t\=QمRHEjakݔ4_+q?cR,UJ th:G5re_ =I_8j\GW:' eG f]x1V/ִNC/lI:]re\Qbq yNF8WdVIfޢ,H`o8%2"Qˎ_\-yJ ^vD} @ "[&-z-JB :Tm?!|x`p,m:Iu\lP"\-!y_ޤI,@(z@F A?$ĀPL(Re7盤YrʅmJ"}1!f_J;eչ-;؛`İ%ɘTPmݧf:Dۄ,znӁ_9 !/]{ҎoSl.GQ8fbgs༕YIW\ ksk#.. )ZFPQgd3 qcܠU܈7]+zn|V&!25*FXqtjTרō3miٗ> fESf!rPiuTgතUa? pT;JTVG+*X7N]HqߞcWi#C-jIc5nPݨXRcYH:5XpTT:b=QRg;6liHM TUAR9XbRn+06 dr֖W{F 3ő'o+koqY9iiaWqr 9C-> " RlJv%ҤV XHZYSrv& GhE\ [so>;:KaY6hvT y!P*H =tRuk%ngY*J)!>N0YzEIf- y7.vo8^RJ&}ٓm =I_8j\W:' eGjFs׮M:ޝ+nBIzؘЬ"C}]0x$JTwNkZ$͹1Hn_K9$⹝{T$qA϶)J,μޮ!5'C,wiimNScIMsIJ`2s/lǏvSO=]dmguR{@#|5Vdߞ]Asd>5ypYMDT.f`e(^*x{#ޡG%XMv'sDڇPJB b@(tTn ;$d2՛rԯ(&7w쥤ͭiw V\:!\mTeCسŸ&GX=x |1k [Yփ 3JFCg6ExUf7hc~,{]|ܠnD7Ѥ<1ZzvZI׿WGX(𳖍}G(Qs\+9pvKϪU60pл%Z`#%zݦ:שQP{׻z%/vxn"*;=P=*w? [ywӢ>M&δXzDG1|.f+IajTqnF68z39޴惝T*xUw8.V'?NC9NM촔ݞ_[6湁ȴW#/j@krbA#.LOE79JKGׅ cAُswOVMCD}l*MP$m3@_>?4P=C1ݠ&7 J#iɂW%nG%׃5}쬜50pѢE f%pkC77X"y1I>Xy濭Ю̑$9Nԍj3CM.e'b* onT-sx VR^rS51 qneb϶d &[=.8T5{yO9J?!=j!!9AdHW^d4fY7\ tN Uk-լשU1z/k4w&ur2dztJ^/G(FW|,2GTw3aqР+,dnvp bQdqSz9/T3$ےwg9K챬Fwh*+[9b!P*77 adݔ4ϊu䆀nmH=hxpRcpE8yÙn˯ _0RE^a0EhNQ㺾P Õ 9x=R{ GkdRw4u' *vatdmgF%zKܞ{Mթ4?gFhvcW޽K_UU򰺺JGWv~iz'U~:7SԠʦ4{W^yEU𩧞.v3_j:G)` 'G}G%8v[ `.l4o!Y_a D& +QUd~B b@(=F %6GskJ`!"Yϟ][$-kt"hMv%8BS>KKt$Myk/b7(% fCe玲f,b[Dbt ][Z4!6xTm]y-GfJKsGtXCôv&M^jlI'IXLM Y =kur60 K.}`z9tt1xγyi;Mwr65OZ5eRr37e1:V˃$Ts}]E-iG jKTjLG{ureF7Y~uIT4s!}W^yEU𩧞.v3_j:G)FKp:XaЩS1D+z3uwQfx(@(Pm8Ȇ޻J+D n i2MC/yliQzG;dAӍz J?bGB~hFgi޾b&+ *~*ŌF|a*=?M+̟͞Q`!"4-reutA(?n\f~"qXƠ]λ#*I$0vg'V_-豕NeţY.|ƂӖN Ejޕi"SsK}UcF4_ {uBl7*#|:N՗e{0.YmI۞_lAl3抳SgW}JQgSV YsZ_r|@łKb\9[5R)0_ QFG'o8F 2GIy~\P~$JT+u|'4or!-4GW_ڐq!\C =[8~+-df;E?3_} u :`$$ nEiy˳a!^'\e'i%x~?c.rOƸGʳ"_Bg@PĕuLONNe0ٖLW ޘjF#j)[qNztJޗsgR ;nysWX(Y2νQު!;4I%=쇉'Ӆ}+2]_!SNvKM[Q۬ecipհ3V+GoS.?a'aO+/6ыh.[xý\7g̓nzMGl!R. r/gyKWM%g? @(E{v#ǛPV:ܞdX 7yZ[1_6(ݵ87&yQyDzϟ7 ݭqr}upFmip8;0ޔ՛E7Lˇ>TFRmUSlX*Q㗞?GAP]F_ ec/EDu ڎ~$osKg^렵< gR`y싎ujf3%_D$~>At=[!Z?{U i6Kw9j&ҕ l=[ D0-:\mo= σO{15I$3a VwTUKUQn6::7_ƝWKQ]Ood!~nqRK`]wm.jn>nhdGuRjWYd(4;m Rw -nYnrlulskU߳ 5},*| K䭻|WcDSmr X4Jz/ M<-=B > "??I#bx n3zFZ_dKPSs FSRéHQGHߋ-omoԆjI&7 Bn7Pw,kܰV=OTdz],"S(u˫;:B zI%0ESWzi4fy;Aw oqd]y#ϡC ҐFV D4K4PtuYբhe1HP^y};ǏW§zJ%-B 1%¯ߡw\5/di~8eb|t ovYة F/ig+q%{ N ʞ~Ӳ,6Eh@(=4%Y{-:r䰾Gr!%Q%oB_2""E8h*Uc o?o{S0epiyfZ>--=Yru qآ27Ziyl<#İӖ^{XWڞw?<%_I}YϢ^..k[v:zxRg-Ϙ|z~6;咒?KHCZzv~3Ok-A0lr'J`D7лkKc,g7!?CTQo0#WD~eVoE?^#޽s`ޔ,vi͟O5]qo~ovQs&yF-Ը-pQKZiPn vp6?Gzl#ESJ˾4%Y7B:שQM}SYTf.bNC&ڵ9MS7藯GѴvN~7HzQˋ3NN2[SPjA_ .[V> }?J,m|69pc$hT0YғHBҊn*nT0P(RøKQsy`TϟN[5næk8+%mDCcKD?5M~f)ez s'"(x*:d~֡a.[8'%~(☽e"<-8KϑQ^*LC-yyŝ#2f_ֶ48קRV],:3O]2&G~!_}1b5\fz6%zKܽ{Nauu=$לx? mhf@e1;l7>ǏvSO=]dmgD` Ӊչx 'Ee; @Q`9\bNM춨9/ t1 ĀP{ +t[a枽MO֛4:KjU?A]v J`_ ;ELYP5z}ihZ[anZ|%KEwctP{kt[u eX "sőޤ:} ĀPJB b@(@ %o&v BгD?O'hG褹|8v BQ:O" @(}]5 @ %O&saM7 @J`26Fl@(}-A, J`/~n-ݔsx46U%CR)k4FLw$B b@(@ %1 tn3Q[_g%o~;׀P{ +t[<`+n5'M-MOnBGN9[ i3*YhnTYeov` BDҳD?O'h'kte??I tܬ-nRXF_>qvsw<:u*H^ St$[B킽+ԧhwoӻ'i,PBk[?Kŧ[=L ˫UZf41wuj1~u 41ZEZ7,,Ђva[|UZ zݣ \$ў{naw[^NL1[w]&^!q¥AIϿ>8; .FenIzF\oi;HoazH/\Ď{jNa"(W]c3` t4QZ~Vl/~ d3D'NKt޺qm6n&WSg7Й 4ܼv+u`_X?;r*._x;0?I8:4΅+N[(HI*qZQv<&8NaC{Glɏ;utFH?.?y񌸮(h31MAHWxs:^S iega&Ӌ}Ƽgf %XMkczStdJZYVj#n3=vx,SlqXgQ4q5c Wr)~F9gƢpBλGqIp<8mIZ4r\HʈE\o8'm6AꙄi*\FwBrP"v`3vN HhTAYD2qdY,@(Ñ49ih-} o1-;a(Mė-.#5GΤ'Fi5F 1Ft#suĤp-n\q텅m`O )+Yl镔8ϖ/^N{MÍp{VRp8‚/!;SϡCV:L7{I /=7 z)}e'k;s&zG{#ǭ6L"IH@$`{Pl" D%1 }śws?-febu~?Ҧ=t?B 1%¯Mo]pzYp6w;UZXŗtQdK2o|$]oVri䡯vICplةu@ao*'چVp@l1 BDҳD?O'#/ǩ1Ç)i%̦_n8#ȦB]{l5[!}~ݛ'@UїgmWy楧琓H#=7G4E g8wjP>){0O|A6jxRw}Ώ-r%ρf H~iKe;P_go@_z{_ $q7j}'Nfesrvr{WF9/sIyc߸13w[{HDp= Iی$/-r_,q[n-ϷǝhqK3S W4ҳ<]F'W~ e0ӈ"dJ\&Ni%xzrsc;o.;6欟ϑ+Nw_.2ꬴP!k]nStH 5z9y e:{o:Gm:uHC73|b9_#̴?5`d7^~/2g 'I g-O g}~%='9ӉMLo']yLY[VlᦒYZiTL7AΟ .'Tbδrq qMi^r9vKwVbP,^&BK$sF$Oμ;>GμmWڶr)gy,yw=wc~KstNҤU'Iw6a%CM”- 2!ԗ/R5Z 1FeF*QR4:6v[Լ,.EGt;.mԏcG5@yq!ֈj/,dk¥ii),et"vz!G>Iq t!NLFp+ߗJ&SIZ>GμKV.LtϳO'h *[ݵ]K&}&?}~ @~ݻGs/Tyz^x>+Ai-~V%zkޢ#'G=|& mOq=J' 3%K uWﹿ2}v'~ d3Īwlwѽ::P&vR`!uH-W:x?g da?Gz)==q^U0mHП̭6LzG ͑:DPx&21y`'$9y" l/Q}=JУ%`xd"vJ`!K_+W%tSǵ5aAVQ(Ȫ;VZ`A76nu2yC6K %,ɮeMS/Eu÷Վl\ @6+xpST6fM>` l߆>EKo=Ic`zߣo;F帗[GhSt[[=*iY7nx'Oߤr *TP=:L^ 7 )-!p|yނYFot燓Wh$mIyZ=zV9{_˦z8]'vY7ods7 ]q. }l{{]vХe46L %\ZY{pyXҡFM" r!xv !9=ڴv7k Fhv↳JlPIJ"N҇[tI>ŮP 4:QFe&(봩0zt"a؞&-/2LJuO+F&p'rm甑spCnJb52Kqfn9ocНx% CLN(.yq,¥u89~uзR.{ 㟐ϻR$,Xp\7n@PiimNScIMs]d7Xyx 5hqyL8kiܲ &ndΜ9PntEUwB r J} Х)pc~ܺ划̢6R'/H6˜=D ryoK8GY~9XxH0̓9Zӑ^P*/'ߛK@(}[VЅ#e@&MrMz|F&0݁, ag.Sїy'GѨb f<[w %bu%ޡ܀foi, 7 ֢uO(u+Iy)ѣM\qyXxSȳЧ=a *[I˟-=: U.Lpo{.̛#]%mT# kV lNS]^8n:e&y_^^9H0kF&,Yy#98`Z &5ϟA]ʝujZt?J=kz.ҽc'9K,d21z(9;8Gi^x/]j/s]׮:U:zOӓO>s!}v?ңpav~ .|ꩧzKX`!szOĀPJB 1d+tLvm]r="%y:yD;,kk4y"]-=;!B !4%%Z1}IG>K:HB #"%-m$`B LOE79JKG G&{8uKG]@(},p:9YB !GK7..k|@(=z)k;$B b@(@ %1 `YE<3Gm} ހP{9igwEk4MKMjhn^MO%Ϗ58 .72Ah?p5jMX*Q*r.np>7dy1:dI0:mn 7 gH}&̙3\P@cB+.d=301e2I#zS(3G45S|;:6ȳ ʾ0AS|?fa=$_ = = tA @~޽K_UU򰺺JGWv~iz'U7ks$]Mnҕ#t$6pO>} zDS5.,|w՗HhO}l⅛aqv7~ .|ꩧQ{Xy":|XW4IJ35Zy"4͚1L5}C٤J%J"5-}%4hzhhZ@9l5]'ibSMsh, #U4mˊn\Ic4VQ +rt5|nFOZz-٨\N&{:/ްa+ݡ,ܡӢ?)qa;^v`Mibr G,?ǐ6\{r<;>6ґ[ \q*m3cZO☦gGWFT#ҙNhG<4Muu((:rLJSBqdQ"N"%9$<=O"}])jǂm 3 [돭Od2c ~7/vYt-ߟqNjw=+okn_\UdϦYf`(a$Y^ɅfIՙC9w;s%e>蒷4/ SEc2.3kZY.k^ZjKf-J=O$-jvihujR@irl E ʞ`4tںy~΋51EyIuڵuh,¼ݣcIZz&eZeYf"M4Mt^ #C('Zp.bK]B/HB-;L]lX+wEײiqE޲V"nwV=랺j!Qnv&5>.Yit|'ԍ0%_)y%teI5/ȔY>Yf Zn4Xh ob+L=eљV w߆kk[@U-^d˴:$ƂD5 4nsd,޽^aS4k_pzmǫf[ZM"o8D{=ًϤmg}|Z t/8FŶ{& D #SI# R0I{SڂIN5*h%T(f` ewUd$Cu-JZDp׺츌z׫> ~';~w#w%g\lqaޕ| (w:c_ǶhP-r19AV_iOuʴe+qls-:8mCxކH+՛{%}1ݘp"O_QB 쫚8QX"i̎l7Tckղ u4mxoR[$͗T/ݡ FH]s ͖BLzOsgEG='QԔ3MWȂˌ2){I[ZrE ZwT{C^YyD+ekmdgKoIy"[YnټmWu+jtF][[nޕxoAA[׳Ye H{ֱzI+u|k A w7^+hIg^>)7'g~HZf7Cmu;v}i6O0-x%ܖyڷ_RD,<7,O푧`];H D "H D "H0Z֣gnH=|@r$J!jH!9%ID;-{;xBdRS)i4;6-9$D #kѐt:Hޑ(a$532WrIF%I3LO焞vF ID #.չfKsG5cMhgvc;O?t3rmȶmh.\;qyٱc{رcr۷ow,~\ =5μL%"Q% Q%`J216&S&5S8>8o`((a4GTF{(MhSnȩȔn\o1J>ۏa*K ַ۷2j%166!%Mz(i]5۰T<}̛X.yl,@H0BR=ՐܡCrH99U ҡi1/k1#䆲d 2/{`dEOgMNV2R.grrERu;oR_n+^UټK2_\XdD佲/5D #$%IIw㩔4]Ҵj Fh U)iD`]lpq]:4/zu>o~ۦ'^Ia»|R~Jq2ۧWo .K> CKۗ8Wcğl:Kx3^68{t&j9[N릸Oŝ6Gm4דIdE夷 oYm7 6I~NH1[ԥ,۞ e_ :%FC 5j4MUQ_`mE T+?Z |5LA}Ů+jWQe/t$]),3|9raټ顫q%[hpyt.-i||q}w|(rբ"Z-,wKr}&+S-v;W:!ӂs,3[=v"iu5nT }JZ.~H޷ 筷4aaORp;&|q6I~JFsSnC 't?-̸kqD #QŽ %M37YYV{mC8ghy{/sEm~N Z<:]7F݀x6|f]@?{Y_w\ˮ[zʴ[TYqRE|e{4lfkX/ ̠$Εr(V_?uiC%Y)jÒ4onO1o|X:%:߂ y F%B3LO$ 5JX8+J)ƴw$kS :!\hfl0u^'K쿅OܶKe|$:is=yzܜ(j.##o5fuFJоMPZ2qo:Rjy RBIY_ڳ B8+n1d`X:/ڎmqFEy|Qy(;KXPm_|xkytƒk.^:z2Y7<8uעS湩Ps)]NhIFM~7 ) t~r-$ QKu\1wnDuCr5%Vue3[Bw5iRR *+uзoOU+/ ZWۗij=6_u/KP/n{N C^}!֗`C:y֚vfRT/ILkp^U~Ztj%`E6W zB~G 筓g8UjvLۤu9Bb|<qggAeǎ]cǎɑ#Gܻm߾ݽB FY1M@99~,HÉp$)$GajX\xQ.^q gj(C)(ٓHRD@Ho=6aUC:?v kiv`(@D(VoAOͲkO`7X%F4h ՙ6H51V)x\pO ެg Yf%0%j[:'PJrxoj_QobFdi e7~3Ͳ߳M||jB Os˗]3i3 퉦:a EwZr$J! MJڽOXt5%w:-W\(W_Z?yuh^8|4|I_MONHMx8~X2ۧ]gm=nzhV~YDӯ@hy:D}6tvQ]lDIn $+z)ZjZ?wx{oe{t܊-Ԥ_6Z/mU4ko[B{:.U4N+κi/۞`Sg_9 MmM8TuX$JYAd2\ʍo߅UQ_`mE T0ߕw-Pgn~bWu %/JWU1;JIW"ˌ|Jp*?>tU,(7_.[ եo/~'/3~۽` Q;.fص;>yҩ+Eoڐi9tN筞Nrhz dt]CG4ʒ4?koW'<'KJhLE_R'Yrퟶ=~Ͽn?_LA\g[,GԨ^Ʌ$Id.א{Z?XA`myL-M*osoA[˔G__T*ZBL51{gu/KVN(Ntj'}ZW҂0unn?|c71 ]ΕskαnxޮJ(Jɫ7RЬ/g´]pbSܭ^}'٠:Ypf3մ4cIdh'HZ|S}[g=̗/`~cEcIMOsJ%ݨ=ʀrqOuJ6H~֍&\q۳>6y;H0RzJL.TZޞ`-1bºkM ⬯B2 J#omCR\C#ձ$o'S+|ٺ!db c`:/ƗP$Z@^72O W{ V2M:[拱e D]"/eyxzܸ%ذ=L8ILWkp^z-KVzϯr{~uϗ3zIӚ,юLk;o }ﹲY'₟C[Q;X-,%|s}=mgu8_ 3 /|q-m67߅ {~?^v;v9rĽݫ!dWJYo$n,Py`/ 9:H״HTX=zԽkyÙ-JP6k-J"$k=zJ=F#QZT`.aͮvXs:0;lOl4% Q% QhiT[ߓmHuƦ #Q$TCr!&T=UjTOxZ= FHJrӓ$hd#iI̗mlj %l_vCdL^@yRE26֭ 7*YnPᲚZm֑o4aaI>/ } j@?Y+exǎ1Me|zLF W$o{6d re_u/g5>j]1(!QHnv{%4/Ɯ4ҮݡI;%qŕ$+Ooe^wK֢i]ޅl#ʦ%N.a˒Ppm"+Y lVmrʴ5Zb vlr&/dxs};vUzΒg}z$Muege>Y#M2K,VR 1o|ⷊj٦<^e6?%KfivK izJԔ&NWNp+WOe-gOoZ8!豜tKV}j~l oQfo- "զsL+ժ?}zOtZj빜IËD #%>IzFuyقTz=%q6zJVyP%.K4jt촕HQVAĸ`!t٩մ*Lw*MzXOd-y~Z=tJcW2It-g<6Z[k }mgE˗if*${بJl9 FCR`zlrxԀֹ,ݷ=q\驜]g`Y=J0H0BRL1zD]:֤$^MfZerZr1Ov{2E׺ir,^Yo]lzIpL ѻO몭+oP&/~%g\ Vgy&_ٓꂛ嗃[ ,RgjhuZpZά,ܗ4_[Э1okA QPY7LφOiA}Yقzo''HqdnnEZr&i}~-ce եouu+Kܶ'ٷcml9cbI|;^:[d۶mn4п .S8رýw19r{׿۷WHdW5ʔԛ%g|GcS$ %zAq5,.mk,3M%'ES x*%Ƣ{ UuQOH0 I;$C̍$7LI˲tNKV 7/Iyo(a$532W7YkRs8$J9$2=[~&I h!QHIL֤\i@ FH]s ͇ZkV:&G$J!i ?ZOޱ hG$JA$JA$J-j3Uiў{d?Tۦn#c2V TܔV҃A9dJH0BR=Ր{4j&Cu'e|!qIn1Ʈn2eJ0 H0BRnnx*%bNRTĩ^N sS2VEmma'5uM&?X?"S,B͛;F Fb!t6YuHr]==t?>Nz,ݘbRj)Z{Zn.ʂ&GAU$`XʶyfebUx-Kvųny$>QBԨ^yu˛KfeôI|&*M淕41%kqr փEMeBvgR/ϓr{_F[kR(]<)%K4rFF%I3L[KI?sJON^0ݤ(#!I# R)[x(ɒ{dkQ-J6K6%IRӢ,6HIjoRRvk:!''b \I3l\,J%D #.9̈́sG5?.ii MJmfn]MΞHg0%&E8Ҷ ZB]l֨`%]%ץO5>(7_42K=#wvl۶͍wcϟ;vw;&Gq}vRa =a5]հѣ]^hQ(%o{8#Q%CUkD \,De,1M3I2$J-j3Uil OkFm=GR LHѓխseU{򵰒m]z1뽾A[gǷ| IwdH0BR=ՐܡCrH99̆2;Ӛvh:-SDjH)ZcmQ59dF7frnv %)v׌nԒ,Mj0/|HB&'K>WU{H0BR{7JI迩ץN7I.U_/m_,x-Cpq^Oj.ּj e? nqvUޮηOKl! ܶu |/Yukߎr&Kk[_t:oC%Pm&?X?5%5烩-d![XuR4)Z$ɐ(ad-63%K;M27TIRy˼/B^⅂,ځPW:A\E2o0r~ &+r2΂))epno Z[l[d!X%iR p,hJ3/r"mˌlyޖr&Ki950oOUsoCKݺoHf֘cQM}bQ*g$d-_`T˼{0 `xz'ID #QŽ R9yE QdoMh5F4Pj 3ZZ$f5/C?nh~WY)f{ {վK-ufփZ۷%b-8Zz b`~!Iej$ rc{-e3]\I[;~ۈ$ɐ(aX4SOtN¹P*7ߟd4д{~zݖ.jQ="Rּщm2K$ÿ7Pk@ȂcZq[}m-zJ˴m{VZ@|^`O?)w6x-1!w|Ie)\ FJ\Ԯ!ՙYYܛkݳᬋEbdW/'9֍nn n7_ y +RJLinsٽQnlTUDh~bQJkb֪佱_e\}^tsQmX~L肵Q.@%Tƣ\1$Kq3ܰ P.DP%Jx+ȳnvoZtj -Q-7ݿ1=EZ95hjM aA{`[2R|dnn#Aj%Oi>7_۴[ r";!l8`VθzI/m6}97tOn$ ˴uVޱbZզ۶'Y|uWZ~ol釗>zFٶm … ryT?Ο?/;vp;vL9oN8אݿ Љ%fejX\xQ.^q gj$JLC^+IG "H D "H D "H D "% ~G X ,6j%`#D(@D(@D(@(=C6wX/OXJwwL}Q%6S(,t@\žgdn'w#QyW+9DC=X7(?zy|}REoS>xrotcUWE>SOdu"WM:AɛR[dK5S2_.-$J$}3?&~wM@O>+K騼߸ UMk%S˯7^_׊?,/$J |O~Z2rsuO~}b5_/po6=7L}9i\(K~5׊kyEm|<}I3ɛ'} Q¦+J?sZƺ?,[_+_~R/:{\nd&9~֟䁃EyBFjC/lyy[?\\\k!c?|@ܳ"/7$/⋯țt'ojnXch<16g_^#qsEgoHAN&xBh7-~ha9}P-`ڟ!>\#;}&O?-<7~"IzbB\3ğKnz`_(Gw긤di$JzϾ(?~D~=_W^?t|y/뾵E.m2O$~ImW3&u'_"rV~={o>;K>[}w|Sgsc:ibh<LEs'VƯ*ɋW gZ>pG{|[$_/o]+/\YҲguX߾wu67Ou){t )M=y\~7 wUW˒&K\u5:Λ$W\qtW/s#5F'0`OZg-&Og1(a/MNAfG?!9xme.O]\UxAN=折h׻wa&y`ߧxn}ɮrvV~LuR2m2;/i9>_mw~ĕ=n/}HۖfojS0u/k/A׻n7òE-ߵ67H^/^,k3[ZogbVvIݫhb5+ZfsIKf훧{;e'7Vy/|K'oߐ7}Vog_]^;۾!^24QʭEB$J4fnk8%_-7]{|~\vz[>9lw.ux;%}Ixb51 Q¦2s퇮gxA}v<'3\)=$IzµݣwOfI=i&k}]{7 St~6A=So:Yf{ l!] ;,; חUQbkKw;]-^}\f$E%/W~k$urnsx[A{O nOkxb1C=$|;ݻ>?&W{+rn,ƞ*sȧx磏>*~;^Oa}Zw--H\v—E|>$-^;tsva=$FL%lN~5IU$>Xo|1W^ұm_'%RGZ$XkP:gK6#%lZ-qgm;w\'%OퟖRy-%wO'oD%lW:oxC]ԑ(@s aؼx<́D 7D (<: Q% Q% Q%h%`P%lVm?8 6fDvQSIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629144929.0 guidata-2.0.2/doc/images/screenshots/activable_dataset.png0000666000000000000000000001701100000000000020543 0ustar00PNG  IHDR`sRGBgAMA a pHYsodIDATx^lww]Ӯ ,V~cebӅSt*j2p(J7 HwDWtڙX "Ewn2Bt3wj"2j:l%K6i||9U=|Oy>/ADUHDn^Վv=OD`P 0(D" eg"nAx[.O NC~ ^~q]}~ʦNEU;?x v9lt󝘺NR6No܀SpafƏU]zD+qP6\ogspbfpunsssno%0kuv wpc5^@r֭.7v>JdxnX_8毱26x ݘ]M]zmw?RqlS%oV}åtM 𱻀ǞR#zet@6{ R ?swMނΎY|ck<ێoމ][/oJU,|O/#F~!0iyT y>`zaCJrYbQPnI+8Vk{5oZ1G$04,u bWwN$}'8KM`U᲏;Âttsd:ZetmfJ#+K=C忺{&o೛؏uGU ]9"˽ze:T6V:&)ԘQP;7`?o|z=8y&L[7XwFW[_߭J?"*=?"+KݨTq#4pcXq|qg%ix|]/~V67UDqA'!E5U.mrvg|"bTuQpRb7*5^_8.R~ ;8pt+QsY\w>O R]V泬RHir}UwƶRhrZV͂B" BdA!2ᘾs^$RާYQL0(D" BdA!2`P 0(D-,ǎsDK۽{[z KSeǎz'N ^D" @킒?^lXV >_ ^$2Q]PO …kd)+\qX13~9I]}. 1|_ ~ݹJ:/0a$K"X/AVϊ$@|bzˁ:*R[TZY.AI{TNt UAH HE]4vWzL<5&pMSV*k_WTTg}l{*Ao9vW(z^Hɾs%އ싁_ʓ_J)yqAyզ֩mERR$ݩSZi ڽRrQI)1UFUd'sJewݜ^QNn`w* wSệA!2ԒZR`z`BdA!2`P 4Y+zhiSzniMuzXe_ua15]]/" E9" :%{{kqatTJMGmQNžsOgƱeP;'B0Q!{,)b|>HjCT*81{NjBKaP R`jI-{)0Qp0OdA!2`P 0(D?爖e=:=rw%N<ɠ qn!*#ppBdA!2`P .(F^A0( D)j(v@ wzzz,Qp(Ҙh" Yɴ˰PpىI,CHKYHbHb)u9T5FدDGS`UE.X0$Ԝ̫D>`tH^ KԨE-C@T őW?_ b+=?J;TpA U~|n^hGH#BudSA8\8oj.SKD2 f}f<{dopzg6;%m ^mW gO `@AHeψ51 gr=!s,^Q{$L ?{⤰~"W)6H:bP 0(Dx 14T_Fij^D" BdA!2`P 0(D" ^z Bvh&7IENDB`w$@XLBQ {<xNnow1{z&ApGn3mO;%4A\,@pZvjI1SKĀ ZR&-Ԓbi{jW p5ӑϐbJwD=B[d"*)>4<`GY5I S+ᦵ@ffi"w5;;ʵN\.ʥ<tEKB'3ڙv4(ñ);?iid}_P`>ɪkT|SiyD9^VMk&_dcj!ShhFUe9[A@HE#s ŒF CI@ ॑,Q XBъasOtaڕuF|=7 մ EߵnAd-(ܢ|CuÅm1 hBvz"r~"aDmTwtENi@Mp'OJ?էGcWN_⤈;VwIkHw(SЦ4"A6%)b;E&nmJSi7= "^B2M߀a'SK*ܴ Z7-;);NNNeB{lXj  w;NkD7ETUQ@^YTݻ%@R^UaTM`)9f7pۺîz]FZ$xgǩn@'e@pM^qv9o0My68Eܓ_;NitGs7ӂ2q. w ;N;rFG, f)H ̴N1- ڴp&@X{V!W<,T! C;+Np?ǡՐ˯8-VC;S/iwYW pM OM KԴ>*P ~(7i!&-Ԧs t;8cy̾)M|ewÝvwT֛ zǩ_j۳mٳg ۡ +kӧw;N)4S6=-{hw;Ny ÎSfO+&v%0`rgp 3j۰Sf4a݅W0IqʋlY>;g{H:ܾ8XDXP*ڲWVXSF_DNUr`)IO8&p!ʡ SlfsZpGMVr*ЎSF7ai-@pK;6 Zr4څ0jf#>H"jHBk;S/sZ Nqh5$C;+Np?ݫa[[[G b#EpG4UHu !6& Y._0Eۯ;~@ɩ{7^ý*d jB$-Q/-@p[RXUܮExA-c(U)蘣4(1B6:4^5Xȹi;m*YCmzZ7-#k^/[[[c4~4 C}((*snӦfmpJmhn l2\ 5;oo~)>]_:u4,eJۦ֗2t2i ]"5JrOo[Բy\olق%\zFЋ]dJШlo7mڄ_ʤ5tFЋV+?.sj [\:I+8ѣJzћ8 s)))kܹno0+eLM,>qF&S~=nZC/_s̛}IH*ZNY1b3TBc/f$!h;gn_HBRw*  ܾF.%Z|98gJdi,`(d% d_,r͂DIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629144965.0 guidata-2.0.2/doc/images/screenshots/all_features.png0000666000000000000000000011671500000000000017565 0ustar00PNG  IHDRgsRGBgAMA a pHYsodbIDATx^#}Z")J2)"ebFG -k mIz'ﭟ(Ee$4mwx$Ϣ'&qaw8#+RB;Mk"Q9~߭[@PЅ5]BuunȾO?'`ty!P P 8ppqxZo;ݓ;_ y-"7^BލWʯ,?pW.~K^{˗m_׼r}fKE4xŋuV7?7Q;xʿ\K^ _(o/Zʍ7ƍr !o)/dX^=~KU=s?_^NϒwUCyß3>"]"~ߕ[Wo3[/_/G+I_/x{KÿO~YA>?A<_{iYa2 > +w~V~ڷ/.J[_/xu_DFJ-'k?&t dA۷X o4Sd[ѿT|D}&s`–ej)Wpg]n~s/go$?EEM~i?-ܐ? W4Â?ܩE(DkS䳟3\L ,LOoy<-%?n乯>/ou"aIFjy?S}멷j?Kojenu2u(esҿ_^̲X1'=7?*(O_nBPj4xAcci~J믗* Z@ǿ ?&?hteiEIߍXeяkyfq j t"?ꪟ1>jREY|S//}%r_g_57|]~&fO+_!] ~N^ߦt? d\VVa5"ka7oYuh~E$*pM& Wp nk~P>G?;ɍK^pCΜx\*׼m㿥'=<ϳQ+kzjJهc6 Hck"5#Wpa˩oK^,_/oRyşȻmW~;>)W=nSL /wvAG8'kkFgAj ǂ ZZY ," 0rBwKʏ~?jt/Sz헾VB =͘0/A Q;f?_5qz"W %5VaL$Zֳ_n$,mo4;uz]3h?JwgDo"]<)?go^\MY>um}KWɏ~ip"/i\]1VXpx#P P P p?]0ңmj.@!.@!.@!.@!.@!.@! .T* XZ5Y\\tCEiQt΋bM.5yEćGjIH}{OY'BA(-Xij7+![ o$%?:<` {u-/eu~ tLA1-F m3gZP8VZR^SJze )EK }O{ZW>)ulgMJ֋?5vˣiTtڣ_XoۺӰ UZ@똕Fײ@QON'uqvNj~|K=yp4\ha©/XڠX3ؾJժ4:K04 (IU wd''fSl[^Z<{U}Oec+9ٶ׃ ;'q[f-ϴ=g8b m| 9 <-jR WXt+p48KU}EUZ`58+!g"&GYO+vX%S6N]똼XCAA_anz.&*ZIUƷ}`Oy=8[iJQmo?(dn]7ma82ʝp4O?m=Moe[kqY:6fTc?;G؄k.>CIIԬ/M׻L+"XO H՗tmj1`_/;fdMb2QYؓhg,ӝM:+Fh`(uլ6(ܢ7V5}=Ws Eݰ> 5֯!mMw݌*iKw$؏i:HWz}}.P\Ê `j.5J`})&:G_5 zRA~ Ey: >1/v._7 fEfdC^~! f17[)r/-wdWy-kJںDwir+K>>&hF(~P)~6?6J('e?Qo?~L~G=?S?{3YP=?ZM𫙫u'0k~pIxo' 'P.w7#?޳OsrزzۼNRjm|VS4 +f|תEi&j2}Utls?@sSAW9oki$6_u=N4`e7>/qק6Gr~r_>nK3vUcEFyy<_p.~nu_nir^+H9xpH;gg,3S# k_(ԒQ0  .FCopO'$!hyUZK~Q(!ٮZ( s[q3{|~:};5i ' Ad=)_NTȉBs>켥 zc[}i:Y<:D,MҮ w%\zWTEWFw{9ÎރzL6 ZtHS?s~ c%ky9OWNSnϘt^O^YBz)iia7ߵHc9VV.uDڣ#aUk~&Vu=T_'=_nH}Mb]z:F۩STO{RSմb/X'vٞXZXp7r͍8,BWy3nv'i?6 s8^ǭP4m^ZȲk2oid-:iҀ%ϘH~<ް<ס/OqRXZ-e5Z.#ѕ` ;9蘋6}r;X"Ip'htCfkOM&yH=i MwQ^jcraCRԹ|= \Wxt;\a<_u@y{ss53,!tkMeImS:X4'D7!4)Qæ?ݶ*&ؗ˟{z0U_6 |EAkzzJb=/IkksnQ kšWG1|Ft$aקop7_V'iǝv_Mʀ.1^?g-(Nv;l٠ιiJ3l?I7ޠ]+IαrM:i^kZ^]cwdl8/.Ϻ]tta*>ԇMTI}ӶESTh$j|bm:`V֛&zϮPrK׋ИW!~ =iR57R˳{RitOEyQ?n#l.᷵yIٗK#E׫!-Ӛ̹s;_'ߡ{ +8t:pƅ9X~ܥg\gtMzϭ=^Nl$ڧMZ-F87/i-q6+Ϗ:bf̀$oz)iz$y (zE鮞qkjcZۨaڦ3kEp`Kw$pDX)PD&DW7 X>:ʺ&îϴmǹ|ؾ4p`J', @|#jJToJ=B"I7ε6ۤ~`jp`\( B\(B\(B\( aE7x_u4$8b&\f'{u+*n2Eh0QRIKuF].ZRs͟57%CY)djq" 0hT:MiB̕/=]]v͏;qDң񂋡 .\Js*ŚhءݠUb:_Ek0d3X0aA[aM,0/?(\mF#9輚|aAƶK 0'Ps}=m-;oWKu:tWUmj70&\> P P P P P X#e?9ܵkĉ~ @._&ԩS~lQGf٫Əɓ~j2\Bp. .Xf+,>Y{6˯_O^v7=nO\3n_^-gߺ*:,u!y:Uw̩?8H#kn `ƅBiE{cY7~pM3m3u(noX*:Op,bXlz:&\Te16Z~AАJ![Qw[iDgX z 'bʎyK;߅Uq9>;ڲ$;cY+} Mϟı0 9ȱN0EBǧ[LT=ٳa.A6_2Ճ'ZnM삳aS 'ەYX6iɴySTyMvv$" uZRκ 08׬^YvT^i^yY`{A3zEjsIτ֪[9\w}WVB3#Ľ{zѿs8͢|@4D5oCŚ שHS-ђZO-H}tV64tJ7n4jJ Nmα5CZz kGnې_MGDO]]oZsF$֠sM7y~KmԼd mY FgM-Yy0-vMK׏4,^{pؽC Go|M9|ЭC.P h z}FX&겪_fAFUVOf`8*DY Ya!o sۚx}ȕVAEQә0Q YAޞd.Uz [/kCSq_Aހk lȆlFA }(:kOvYжřfno,i@8^dXgyJ_&y-<琂 ka@Gnl!N7Q3ĶP}V@oŊ'^wSSQ-,iain6›=Ղ0>lڶ4|ڟY\Д#5Cڲk2dbzFEpd3))tܝp4Uޠ8f=ws10HtdҸORt}~Ԇd7ZJXO;5s5\?FGqW˂­r_rQssO/׿O׆7|Ώ&PϾ#>9c5O\e5 0\8TP P P P~݅ ٳgX? .N<&ʕ+~ ܹsG>0>jGv!p|p~"fܵo"?E~ϋ|E"o~M8ظ9_Gq7nܐ%s0ozAp̸aO֝f1EАJ!̥UE7Z:=>tFEv0A.i7ey46N\,/o24,s7,wF~o Rou&\XamXy8ZRRޓ&;^A&RSU?gBFI1* au/U^Zv{7HO Зv< r|XwecY?H}Dz Wc?7caپ :{uC5,ӳ%wu`t/瘳pL,UZR M)Zl.e[ 4@SqAiDc'XDuKC#TSKaSo7YX6iɴySTyMvv$" uZRκ 08׬^YvT^i^yY`{A3zEjsIτUrFg4Gw7:{?Z0ʹ[}Zقv~m@1_.7\;-l NU?nFQc1Yѳ<)QKF:Z-iRgϫ4h~q4ǭ竪ҖZX[}ʦvOTORBF')BQ궃ҶQ-'yJI;u;?5YYjV]}j(ڇ'I8zE'?V ]v{՞grf=/>afy9Z~ ?tOjzfߵmAyHkkkgc;;&7!+RuׄAe^͜N̅>Dp|{{Mz΃Q>RqtF{O^tďĦtd9ƶOt=3 5غ2Z ZfCkW`[5%@ƶ [׿GnKByB~ZkH-:.[/ʯGiw.{`R,EGog֗:O&7k7嶬EO+wVd?-ȴWlR eւ.['۩ltYA+޺YOڻR~CK.'5+WXcO7l=ح 'fv<,|vk'lP:Ƞǟ#w~ΈD>t&ϏSZzZ͔c{[ Vó&hCW'<ﱴCc|tjO` 6^SQa|<F3܊0႘D=@@u`i7ߞ`ӎ+ER .J_ϗ=y5:(Wנ =$;wcz^'v2 2(\k,d ؍ 0舮 ?eCg.׀qWtx[ܑ5gYVCG cTYnk:Uco7x 8}.*񚊼f{ha'uzsDamC6nHis|ZF[uNPx I &mOCuU|ieӶ₦i% ΧՖX k4*bdמe3)T؝p4Uޠ8f=}v͔s10HtdҸORt;Dڐ]ksG:6 d#z3EMw*5 {)!frrZ/nqFg=ZˈO;aqC,=hֵ5}´CcxWs˟s.!p$jEfolߥ%D~I(O:D3L}:T q&ahl2Jp{Vӄh'i!ەZ֣˲ |s'&heʥ}[%L\tC &V ”haΓ_ؐ~ao/uBXT-_<9o=iY yk{?ooGeCnl1ul; Wy>Y鹧$~go_.=m~p=:Xa\!6 X cV B?K:!ʧɯnDx_|My0yֹFҮݬ}ٟU\`bKwv횜8qOM\:ߙ4*R [` (ȎUa_{9<TDSM3m3$}{i}^u8zN.yakܹsrY?rI?=yO crԢu9uꔟlr!Q+ S ,Lޔd}UQ,UsPd#\0M3m3uy*:]+pwVw ŞxhXd. :Bw|uek.5\|OʗE^[/տ/E^ܲq]gK_{V򛟕w6,\eOWߪTVؗ97ظͳeNV`q3Vs4nEQ&]sAp .C5NpA P P P w.]p`ޜ={֏ɓ~j2\Bp. .Ο?0/Ν;w \3{zykwH wn/ל. `]-cQG^_+ׄpd.w y~_2?\<ni\ TٮUE7Z:=nސY:*[~Q^eYh4Ky1qM8\KeV(s__s>8^?/7T*Ă]Uyy ~jԷdo".;*xYlZ7 ?%iwTb:<мď' l,k~c[Wcz^'t"s8v&QPxA$c9/=L% :]`|O\89:#B?mGwd=<\E*U Qݍd8Tm  @ ͋}OKyXSDʦ~lveEVb˒ӇiX^@ʆM C5YgEŃǶ.9<$e_1c`gm8,As$g‚֪[9\w}WVH3 }^Zsۑ|F^3tuli`T"V] 7tIDLҨ_\.wդV 3u4mnp--k|ț"?O6S7ZWnEïlꜮM?xƟtj`E RBb:[X(O xUsC Q!&uQbv[vOH󤝒i^YlEхKcU 6>}5?L`ls:溴.D;}=3yx?;雴%qW|m;k7e4n{Rv,vLnBʧW!Nt^[ >~+ۓp I{*z{8>¹|{z΋QRquFgMg]!KƦx3<#ސ\;-sQץAEK 덠.u[ WzSZ=~W;@J={ͪj5 "8Re6ldPT}-~I(cz FI~~d3Ȟ y,˲۽&S Rw&ܖiΊl'ׂR궡yik{KuLozvjp~@ntY1+޺NڻR~SKuD'SJ_0r(zö(ڃݧD;}Ps.K}Oe vzB-}3Н3"Ѹ|$ϧsZnk)5oYjgMe-v>R]CPG4,x^à{.cfaOr^ݮty?{?JwDYz@x멼&kMG,P{NK֝ wJ@=knZ>]s'_#.y#Gɚt06қ|f=wM'+'S*z7W3` QF k1mp'0*o&>=h;58?a(-SXӨo\rdY2z4`Ǒd$vlݲp&Υɑ7_O}|ܖsʝ\ǪktraV ^l%>]|l$+ak%];'KZ`kyIw;M6ei=cc˲nZCW'<ѴOd+|tjO̤ ZwA~ƤE5a uU_kQ <5&3=uc?=ܭb7woۚAy;y@S .+?:mOu1ĚF+kh{2W1lA oy49(HM{yjOڐݨjCo]=u? jY7yM/{ )j`'}=s) Gv$#An#e[Y&;M!5 {)!frrZ/n\*:/?Ǔdk5/#:~?> \Yz|y?{?ד]=-.5x1"zizTK)]nMtyLNg뚄'"W𷯖u>쫥:+Ҫ65kɻ'aj W92&{.9VQh`:- >}WZgu?1ロ;-Usޚ ̍\|OʗE^[/տ/E^ܲq]gK_{V򛟕w6, `+oU^*fyw^l2['+8 \|( f'B\(B\(B\(B; Gp.\0oΞ=YpqI?5W\!@ϟSΝ;}.Wo=u<;$x; 7kN0}(#^^/ݯykB82Eq;ڲ$;cY+} Mϟı0lscy:Xْ|;:]0|zfE9͐u̘JKV߳)@Υlkaⱔh*.h;͖hL"H.uiHc {jߗ}?lj$|"+[&-vy]vW5؉I.@c#DڹI%:n:z cn~zSMz]K?6z]Kg<3 5غ2Z ZfCkW`[5'@ƶ [׿GnKByӂ~Z ׼%im%Jp6X&ztz,Hh*ӵq#=V0c\!wK Y_< ԗ۲V4Qo7mCYi ˖+@Iozv700[9]ւي.v֓R=v坬Ie}M,XQ+,F'EYؓB_3;uY{B޺O6(kdϑ;?gDql :tG)-kfJKֱY-뱂pX5iGdڷ5 պ~t-mgt3W$N_uSni:y/_w\[:it뒔&א"Ls='%`N䂋(@Ђ{R 2\!</B\s'`5nه˟m@TM T&Ž離DGv@ҵ4#I By=EߑxʞJ v3O]M@\o ZmG%W O6\қRhZZm7-M?lfrE/4[{CϚ?HizI1΁{Bn>w|serg52Ȟ6mI;>tp咖2a0]+Y/kA7Wa4*Tk!wqJxvKo]+9Yyhk6kEltԂ Ν{|wꮬ,f]KAlR, EyayO ʔpHt_Z \pA~%ZCAitأ +Tpz+m|&oZx{zO\35Jg+eOQacM 2Y!Ş.Uz [/kCSq_Aހk 嵬Cx-`C]~ m[I\ 󻺽۲/y cyl0`=v7 şZoyK֞,fʹN$:O~2i'):["NmHFzYPֱ5ILieSCj%66dwn_WI&܍h^'쮯lXgd})%zwu5'Z?garIߋ0].!p$jE{MDt/ )XGuɹxcN튴MAFYT- n.z4wPhB~'vݓpX_ƚ1}*IݳףN˲ w59讟eʥn93|;?Gy-زZ6<6ofikKeg9L E-_<9o=iYBkK?^#Y%q_uNǔ^ֳֵ\%V6wde˧nړ} Z8h\aLysl̀26iݠ,zSt~,O~uJf"k` ;_%Mwi5$ fG|/4Kz59q℟0Vu31}T%O>};F)W^pAΟ?a7R єedL IߞvZD7s96rC:Ω0ƹ{Y0QWGv99{we9yu19{jOr劜:uO 6jh\_+lToJj`X)5%hغ25s LAj.>'"/{-"/yMn߸._=+_yYyʻz[tj.`+oU^*fyw^l2['+8 \|( f'B\(B\(B\(B\(X?/t@ 0 Ap Ap W.\c@qΞ=džw. .Ο?;w͢@!.@!.b"hHeqQPi^8F.jTdoφ!C.u)IG͢JRD Ui67F!ըQڍ 7Vd"5\9V[R'*0%7te{oOu A5ݰ3_ LZ "tL~>= 2\ COF'K0 0hUaD%E~\Qip5$TR 4H!͢ZuKfMaGu%iTҙ7 _zd?'gDKVup.\)Ν;'gϞSÕmEZ][bfηP!flHPQ\\8 `\(B\(BmQG_E Ih! .Ο?;w͢@!.@!.@!.@!.@!.@!.@!.@!.@!.@!.@!.@!.@!.@!.@!F.J#KG4Ri[A#{{{]R.6v8ke5ڌ-k6:,F!f3BJ u_]Rv]5,hIu{[ꥒԛj>WLp:RyJf=eiUX-15TE%" +&4=ZR45(5j/n{\}+Zԡ7rM,Z ]YFsU.j7pG~… ry?ܹsٳ~jң\8.@!.@!.@!.@!.@!.@!.@!.@!.@!.@!.@!.@!.@!.@!n~pwOb]p9{hYpA( P P P "hHҐO 0g&\Ҩ,"o,XrSP_<L䂋o+e{oO:CSQ|Xp4֥֮Z ]J7::aL~EiLF}6j6?Y/6/-m&NڋR;c1_XySFrS͗ct~%QpJƗ}:etv-UVÑpG~rk׮t'N"4dQUB VMׯM7E -nt]ؕV5Om=i{mH)X+^=fTͻO0]adz}4|CU-De#V~ 7]VN2eeh+ L/C%(+ͯQ;GC鹘0fT8 tSPO X`⌲mھ:o!gE.64IdPy&* X`bͫl}mLVZ|:͏H`ޱb"͢ITT V5VY qo-,|?@"imRg8v&\D/<4tu_ Ya|wm<7_ڞ֦Tr|G2=ʑJ~w!@_b咾KA 2`j.A&%8.ܷDե &NM.z 5Qɯe?k YvWv}5J/m1oPK-_lY՚+%:tfa۹>JId\̂Dc5}.+{}`A b-ivtk.5x}!P P P P '`{gocwB70q~b]p8,gϞcG˗d:uʏ Fpq Ypqy?`Ν;Gp1. .N<&ʕ+ .s3zK7l9SO=%#?۹*?O=x-O~Qrn?#{9<3* o-Ovy;){^-_gCWצ~[#ַoWCp05Dq4ݸqÏu=5 _Kv7R4$ QYZOhܼ njXdN3d$Fc(/4D}[Teq1QJs$ RssBmFE*A]U?'C|?Y~\"ٯlئۢduaU5+gʲ#k/mQfүЏ of?m޾-ʚ8Exk.Әoi{{?~*>=7!7g Va?S?ny+_ Mۢ&\T#mIm!m>۰گT$釋!\l.ʾljDXD .d%3eXp)`0kʏ {{oȘ=Oi`\XYFJRw48'KZpn囂SqM7mAM/ɟp]:bz}.н(n^Vγ!jZCM%fՒV*Um[?ڷu<Ϗ^skjQڋwz/ܩ/݌uѱ.(%z!%[[+⣅)J;s LS' iCXofz q;YP[o%n˷ܢķʭ3x+_y|os×=eC6fQW v1ۓ&M,"܄5A};,gnW+*?S>7)~9T D@ XĕZ* Mf_T(Opqt\WѺעa_6`AKZAIǫx Ac騺*-״&Nu {o2۲+,lK#-dZw3_.Hhtc3o?~_׹Ey; ^eov+^t󧓪5hyjIWЎN(7? Yۏƕtu]h}Z&u :d3)]vc`&+qX ;ؐCmj4Ѿ([%Y"2IV+~2n2I1O?Onޟio^'[w;9os=| J/t[s5WjZFsjnp' LbzwEu&# Z:u:t=f)͍c=f75 \hx).%L(Į_oay]tM4XSh3U~QMul8\hYZC HݿƁ ?zmb}.[fy֙"ݔyϓn=,G&G#|8-u?G2E-{uĉϟ%frEPRp8ƞik H=z sth/I7arޛMڦ~r˭&Wu; 4\Knˮ̔w=`Vi{ϻχ@jI+G3U yb'gO^Wo@ts3i|nMυF-CO8x߽nv_otMѶ*je3QBJQ~L:BYWc.0%vϲAuT{; ?1࢔,|aPaa\MEaOM\a0we\%?)zQbJK3 $_s17?LĂz]\ӀMњOOؒ͋2'sctZxMX3k2>tl#[p}qKKKa,w:KX8j'l56;ĶOv^53|nL\E};lW{W[dTfl~fmv]X힯M!O:`5mʒ̡g0vui )+"W5./w=`~T%iWgF,)I?#YUke7"5 vۘϟ}%eKws59q℟ovuW q {ZX>& .0Ν;'gϞSCa'O<è~B(p|zݖmOC(՛R-#{-Ipy2*>3Xu<ȓFܽmI|3%cϓQ1<Pc(6,:n}.pp|a&g>H#3NpA( P P ۢ_8*Z`r(9gǹs. 'YB\(B\(AC*~r,Ǡm[5Y\\tk#0'&\T`]iĊy E SגZ-m[^gH '(UԪI<8@J~8&&,$zIbM5iZ6z8-uYe?%ճJ,I߇2uOU>˿gc?φ(a=sQmJSjBK=ۓz 5#ͺJuv]mlhZyͪ`_ VPڇB[-R&OUi| {=ruzN~Q06\iݭpY!-4׫djͧZ'[ݦ#kYIyYM l:k|$m1k4,pgjKZGMj:Xb5 ( lZRߖNjm0&\R.^X `#h$V'%[f;Ú&Y-׷!s{ߒϜw8:ܠc|ob`Jp"4!ؕF5 $vb DG`MlJQ 󓺽CImvc-uE3\zYpG~rk׮ɉ'… ry?q99{6 e"?rI-=> GL7#,.^7$\{Uiv%~ώiպͮo_M$\9sqE"X@Kz?V.I#u~}Sf7p(I};%hfMc"sApy.! fF}Q@23?+\ M'$\, ` ,F[>$ʾb,}$]`Jsn / &Zca5a a}5%,P lw0p kbXۮ[#O:)Y&AKZVjw5 QsT̯HXϘon߈ۈ鄓!Gڼc=S1YƦjsOVR 6'j20#8.\$kpL8}.bb5Nc]|ُj.Ss1ʕ+~j'Orc&8h KcP_pC{Y0>4#8`\ǔ}q8N15_wܮgKF0+o cfQzy:ʚY^4j'`iimkR$ tzTާCNwqkZVgy"^skIK&&c2'p C7bttARJKͨ>raMdnWcWZnfצm_b_E,..4RiH'"k}N>‚\tIVWW/ڴD%Z =ۮk 0B0ɀ`"̕zHN>/_,?3?#gsm cZ#@ %)tTm 0ltf)/0 \ԧ>%oy[//I,ʜ ܹq|C__q}|~WU~w~io˯_ .T* $I{C7'8>iTe#ʛVM+8[o|#կ~|M~r0G&\آa dϟ|祐VP>FM5~ΘjiQ;skM>>YXXs{ncD`M, &D{{燦?Z%)@ZloץיY%H~O.uiH>z!9}kٝg^sGĂ ,o7%~[6tZ?i29|YXXhނM۰,QۧuD*ISJӞGOt{>O¡SC p@-Q?^]ľl%̀Tç<;z(hxܖ}aG眱6]6'QBUKLD Remŏwex E-6?pDUd<>?}l袧ZtO,ϫ}H0e_y&y\ZXQ Y{}ɯ=t'i^xQy?'-lTz,5mfQ7k% ;fFj.aϨU~cZڻZGˎaˑϊlˮ5>:sI֗ygKT b_mOy[߆p%ۢ~>S_'o]ʾZy򠬐(YSRYTȹߝ@Za׽N~~ʦֳSY^;+Ҫ6dհkf1-~k׮ɉ'… ry?ܹsrY?sN?6?zq?r#~G c)oBɿ+-+PG/o?^uo~]AٚXS(SOn&,](?azݖmOC7̡}.i`aJԭ'ٙhX|! ;?.hQ^צm-'Ep1o:]KRNۓ\y5yٟYܔpm:)`.cΚ5dbY n>E;R}.p`TyY1FnpXpq.0/Ukf-ọQ[ Qff<ۢ@!.@!.0vvv~̙3i8||-`&rq|\N>-ozӛ䕯|'daae6SПv0@0rlYD|ݐ \y衇\S;p~^Ѯ/s\\8k?Tt;B gШIԔfϙ_FmyXK'`d"p$*-nޞ M)Z-e{.%*$-i4D)j]Ґƨ(W?-7+.Wltf)/Gc= Z`v\8JRΫҴQۨuk6:-5Y}ZR54nuMVȓVKZT;`~i48Mm甤 ,aUk͢mV[k ϚJݧK"Nө;]U==:u ~~w¹s&X^g0ޚľ ŶGG~?sl-u6 GR q/-U_GayF W{`[5wM$ڇMm7e_v\C[jATg'#m[>k~Ncv< >Fz'w>G*r_"4\W=ܫ| 򀭣o*n{: uGX^yw>uﱾ Rp6nkkbA0n~pwOw59q℟#~N SΣþ^rI? w9uʾ1aY(P~<-=ڦ_h_4)՛RdgdE-p5' |Լt$u!iHCԉ,X P\fFE `rsAp4 `<4P P P P~`\h߹9$~D@a.@!.@!.b䯢}'p;\u]~ Y^Jpcb,fA.8 Dkss =cϽTϯ+d˯?|:l?|_=3O(#^^/ݯyk0f qh)UgJ uoi>dy{ u4qd0)r55 0%Yjoo150`bk+4#ױlz5}Bz :Zj^}ZYG][h=]Vx[-`gWݧɛs̶!dpL١R^}@"sro)SDm,Zz`d1벑ႋ} r à -ú cfdO$,[J7!뉻-Ë,9Gۧ `^p:Pv,j~y'kִJ/G˧e\N;(;}}ٷPX3kH7qMIVI)BkZ-OKbrf:)Oҭ0躁M-[ڞҿ i49N`9 z:[ТM\ 1j+\>!1}CSfzpΚrkC5p͟b5%$CZT2/Yg}V[cuUov=6ѡྂlHw"5бҭ ׻FR5Ђp(_G;,# $m<@ `/=ܝVNO]w姦~ok%͞y0#p8LW^nOS|GO ?Ɗ>dl±Bs<vMz'j̝pdmOYAXMFOBVMɜxD>ɯ\gҧX} 6nlcN\^ 18Gบ뚋Y6NlpX PEM58ͪYq@a.@!.@!.@!.@!.@!.bE{Cdaum ii! ^4$wl_m&jKV--`>L梼"+5H-K3aS )šp M+}`V@l] uYN[.j)~}gS9few8. YxH6[% ˲nʪ.jA:D(nu3%k; ~n~pwO>)wu3Kg~Mψ<$ [S A9]n~2HaZXT[+i_ّ5[ZٲtJX>xiy_zvm~*ң"VH] kV6esϏB~\_j+€AGdk%ߞu6ࢺCAGFP3,fryA5kjHud%Ԉl_[eCbiױ@|myM h!˽V3:T۲G,Zz`GyEN1벑SyNp:PjZ_ޡڊ5sݾ|ZVeY9k;}}Y ״iEZ MpcܷE-w;}+.pMŤY퍲D}#+coF #\o~Zq @!.@!.@!.@!.@!.bKwpO>u]~j|3׿Oe;yʕ+~ p\n^*vʯh{vQn<(0+8z͢Veaa [~^!m? 3y?bKVWwe}g_wewc$o ܜx1wʒ,Y+I_4KyL#\l5z_7#ic՚JR{i:0en^e|νEM,κazO 3fY#<y~֍EE> Pp";R.[y_"4Wt%ٴut\jˮe;kgNx,nw|͎l&Xz#֒_wg}WV;w!ۦ-k~zGu&1Yyٙyٚ.yu:tѠam%_%4)|ʴG$]>R9IY)omt}\$˞& GXgqaggmT|NJR"[WeOޠ\mKg,>Ꟈ|8_pސUnl;^ޕݨN: R.y"{۾%핕#m_mXH,/Hu=fVm GTgq$]sI ?^ 7_l? 3\L0Up`r+8`.@!.@!.@!.@!fOm8/_"孭¤˶$ړv[uXKT֪>//R{K.6H[͑ʧ.MYt}% .kf[%Y_$i/u;ot;uחt:}mnsbKwb'|R.?5Xޟ?yH=q\n^*vʯh{vQ\VpE(B\(B\(B\(B\(/tO40_\, @!.@!.@!.@!.@!.@!.bKwpO>pvm~,ңу HfQ Ap Ap Ap|-`f-^J{gG>v@#… ~ 4={֏(LQmG}?Os\Lp(S n{?0 Ap q@EY􈂆T* ˈm7~[5Y\Hc1EU}*ͽ 5v]J~aukf,_ָ_5hH-mqkF$Cf-Y]X#eYّ5 AߨޏaTKB?6c@ooc1e čmQmދ~*oRޖ(:nm7~"A}OJ[)+~\ ?Q`O~ ԵkS.fEqU4Q[Em[#i֐뼊z>=uߓ^MCVKZT+Z :YGZ}jHlX'?JRՃ Ζ`a'@k韬Ǜs5 -FՀe%iE.=Sַ&G_ljn%t^J xx^m<\*ʭkَ4FGoU6TY^ߑ/Yd, EAezT~Lo}^ i7z {Zm .t#+H%_xiMϨǖ~8Pؐ2# 82=kB)2Y(JuۓΠ%eH[/ʫ hI+(xUӳ@JQdA*-W8b(fHd؇A%)B)e 3"pQu#YB~Xӑ(vAyù MbtFMҼ\#F 2npQ;u5Y ;ܶVe5l_,a8S& A:Fu߰hJ*.u\ ;iY 3X^C$lV(qMZ-wv]V)o:zf'h04bpYXay]6mOY(S` ; 3y }.&>@")7}.2Ŀ}zSꁾYa5VᝯRt (ԃjNG%oMVmP| (ԃ GlX}$Es.H P P P~`,6cȲWKwe?,Nc#zsb<ɓ'\BppD~;/\@!.@!.yt~媟ȝwk܇s{O;ñ;{W.=< yQ@O.=..O㗇C<,P}~+\z.X`<,^$x \iKYpqnMGaow} vw& ozU>&B]<|UEqKڌx|~E\\7<|5-*@5 jqUyoK{fE%{ߧ-N]whxUC|9}ިC#X"Ƃxm=tBp\ dj4|j6\aud:?T,ux_i4w ֬ {'Z kA]Ξ]w>>=+]cMʮ~5;wSAZN9.xI̛Q ZiNHr]K$uOv0޻+};=W0B߇'vϻs~"pG~n_WrI?+WȩST>G\(B\(}.Lsq0 3 .0ۢ@!.@!.@!.@!.@!.bKq.SKIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629144983.0 guidata-2.0.2/doc/images/screenshots/all_items.png0000666000000000000000000010263400000000000017063 0ustar00PNG  IHDRyMsRGBgAMA a pHYsod1IDATx^ #}y)DIm$2IvmI'8^~:~![Ct98sx$F+:rWldoI{5=Z%8srSꉲ-Z"m5Gp׿P@7Pyz.Tֿps yU yw˫kk!yD( B@8K+/I,bGҖo}N3r5ߖkr͵%+o%/׿ُdPӿ!o[O˭o}I^پNZ yoxUN'?I> zUr-oF?!嶺!ovZ''OUҵr+r+ro˻oȍ_ڒwk_0!_#ߊwGk_ϒ_|B?;x"}0ߖ.m{Wk3_~%xK7ϾooͿ7!{kP_J>$rWp9=gOGavs,NPw/]I~}EYw߷ߥWI޿仦nR}"?o :ܓ5˓Jݴ:xts3NZTs__w<_ 5r_x/oLBޛ_yI{eyW˥+_/o[}}O7xEn2̟ B^N_bUgD=;ޠ7ŀgro\\~:&»Wt[/J.?Uvoj!/qT6j촫54>/O[-_<^t~}0~8"c"?wڼYz x&׍?w'+7Xʶ6WZ_PwM {‹~;埼)5N. )O3QGrشbTߍ3]տ͘m.6m=qW|_rx/-ٯ(79mk|KW!;>osO_O,%ְ_ok2dVg5OjKָY6if2Lg/8ESS+xu_}|otF{ۯ+W6"5,ֻݸ=ƫƉ`y~1}UW4l>ZVg/t"[} /qvA/W3_&9?&Ӌ.䙧_#ǯxRzG{Sw}V*Gcn4Կ԰.E/d i qtOp ym4; *mt٣Sq*7ŦFY33ESbwʧ^vy@; I]W5u])vik R뙾YXܯk~Xoi_پB˜A;cgώ|(bV2Ð yu?-_nA|W7^d7Mo#Mnk;Kk^5ڍ%=fBf!yD( B@ P@a]K.xEM@ P@<"!yD( B@ P@<L kR*$ciTdaa5c*^=.T$1q0rȋX)}w\ TB6dkkKey&v:G$K6M ڽοgڐ1M7$/gV2 gg!lYVUZo}Cn mm}٫KlO۾a%gJ|!e෶<%`LZWXZkqJ.YR+븿[kiU:jƉg5Wn2~̺Rem>hh҇3_z\U\֍^&:݆tͦTu-[f=SG46um"j3MWOVh{H,d]1kr6ύIŏ^?X;0} ySŧ\H;xkt''hJK's8 Ogu'ka/'e-s{6Ni-p,Qksc ǦHu}gCdeKL&˂SÕ} o m;l#7Zl+I3Zδ=ܲMvl~'aet;bks;m:t;^g` 4+K.4.fSNCw;|gc,$L73…vsaNYmV˕l.A5:gC3u jJ^ lajtشq-svK$6λqXd0]O/\NEߧg5?t}\)snȐq79.hkGf va=<o5ys(" 5yCM޾ڽ`Lȳ]|嘆fN[N; 34/^"P3,/.Rxw/Lf$Wq:f)Q ֝ĻPd N= -eZtMiڵMu;= L~ǺfXˉs-<3{=̪5_m̳kăa➧6e43;𚧟j'8` ٙ= T#4O{^ AWv գ=R\j3t{ppwPGo'Njn2~R״|muk/WJmZ<{*6ndM3ݳKS 1׹xB֍({mѵm˟>kxz_洳P]k/k{qvןNN/uOuǍ'q۽wZ~]77:˘,*}Y?~wcry5s:]n߅̫2l)؞HyV z~9mLMjXlGy@(z(9L-='Zj#r *?B̺k9{ԎdÅu>pniק; ljaG}M})Ӳ_ڰNIյ>leHu {ӇoNSφ{_W@|TqӋpje\1}wPߓu/hgceꟹӖ+ :M簛3i˘N{{:O9].e^:}Vz1l+ 3yR$aag%/V +6vjUߪfCD`;!-FQw741Nkre vo"4,Z_*,X+ף:jj54 c6HJ,ukX{%癶\7[(uqdMηK~I_ulrld[=m6xCg2-;8˒?k- ZCʘXt>gy &mal2C2 Xǽ;^Ww65_6-p)O߉P&{ulW3e#^ ՝wElg`; ]V+$ ҵu>JӱN0t5sidazo1[ЊZj`5ZҶGՅ9_}~8˒߶QN+mYAѧ-Wa5t{4a^ֱr& %=q[Э[N~;2Y/nuhuйT kW]=dC;/}.t(l:x2~ >:1ڏpiMG-:Wش] ޫN-;riZ[̏Eq6o^R"^oI iݺΓx1 )SCbyGŭϬ#|6r}>dԭ{N;PKH{=atӴVՋZsnq%5n&f̀;Re_<~gGmA?k Zt6qqqTtPzdӽ^W3;}Lzf(&ہ%],n5wN,uKau(6h |;gzHtfJ)~`\A.PÍ2L7gmIev&`((B@ P@<"PY?3YGfB) 쵭U? ymqB^QXyv;{φs&` ޸ӲdO7XM׼XCm;kE]hF卷i,oګ_cM!Ё+%诵ouτ myid,֜Ȕ3ey=ߴ{iM6e>#b;ڥUX<'jv^>^6ds%gcY{~KyOK:-Wi~ O _;y;;]]XI5  q}֭]WZ!ZP 5nbz]JZE7|UK^qm3j)NŋHuGcOc\{t5:~&F.uA|%ZܳV;ar'u~6dg-eA2x߸[{={|Semҙ^gWuQP,rd Oj VWR(vv YEkS/RWuQma5 0=Nr]:݆TZjZ8B5Ғlnݻخ`<=( /߳bYZgIkS8X;tXhnEY;ic,#1K.2x{^3Ѹk%*2tC

៑Aǯ#~_;hvۻ~=/ֿnsIl3 ˝l6dhvPc3?$² sC+/&g:7^hHq0Qcj[ALå ,q_WB G=Vi{C{l/4ile;r_ju^DwH~f+W^^鞷⚙ݧ$Ռ ;VnY}}WצS n6X6 z֕ɱ孯oZ{c ,'bVʵA{>9;y.5sq]T6i,5<\-ˆד-ݣ%g`8Y#:۱ =sMN5c\wrZiDWAe.)hlw/ jL^2l\,˓XO> o|`5M țV^dQC C[^5f6!E_g/MYr0٩ؚf7\ӹ5 bħ{ 5Vc)rx˲&f.szjmzbKZ >V㭸l'!kKZĆ_{m5 9kx~@g[8iǺNƬVjcYuͻӧ>&j^Fz޳j\`JYדo5vov0c~>kuI*kz&I}Oiuk"6=ܮӌ[vε4I|Ϻ< V_lCkts*HԨȂyB)wl`F6Cov@)fkcX;9lUomΟ?/m^OɛoA}5 ?ҧw|tn.\ mtq7\ :uot.}{9on/^IBcݤ=>vWt68EO& v^&a'5y7~'|gEGۢr s$[GțA<fNBޤ B <}Bn xw B( B@ P@<"CHN>_vq?kX;uo{ĉs&` ~k߼M6 /&6y׿m~ L3߾^~{E~{D;E"5j9kOG/0W\ے٘77Ga!逸uN#OkTdaa55)jF}Z'-߶rf?]YI9x\j=|y& XyNŬm7`Э>AǚB^X+5ڴVC*P-jVu,ܭȁ:ʺmiqQZͨ%b7XĻ5<<Xrסl:Y3ѷ}v^GwNMtaӟN.+2T,einirM]6͠M:JSl|-l,gy/FgDVl*2Ym;ُ%ƚl zy6koIV7:i)7V9ܤl˹vL?A+Ȣe +"WJƆC^&R'CVY,fP+R)ujܩUcڵ%qПrURokN0!yoiڴiBKjɿqFJ& hY,"…:.lt}I 6w긃m;VK-k)INj1 g=Q&u}vY5}t\n9.P5t;If&M$:OQA_[`; f=^ۀ/˥+a'm,j ն)}-_,JEPq_Ӻ-,';jl_~ ֢,P %k360<ؒUm{cY m-xq#Yni Z~y>`<=( /߳eiҩ"vڦD{Zl׌hc;A >~sHڇA3)m~uKbY_u* :gǰ4&CXc3?$י\7Y#ߕ6_}ο]ӰnݟGW lɵ~7ѳpiy_L| |-v&E2ƭ1 &Cj E277i;UlHr?t@ d #\W3; GźC]n/uʸQy->:?},L}x{K_Z)%ݸN : Z _SK1`sbذKrg]332puGKϤC?fogmt,ѲlӲ~i6- ؼ5p "}X_ߔYN35p6Y۰Mo#~~V=du؇FyAؐFoP5,5qpB9䎎Gl-tD \wC7*$m'_5i;ՌqMri]k^)_ח&ħeL)m;?I&mt6\mCILnA7u~>kc\N鲸-YP0WeͺٿDEm2IYH$ܩ3Zġe\2>5yժHt.Z@h5{wܚwxJs˲&i^ԝzuWZp.jA|? cG:ފ Y&Z.j(J9-m<[c^ tvGM3=lczĿn5!U\(nF/#ɚA Zo=ٍk5.ɷwa{'3v`}jse'O=$K*t:Hbu7t/CϩxdeҴ[n]f5~A'{8޺e;|ЮAfJ#yJ߸GeC=9)m~Yqs-UĦuq:V#Y0ggSj0u7|Ry6TKWmXNi쳷Mkzih)tl[jU?iwAFY4L^gû&EvӧԩSmxF6Covz)fkcnskMݍ@A8qB??^:ۺ>%w|N^m.\ mpfAᄑk{ӤkSi3㳻Ci/^$v&[yM}_o]^^\/7Uߋ~E{<%yKru׋<bgo^++:лo{mݬ vd5y`6[7)d!o75yEE( B@ P@<"CHY3f?kw)ʉ'6qM[7ywjXv_I\&\k:=ӿ6{~H{6mڮsƱvq䬖kgK^»Sgsa2> î#p?RK|t:(b;VK)QNj9 g=Qfu}Y5c鎹Mܲ3r]-ZL3n6hrYotu1:9i|?H>2/,HƆk򂲔{т@WۦoP|4*gBƵMCTl"a~z'1mnHw~jZzD8BI%9zSiAubM+dwX{BU ea3uHeiiZ~#ߜk06k5: ^]Nu6\{*= ֞1>)ߏŔˠ}ʶjlIX,`V-ުe7ttRe-d,abU IҮPzSS Z#RWS zV݁G®:eܨeR|4}p}Wydw>'N.:/$k,lmN)ŀ^NȆ]{=XonOHf)ug{9?;:]h6lYy/jC>t)k9໐\jm66dm6Pe$y[խ1lYMvAYf[H~d|^2FyAؐFoX;T ĵ i}-^Y.Faׇ-ݣO2C DwtC1*l_W5i;q͠eIN;Fy4^hħ8eSnGav^K^2lAu'.߀i9(ˑ:1hMzjtY,jdx˫ff"`bu5IYHܩ3zZġe\2>/>{swM^*RO:vjB^CmskRFrSNRZkJN|kL:: uwݔdZ0}肍tVD;4hq'l]qX/,.we,.;'md˷DzrV}MԂ$k> 7zY'nZVv)j|cYѷw{q{':d9ڵՎ<),,y#i&\MޯaƑs]*&?<`r7Y{em1y^J7f_o(8 > u}r5ys!o[C\f&B^PmJHPMQҬz;a "c}EI=?J5R:*w5+Z$>&j4EKŵ⃲Zѯ߹"!u\O.'# /uy~=f۹X:0m=۹E׻Yb߽i<mmg\]V:9hE J#unnOHϓ7RFlz:]Z-ki>woyh6m&\#ʵ;@6Qj;x5~<}VStx\; -֞m[-Gݺ^g<3(&}0&->5vøN>-NZ(ҮqN;u92+xl.mM,f.^ ϵz=̓ [pӃSwcܰ8J:9q?~ܷ;:tȷu;}~K>#m_?ۺ]pA> oM L^|lȲ'OYZ3k3~:9&2nVh֓Vs8G/z;Ea'm5y<gvRO]uzy^_sW~/~yK /M]m_/jf oQzCږku~6LV jf0nMޤ[G@!yD( B@ P@<'`L< #g&BީS|+'Nې5yS[[om Ix6EoCN!` +#)}oy߭Qcͩ_{:z1<)r\ݖ=B yO=?vAoC^" 4=IT0ꋄɃrd˷kWuR<)6ZO>{ g?~C\krFy|貦TfjHJ%[ͪz9p@YY=--.JS,\+x֒u;g{9 [:tMXg~~7k8N(:ݩ!,Pm=̺˃I 쥑C^Ԑ-kS]:PM:JSslsșm-"S eY^7m^ZxψXA*2Ym; $ƚl zy6koIV7:i)7V9ܤYZ݈o,kwD43cnhqyE+ȕa'mӵ>t9eUX+kJ*\_E,k!FEuem=?$=M;t%g_8Acos;eY[t^|돔OjS4oۉZ<3yϿe+oyU[<7-{/4蝌GwHɱͺs$Y;-o|b9?#igwj|OlȾZʸeqzHYkOmIgz]]uֵGMus}:qyg :eY=VڷlY^oKu[;~=qy[sq/83`orjlI9>t :jiSVAU:|#T_4eB7KTH<旾_}"y1&(#TC,.-搒M {Gg4mZ~%Ʋܴ ]ZɚwKѺ[ngg^}S=KOYpڔG44V,{%&~'U3rfQNKHvj9҇ fdׂlmJv!];0Ƞב[?į};]?Y_$E]WQ|6{ OCj24u 1:MruclunVcAA-[{n-Y lAk{i)>Lq._,lKƆC^݇* o-{Z5NZ(S^Z|XjMb@ Ъn-ݣEv2Ф (XE+>N ƍt[wSfyeSNj5SKq;-uVA5 ۴ f]{;<332puGKϤC?fogmt,ѲlӲ~i6-ؼ5t "}r5\5೜\gj6:w]GzNYkwW˖t.KH% 0O`emA^y>1Z\װWYK^v\jؿ,G\CntZWn,s#^Ot:p:+}.伓J&*׼R //uOD˘8eSӱm;%׶A .6!$Sq :1Xhmj@޴8]%d:Y7\ȳMF\<} hjl٭fnmkAi|~]4w䕒i,@t\SHJC e?mӘyzme]㻘E;jD Ŗ.D|A[q#NtC֬"wQCгiEߚŅ5tk]ZMO4Vs:G߳m9`<ςা|)]y\Cؚ.t1'Yyj|fCqggc:l/mG-FZO>u{춽;p˹lwU%OSNqX$Nl/mjSYwn)/:,c('N}[ˡC|[?n6? ۅ þ- )Өi/mzޫ4Z0Jy||Ƽ~vUmƆ4jfk$&|7Vzկr^+K.s/^$~\^ m?(ZY3ɇ޵-|ekfl aܚI&0& y(*B@ P@<"PA< P^<'oF۵ io9yE( B@ P@&YMvjۺ 9UJ )7dk˚aT٬J[0]c $heJR>QWsWXm_EqMuJ#Ro 5c9 Xk3H^W֖4?| ma9FœNSC]PݩؑC^4ڕd \J5<F6Z\j ɈyY/s#R83nX+jX$LkJ戆Z6?#Z).7]ā[}f;kɷΈFݘ>}ZN:~w 9~o.xk&xBEY0 yv]dfB&g6<Q#Lx!!yD( A7 VI&&4̕BfY,fUV!,5lJ5ZR0Y ya.s5yv4ի+I\׀MtmK9vdB Iۻ4Rk j`&T]{V/)[w 0@jvUAU30n9%ߊYpi9uo݉'m !yD( B@ P@<"!yD( B@ P@<"!yD( B@ P@<"!yD(vxגo,8}9~5\xP48] P@<"!yݵ&jͿʎI*3(FkG^((;&7=uo~w Bf e&UFQvL((B@ P@ yaMJuBd#wن7tYX(Im:+ wזzMY@jѿU |,, 7Ve^]2$K<^]68ӵW 9d;e,˙34̒ml*7NϽAw}k_f⿹ֿlǯW;;)_\ݪ3[MHve({IQ\vQʨ ڒv3 ڱj"j9rMtUJMj#TJ&;ޖMYY,FnoSojJw/YWN\dK5yqL<;ԣT4ŝK5iPj~RFCAYʖl>=3nF%Ziؾf wߛ/k e}3asuY__e_utϲ,mnJ+jPd]eHDeWJVAݵI+Y!* RB"G|j(jSW檳#WPP.GhA Hßf?ˀ֦tzaMa9wYD@3ڿR D޶ڵ{=wӷz9*}y=eՒMN>c֚f +LVd$ʬ({-{6~iF5ӵi,Rn`85nZiaQC^`q먶og"g[6=u߈*`}+x3 aZ$5ۢ`2(niF)FB̒Ɇk]4mj |OTh|}>#kg~&k<㚝b-ߠ~{ji/e0 J׾$[;;]WX"Z)qZZT'M^up4AxhtZ+R)G:\ϯ X:]__meY:2]__E?Ž_>#gp"&2xg0;? G|[ߒ}c˿˾Ku~6LJ:l&#: ί 4+j3:Hf+jEGj-jh, +TCC^,>|@ƭį]a<"[BՒMߪk.;~;z&YN\馛ʯJWг03o^lىxkkɠ7olƔs4Ci VM]GѤ/ܙ^aCؙ3Q]CbגDmvYkq1::meuoS>kA˚&oQhqЛ ju^&`2dkvbgGĩY0QA_j1~BH5NvJAk?:%{O<;SΜsw\Vk=2nZ>sFVܣ匿 )xkG D*ߌ2akv:>\)Ӵ_&`zC3Df'r3]qЂQ>vi9uoϺVۉFys 9~o ;)(_=|w77ݵy oMƋiHQ<옝vud;P6/_ىCd5yFCM^~Q⚼q&F5y3:GM^>Q&P0((B@ P@<"!F~NÇΟ?_`z .:DE0&RMB o7˰y lK&joBd!T!mڃe'2qg=yYw/c[ӵixiFԥlԐr3w A4Ѕ0:NVyVdAυijʲG>$vwI& g-{gZXdpaN_US:~ÂeI۷Nw:nkж3#] P Z EӀvj2̧ba~3uٕ;o~CXT)ZV*c,:nݮϳMzJN7A8J4Skl[Ec /Fe%f?k6*B+`o !yD( B@ P@<"x1Ogf .:DmY3!RMB:Ԩcw]Y/Æ;C;9a`aAMͳdٍг`BȂ4d˚fU3J?}_oݘS0JI"Z){W-SrxLMJQXZiWeuelkj{FMb<ݸ?;yhP>Z Y?LgϮn'qC5?`M$fT[dM=Z4X{*NRwR(v SYw~'JjY;%K(NDz[帻W8`ώb+j*l~h؁ |-˰sUMP{;m9]&tw,dP.G;SOwbVK:]냂k bESM#{iv0_ g#ڽS ΐe)Ѵ0r5pa/f CwvlUjH/α>\S]-*Qc561j4Di(ԐXrC&`L&/Qa iAu6j.-[=?kG>$fl?հyܲwѮNT)Yš;e7nP;eQvM+ǼI#svor|:H4 +WV*_20]yڪT޴/-I\UWC v+X~pwt軻5,փS]+dj ]vUvV :XWfvƋA 2l<-P6kfAEcG o.++n0\W5ve)[0}ŭP`<DyD( B@ P@<"!yϚϟ`y/gy3PǏYz!6.\ ˼Qb!ԩS 'N 䍈Ey\`fv;jJ#Ua o~S677eccC~~5ݸ(>/sZۿYg[W*~*s.p7SO=%??(w}=z5o?~C_7>DQy{_(G59yl7$}ZSO zB^X+iomɖo'LrnJ5uWnj(Ov7 FV]w>n\y5>6f4]62ڬ׹\vjvQaZ:V(iד+Yvkl5N鏾nFe)<A\ZЕj9WSKI=Llk`z!/ $H=B@ZyT8aٺwii`[Y֘r%v׮kx&&i Hz>Xj;%iRoVEj5;ԃ񆔛YtC̰q+5yV]pRM6qOt/5Y][i.YcjɦuO;eki3v0vokk}a4qe6:n|@lY³ }Ny1 Ż&OYЋM\0Ui&p];wP7-jw`nӵq3ngwK:YZ`o_9}`~Ïj 77I;?}wGر_/#~w徍s?'<_믿]Ϸr3]y}&r)̿'Nhaxܷ;:tȷEnvj<_}I*r9|ol6W`vQV#sɕ+W||,f<ޢ&찐BM7$o|GjlӸFMތ!Orǝܹ^YZK~aRwe5MJEt 7LJYsI9|@vCJ| ki[ ?ϑN?g1t~>虖 k,+6ek;\v?&3_1;{LZڑ)] Ey>rDyO7~3]3h;//'?IߥúY?{!FYZܛ@_~],O1`Z+}8:/vX46jt W? H?suJOԶZs$gMݞc~G})Ӳ5tv<|ܓOʓ$>QrT}!0<|EyB?:7 Gw|:;5;xBu]26E߷?yNuv19{duuUΝ;u~6~{Wt6aH?5dO<:`^ȋ"wTі"_2& _ȸZ/pGr2sG} KVe& o_!wڴ>.>%Ə%yǝѩX;]~GGuqҦN1'j6O`rz/R/Ց-r]Tm]Z%gRZ\wB9 Kv\ L85 Q_wTuʟRKqc<1$#vėq;@9u(VIi"TYpw!؇Jɚ_rYV{lz4W &3PhhO+2얋1wћsOѴ;!$A%cţG;-mZ==.iTk+,Bհ7p/Y?Z}gzkJnVJT>Gtaa꼒7^^D7Lg"tG4CcP%7^G~\%xw/qڽƋz'7Y/EkNɥLq\V0E{Vj}xkzvZPO?QRw_m>tsZ=3r7)# UvDѝ?^:Lʅ aj]-2 yժ`Z>f/cN:5^zg׻.B0EyqBoQ\쏀0i3s}M>bkLk̻qN@̲qBk P@<"!vF`zx y7ݵϚϚ(qBk 7~i7Xkys൯}v͟V{^yk^Y\\tpMތtMދ/(x;5\Zcʕ+W"7xPW\Wzjwnk90!/ITзlπdȳ&yr crأۤ]|HyH.Ve5tǂ']3gjaA|SiĚ5L{;tW_\ߖo?ɥonOJrI;. {TyP._;]Z홯E{蘝=}bHrƘ酼rه*kKPfJpծMTUzBJ#;kvc]LejAA |R% oj'{y؆;.rθOֹi׾eY|k_w+' o_!wڴ>.>%Ə%yǝѩX;]~GGuqҦN1'j6OgA7-S;m`۽SWfkrdx20,yv\ L85 kYz-)O=V߽^򇯓ҮWߔ~D^ykrF?'o|ӛrG]TR\nO ɠG z` 2<>NJv,?$kV іg>Jɚ_rYV{lztW &3PhhO+2wgߖ~\޾^^z\tE^,#=a9Mn※kssY;'<MAnAy(OZ‹a/>\Fłۛr|{\~cI77zh숆*y0)FYԈ_WC-l>M@Ŗa;^_q7W{I(eTxk7p&/rX-{\ * b}]FF91ݮai!^oR)SH(e.(lE#7ջkU+B[H6ğiMKmX34AUR욐a<>B_h$5=[cڗiϚcwwa][|ՂTݣnhQwiE ダtS[fG{϶Aa@.с>Crc!*,2W_WATW}7ci,u}Ʊ{Fp4dLlR[MRM51m0]i<uޢ=]"ciQ-]xmHbX4κ=&a{ir{Ѻ5߮y2O]kϽ(vv*ʬF/~mW`8ٕ=I-k^jp&k@۵PUqYfi5hAYL4H D溲MgylvmƋ4Ow5:m}Et^ m/u@s]ʴ㸦}$Nڗk̍qN@[!o<<<8!k P@<"n-lčf87^L5-lD0 y(9 y 5{, O -׌˒~8yDvb~|K\c?V;cR+UD7F-A\ڔfU@,0,K9-w}Qok0)j=R yATm]ZknfY|oGØ{.J3v9n6 FC~T_†49*5w];Ú:AK]Xu{chO6MiraEsw׶ے:bo$O^~5%o`kla{̦yY3 f̌=q8?k6󄐷˦(qB7^.+!!'#;7S^ZMXܭ5IӪ7i3g>1lz==lm!# ;Z~y~1BQ)eܮX`dl]Q{J%N_:쇭4j62e>v kuZ!#l =lDrl|MՈY7{.T޺n=aYKb!=çJ1e֮}R3eiOwjpJ>*~pP.;E@!#w,5NhZ :TOwQK 6m n4|>zّ6,c#~G7^د5ikV;lWww4-}~HO_2ƽ?xN(`;sl%UbJP= `wE=O9`WKG{FJgIPmhCSY~#zg`_ko,<~wNSA4 yQȳ4Փ]Z= +\g$h+N[[T6ų< ? :^s쉶SWm'4!_Z0ݱ797a9ww=>?jȮ,"4PT3݃u`{%j3Ԭ΂GNn`kla{&~ ,gͦ`̲qB=ByQjj8?@[ȻpoСC<`΍8] Ij#;N 7BWy^2B!06B [Pfs3|1BK*>bJ&qǍ~'~{<1Mi`|Lrzy3xJ"<ؗ֫XM֖4?| LaZJ,ZOpk_R+Sk')+"0tOY N&eZe'oj,1hƿ-? %L RIOrYMƱpYL--6u=a  &2s6u`{VLո5_Vgk~NPUZk7ΰnzeKV8q+wz4:x_Y@j%Drz_&o|&NT t:i5qM 3_SLBd? )݅oR")>#w#/>=;fY1}7USV5aڞu/vgv {ɂwQ YQ6y PB;%@{:TlwJM]Vwʍ<{>]oѐ2wN{&<3jNB^ զ",K'=jysvwmYe;e['`| kD]C S0f>Ydw J)䋽9xE~)>?]kaylJtmd=3k3;`ÐGm-l4OC0䩅<'<l7gw B@ P@<"!yD(~h~#<6~ yD( B@ P@<9yB) YWΟ?_!Ç|x2;D.O;toC .mq{#7_t y;3n<"!y;u!9r![g!9t#r#x6={Է}}y xG=*|Rtrǃ-z1 |Ǟ=)<^?Q+C oQPD<"5y5y;35y<v7\yZ"!yD( B@ P@<"!o(?d!9ߏIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629145011.0 guidata-2.0.2/doc/images/screenshots/bool_selector.png0000666000000000000000000002405100000000000017741 0ustar00PNG  IHDR8QsRGBgAMA a pHYsodPIDATx^gy#`u쨕VΩhbJj@e#5݆بKGP-8تlU U*TPlNK >9$g>;;{o&7;}}w~*D 痯C+|hbj!]}3}m*za[fY2 T,gKo_ ݲ<]:&.\v/B~\^Gx:_hE7oyn:zf v.Oѕ455E]w+PQ ʿ5O»CG.8v kM]:!Ɖz3ѷ'>O ./4yy-]0y7G~yla ^O>© 7|:-Ɓ'+b1X`1;^=;[K=Y-R-N9j]$@ / <Оcwɜ>O8,*CuN ]EAq ~t~>"[~/}G^B?KK.oOY6|6HácغvOʎS#NjxR9xq_oU#|&~YE/}%NS$_A/Kޯ?Mioћغy7}‹s[ݡSXvapSY`P*wjWx3c\c{DK>͸wEG::o-{a< p%C|^L}\IԯIva*iKjt;]˭ @'Χ?'Ћ/DW.W.;wa!M]NO]~Iq˽D[U|Ar`-Ѳd3$/؂t>p3>'m3*={}mh":˅t)ێ?%kNr̉ͣH2:e(kY“T>7-EXغ枵LZe[klckD^\\&mA"m*µW/ѧ vu^vzcNLB,{A>0- AfW:>g#a.G:a ezݎܓY2떘Pm sG"ºem?E|GRI_-ĊS\EZ y9sNn5:2rȓ^smB75=S% |۵Q[Ӓ -0(0(0(-w>c  @8  @8  @8  @8  @8  ^@Cu&ZR?mv}V>T P.sȕ:hB1s$}0?phi{E5;M!a t0>7f>xA$E`|ӰJ7ǚLmU߯5%KpX`oƧK8ZԵR .W({TχK/i3h䃱FH@:.*)OY1$G2/{&=lk3u=nװO'V=ʩWOĚEuTw7ת-hXIjԍS Jbn gmUAZHe2u>LeK"]:ĭGܺ(% 6+9aYp04j *,2y^j}&PƪbM!yMdAe?jX< _F[5拪,$(zCJ6b%V ,[UgOIY;o\<U5fHSYI3k.)3º.RGnsի>tH%09&rz&]4O^\b9oާhS73.uy,Ԉ\jI^}]ܦZ&qaKJse*zle3\΂7/ivbte h@yʗ}c+gǩSr:}<DyvGM59O ]9u,OH<53 e+:]?SWk ,Jqd1GbVi) Ǎ9(xZ'[P h(0(0(0(0(0{wݺuz/;{ͼQ9rDg(i|[Ҥn Ԏn9*AQ`2ޯ\*_Hf4:1A8BbE,μ[}o<lf:A-W$%YስN9KxHNRn3>u|D\N%&R'cٲ/FIA+TbKv x/HgD弶yKAJ0l`Y͖t#Ag 0vh"5Vꞅç1]nL ]iSZ<*sgƨZKkC!^`maO)f_/?n/'&y.9h>x w#ۄBtzwyVh O0ڇ}`T!B9dJUG^ݤZZ0{5@fg_ Uv@+L7 !bbB| H'%xnԭ6d+]fDo2r( 4Eo T\ZHlA, x0ȚϦ^B+D8A%E&ee_p dkÿm7E\qJ9?O 63׵bbֺM|UԨQ;Kl r䳸\N}jAJsq$ʊt*-ܫ~^)4#[$@p@p@pMK/=^T"л[s=st ,^@p@p twDpޣc=B(lg zY5Xlyj㷇ݙ5_-D׌C}]ɷ.ч~}ϟ&ﮥrsxD,'~FFFxEK'Gh4yz/Y۵=O ~Уq ګאݿ.THSΞ=Kz+]#a}zyOeO:׃kW^ynNVٙAYdAՉlL4.AJH|,`X,z>c:,ti#oz g桻m޼F N/;h:4[@`!R}9@ѝ9-e6d2`m+.ncq𥺃Y5Xq:Jg}K=!PF+Pqc+?ҙˀZd2-fGz @M/0@pMl`1ā@4#PAp@p@p6fq5CE`/^`0(0[$!8Bu8A"ƏӚA*a;%5m.IdyߨV;錗WaD`.Q\,/NG#>dl4/<$XxʹOw\\#E .w[tFmFT@L|A_o|*vl*FkG9['AI\i|[5rtE3*8kHEژ(PAħw `tRb9e"ɷ;P697 }Hio;sP[FŚfo&H` -+tgjbdyu܌8EP)awQP7Yl f;hֈYe K`,8ɶ;9|e零WGX̩[M/0@pMl`1D@p(8  @8  @8  @8LS߃/*^Tq Pa Pa Pa PazOh@oRBjf=(2y41!0yA`Ƭ@ĺ4[*HZ|V}*L* bi8տw,#T`[1%[ ?\Q\b9?Z >GG('S8ZS_dnEa ,)>Ε8걕p8 z.n9O N_<~?'[Gr:\{?.@':HgwTc/D|쮖բ|u,OH<-2 Y_Oqw+.Wj3if. JfnA_[ҵ*Nf)3 Pa Pa Pa Pa Pa:.u^v:=y=r>ի!Pe4䷤I?OKU/2mذ}YÎn9Ĺ5t{xE*Qc%\dc*?N~;s=:5H*km=+gqoP)E8{1Zh>=A͌_פE( GFى1ʝRn3q[3}NY| XǬD~ R(Š 3Yx:Q9GdRG(-°|HAg 0ܑ$&nˋV@Y:|=Mu˔`ޕ6ͣp;zaJ6 ֦$ZU+,CrR5@u! EMYθkqz|SF-C iL>;̧ y$S=: &\ֺAC 0 8RR˖!@ĮS@:PhDڒ[!bbB| OJ؜xnԭ6d+]fDo2r( * GFiGi\ZHlA, x2Tw~{*泩WJ6_PsIFx8|3(\\a Pa Pc #:cWEc ~ hU' AiӜ94Gm9 YVLD`A|6JW^ۏӦM(hqq%@6 6 KV1kH7G>V?NǂCZ+-JK9lt#X򪕴1:h Ԛ(z:vyN̤;sP[\E%;NWV&kjp8Uz;Ve)7ӱ+)Ԧ7A4,Y-JJvظoѦk;}mfi,R Pa0m<q1 Pa Pa,wq `0(0(1xdY>-_YЙY>kE?F;*ac&Q~|@k鎋zd"GYӎUFz>|+ieAΨDY8VŠ6yE\@`@ỷ,ߩiэ_P;i{Ry\\`tFDoCZW=LG<ق&7;sP#˧KvН~p;_؎MŔV u4|#d,Sx>"++GIfi,R Pa0m<q1 Pa Pa PaQ0(0(0(A+z1QE%s!Xn^T@"իW#ȑ#hD *,Ҥ F@p@0wҘ>tt۹-[nw([قnwͺN"StJmhfDjn9A]C:m-Dlj=V!upzb9%=@CJY=Jvp? C籞Ԙ[*bpЖ0Na.A `X1DEb9-=0+Ɍ=CY{փU=k9=S+qq2Ct`Lhze+x~2rܚ{ PW2;XAnsxDVl䲺Ed 2@Vu= ;<@ĕ,ba<گm#iG~cYӄ`1WEiW@!ͨu'CF:܊OѶ[z|5[5=j'^.q2^B 4G@ G)hܬ`ɒVM8Wփ)+=z>NDwXjC+叇ֶF'"G̱? xYeA Pa0eQ>"*Z$j8 @8  @8  @8  @8  @8zW;}3<IENDB`FT\iiir7>\r\\ܭ@Pڃ*+T%Pe*Pe@rU <~89?P y;dn ND~% VYo'"Nۉr1pRH$RŴOd$m͌'~>jh݀|N$ u*2.9܊rUO$Uv TY9.*+T%Pe*PeTQ 2PeD%ʈJ@*#*!?|@@2. H]SPpY QHSPW1T&6(+RehAqUeQ2PeD%ʈJ@*#*A=*z{{݊Dxv\zTn9r$##fYVۼysЫWB(Ry;v0mq?)n={:(2+++n;h4{uqR9...8^͖9'$?!!ATeڵkXwgf~~ٳE;.@U*K wRɳ;*CW^R^^.wc݉իW̙#qjS5^Slʜ*p7*gggϛ7>aÆ'wRz=to%T^bE6mԧ2eϟ_U~wq/]c/wr*noVXXTzʉWn;)fP۷WS~lyaVgŋkQM꽃ZMv{4]$8T!;{~爭[GkW/kƿ3%T^hQxxx+[o]vW7Vh .,]>>!5S[zv|QB?rd^Hg^sf*/\0""vW0A 벫{Vuܹe˖W' m!u1sōM:i;0uZb4*>sV[,)u쫠5uWΪϬ3$z=i/rnr ;s;?7f]rCBsϙ9+`kkgWdK9HYС_֤,K&ft:ɴjժG5hA_%wV}frɞ)_ Ӫ' {Oyn;Ln#ٳ;vXXYw'>C;~3pSGjٳg׮]* wV$pyoHunUv 4Q\g* uvTzCoc[yܿHYfe޿}fL"?}v9i: gΜ *ղwk#ן_?"Dvַd-Ib2_f-^;TްaC}TySE~]{\o8xsty4s$cLfZ?7u*O>K.7npw{L3" "Fv8g5fN-3SԊf͚:u*11>~wlq'n$~K!fwҡ`}1z 'N*geeXw*oٲ>V{q'n*?ݻwp܍u'֭[qqqK~MB 1Q0/qaHGDDoT>wG}T"Y`AHHȘ[n/[LTeq0nYJ#G8p ;;[ek4-Z 8PzqQ޾v\zTF92PeD%ʈJ@*#*UFTTQ 2PeD%\@GeDx*q r7A;IW'*YIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629145102.0 guidata-2.0.2/doc/images/screenshots/datasetgroup.png0000666000000000000000000020363700000000000017621 0ustar00PNG  IHDR/}kFsRGBgAMA a pHYsodIDATx^ $W}{Go`0HG+#,Z: Xcޮ/<ՓOE 2vw];<8~2Yc[7Olb]wIX7!n?5U=3]=]UΩ:Uuuz6ny]F`r5r{+@-@|ʟ~qS}?7ao*䆛-|͝ß|_9yWO/}OW|' Z>bbLr^)Yf>+; \e> k=S^KpS|ȿ4Fcی_WWT8@^/ |iwJ[^U ^OzBzIW+ɷ7ȍO"p17ewM`@'>&WLU)x O'zsw{>qҍ/]~%-&\o[-T}z2Oo]΍'ӤSA:.&i>]Tv2>]-~a>Ͳ6heLҎpܽ?^#7/,`Jx o?_~G~$oK'\ʉ?}Z^#)3h#9e7fw;{q7M#9t*K+EArnMZ4 5s|ȯ|SMANWNfbh>dwu|J}論xۯrr8t"?E^r7RΛ| O?Oo/Kge;|Uu.ċY/ݔ<{)~ 7k{8<PU3xe~B >6?e^^;-4]UשCy &L.7%oէ}IBR&s-C/~*6u T ^r7ro~\*/?7~cǟ:x$wG<.?4އ6Cvn]ٶnrXyɫ%05n%?p>~Gns_c'eߴ)plڅPow2 |xiTMW43!fo2y/J1 z+r՘?it+ӧ* cf-6Ǎ0TJa(o$TE{S~Ims{f|^nVG忰_O>&7~{3oM\~LE=q3M~,O'nE۹^zkU0t=6@-@^ Zsф@^ Z xP /j@-@6nyѹ;"7:ŋ;űc;|4Y,x9q"Ο?Óhm/1G}m>Z xP /jaKҕƆlC]IhL%*t[brַ/*`_-%`4 [4K`x̓n^]-f,Q1sFlVfdmcQ[tӞ4='ʹnu$I:WE{Sө4?K"|&~e}MG 6>8y''P?#IDHl0{ez t5Jmc3hGb4ؘD> K͊{W_Mwr}Lt4澴}w{2nN ^c滞ONIdwT/IqYʦ0Mƾw/>rjDR f>gc3&,o`KB6^hₚ] CLsAKQ9LҴWe&ݮTqՑF QlWN%-KAMXޖ{gY^ׁtb=c$3+Uuws6v0l;wwgȲ:7''yDr:nR^D IԖvc2o+iLzTt{.ڔxA{g5(? `O?./P:F#n2W#-?O-}@fU-?;c|Hʮ,5xb4XG {nvs ehwȍ/SԖ-QBkwتdf_bF=i#mmqС=cO]uoڸHFXO-SrM^𪷝O4|ڝf[&-IǦ= SLi`輜=~m:ޯA LY!? Xzk-wJFvxq֩e1?~_a[O\]dm>94֒]]7Ӿmj`H{|EL6µA ؑF/]fJk܊ AϮtzzm?Kôv)7sN:OL.D$:ڦLM:]6jb (|N?inuLk4C{9zڃtYolVmOK@cj?WѺ4qy=-ImZ'-gK/? ^LcUi\Uaq BLWKdkƲgZä!i<5zF^^:ӫ(Ψ-ڐhJ?6]b6{ Ľٲkd= 5Q.A 0^ɎY22m급b: :_#Ru=(S甠+rQsiOJJ4p?Ye4&ֻtk7|6wf[U7fi}$\/9bf?[zxtJ-uŋr 7)3MXرcn&+K ëX=:gKE>C^zegT>%[#O{=Zu_u8?ֺ {/I} Ư@-5xtGm0쵁q BޟqT{[.c9yٿ2>3맕R]u]:X%nIa> =g'¿ˤm%+kUߖ[nKzbNNQ[7CD,DD}c7O!{ӗnG^l2_]^ml-t PwۘWop]{N}tD[6!det= 㓡MrMz'+/`oQ0iq?A0(3[LIA[n^Yek>,wK6M^kp?SSzlhVj}u-\g+7ǧ[O/{A#;=^NeuP:Xh]}TǯK6WL>N;ruu~ϔկx*##Sv2%􏺊%z͎c["#i'ɾWLj#%_ӿrҥG?QUQ A':SzH$LH'-7_odB$'gyQ'FɊ寏kR]~zebʰZׅqf}b>3 v|w9z|_f7ӼtݲS5N;- Sȭ}e&`|ly<ˠL]2&ыq68&lyӋӦTEu32Mv:zf~Z>^u̾ٹK|;΃롟:k~A>35]^YjjJCX)cC![T|/2Zg|E0h<g4nYnϋWxE6N_:2ӡʭ z{CxUQ`ޞ-۝WX"iG*4mod{;}]D*}Ez%T-e]L67Nد:_WXe \ ui */ݬuVe!,cJEp{ՕW|͚o&I;kv}JAdmr-BeѱXiK^>:6i<2Hԙ yquszQ|Z~=\3 2Sk FY6[s' \({%Op,d|ERu KKi4:[]e^ޛ5]w"|g,ts.s2Ə9qJAqΪyšcdny2 !I (Em}Wyl:>؋z(g폼,=Eb}nAQ,.E(TPcڐ.{:o'r9ʪ]CAD:2uF{k2+>B$x|2&Mc$0 ԋ窤SgYF2Jdx@^'6{pCi>qz{Eېޠ#a TT8yjhy5;& ?i1 tۛҩ"W;ZS:+맹u˞nvm%'/[Fk,߬Rh붤/ UU4m*0wF^3So{E#o34fEL窼ml4 P;˹;_xQN8ƀ5Wfnm̛~חLp7pͭB!Ef.?}2[vcz??W7?^;ƪ;hrȷ([ EEm Mv[X[eriJgꊞk@-@^ ZXEpƆPӿJU^ҿx<hБan$CJCG mHzm%eo[-eM麙I9l[qCQŋr 7V… ]Ǐɖȶ"[vZC}G=up~3oDz.1\˶+8;v̍Uwd̍( ^l=3XzX-1ay? ZJ~g/Trԑh$-^uLl!#ldte~ f h(CzڊD>w盠F:mieZ;g^ϼ@@-@^ Z xP /j@-qC.^('Npc.\ Ǐwc<-wޕy9v :;}{9s{/NrcqY ^zY Fqr^'7}uru?'>wwFKTctk[Lm٤YFulvbN ^V105 i+k{SWs:N_lnuL^Zvjqf";f;:iL=7n#6@MB E.U}~ -+[Ibsi^);w9kwV^ܖHG/6ϖ |X)ft.O*쳩mv`e?3Exsgt~t+,wg{޻es|e5M^ie W_w웸'aC6OrG$ZKԐ6RݨeI^}+f"mq`l9IJȜ#ioOClg|5s1e2UYm37 Z^Ԟ0ᰨAV13aA.**;1 p!Y]%姁_Fs,IgND.|C`zVpGA.ӐNWQAT>m;>3E3m1R\]0WVr>9k6_t}^'WV5nMmhd@s(:f9kf9B'6H>eza)ӰdRz+zreMhĎd nQƋ6^kkџN^$lƒi>߼e9e_nŪ?ntAEACR7Whu %JYq_>3DQ^穼:t[T^ɱVXnY_0iHl>hCͣѐhߤɳix˘lF9v91Ō}Xr 3^ؓ+d'$hc4"!af%{Ev5MfFo@讎W7˪0\=ʖ+֢t[(h4|v%2+]}VA 2>eWrU2@~Y CuDT4J4(zer; s*8p?Y&>YE0v.y 6;NۨəW|Ssb/:m}%N0lt2OnsҫL㧑H֙bܕ]HlD1nR&+oq nHq0\fv-Uz ԭc6ln+=΃[T+TފLi9'7u8BȔ[+;º-{5\먠,[04k<2iٵ}cG[pBK>H) \U6^$Z{N/f} A>p=GmizR oL- ]gLaһzuc걬mفi{֙:m*7˺y-v-Q]f/gW]bdn F6ZExc#g81eݝtθަ uz'}gfX>1[Yq%qyEԲ3j6}}.uTXnE,bd\j"vjo+-pTPdFb_u[zxtg/ʉ'X .XMFFӧO˙3g"agql5|"ϙgm UiV;SNiOɟy̻ż'~Gn /?fڴgw?;v̍UwѤFn-lk-;̲a^ê.WIdHo<I;Rty=/kl=/8 {yy4nQomIs#0nq|t[]!xy_o"l۟^ϥojX#W\#);~Z0/|3شu `/xIlv%q[aVߌ/ImG,Rn )'߰]Ei,S6,ғ{)XVey=)[ΗoKju n+h5A6v":UgT] -ʔ[^ɱV6w~/ڶ efw`߾OQ D=jr;Yr췹tQx4WSeMFi *g^x|΍2'Y>CTdf_]ttBOzZ1z[LY;&>s7EMnsҫ>h݁& r-Kh ;*mO޲2\hE\fv]I^1qhy{Pdm[T+TފKېͩr<_On4kp8̔;)(X Dl>@Cyv&Ö5\먠,^Ϭlvxl.r o^ӆfs.MHOZY 0A[0PxT\w86x@xԭẻD}n:?Hi乆j*1HM/{M2 ɭ$j{YϴQHȕ=^z_Zlȓi^}=޾ۤ-ߊQq{ru [o[kݟi|nLmָ*ܽߚ;qM 2}!ְd}2[gv[tEu k)cmjk|Zٶg\GejoD:Zo"˱u]g[J˙ȭ-Q_vՑQa}4Uiw+870^\7®'9σY滋d{^6nyѹ;^xQN8]pA?ƖLm4a_i&>i-DӧO˙3g"oql5y!]oK(RvV7k]Վ{ԩSnl?Xk7igϞۍy9vɊ=2~x^T;Z-A o[m լ;WnaEzV=wژOCz;Rmk_M[']ð_|E'n=n<:;+_,_'_~\?.}a~/=/JkϐpS?w$/~v:MieU4b@=-_V ^ rP ϼ@^ Z xP /j "8}{9s{/NrcqY ^zY Ff/*~V9ĭ[sקR Fk7ȫgto|.}S3/r\I2!xȬ3/|[S%JٕčVrlllء7㋖}t6le޷D7eI)diE,P#s]aX"ݦYîms,}f.]e]e4KA.,sM|ӫ6Z+k7;큌FTAQXy/vs"#Y6j`^l<;4fyl{W@M^zԸgN~0/;~ft艴aWn3nn&ЯTQe;Hzms=zp؆4z q}1Jdsw~&ݖ]~u7~8{1yT4m3/v[=atio3Eޞ\sn$CU/s&2_Dr'1=WyNy*lV13ա=UaZ1ᤖr룤M#&u J$a7&W"+k)_4P75P%L UV M/ӐNW|FQATzMitWm7.GezC[["#[f+LǷkhm/++8.4pnOmCѾp=urm3IUO_h٫f_d҄]W>}Pz}aKL.zN1 9|~ydiO<\IA =^'S'\>ݸ9;+WYQ^ qO*2 ~VhSwv)_h{[NJӲ㷈6iCt %J#K$#Mino.U|U?]= Ob?ɼJ,NTКޓ}!ְd2rƧ-?k9cwjk(J̶s6²tS_^G!-wf0O=N pLT;H~UgFghV]e]rVuۨ+z쾜x..uy[zxtҥG?QUݻC0茢3ѣo~{gk}Fke*s}ˡ]P~:>z#Y}YmlN:TP;S?ѧ>u'04hڼ5W"ޯ0Y ߯WgKZ)}_0/쇃 `Qkŋr 7V… rq7vi9s@]j=r)76wQiOAn|y)O.ɗ/~qyk_x:{}n<˱ce9hRg^Tȏg?˟;? ;{4MQಪyYcy/BԌ/Z9g^ Z xP /j@-yj?RYK}>}ڽ@ݜ9sƽMSN8{, /e=VӬvQ ^xX#tnG|{VƭAnu)X#YU{|dzEnV:{u7>`\rEpPkkdV|F~>`-Y%JٕčVoƆZ}3H&ui,]Ϧt[%ݦ4]H,[v:>UO+8x+}|פf/?gpIT=F'-<`Ov"K5`$AGL[idԋݔ%5t HCA5$t$o?,q*;\ߍ[nA?ӽk[\TkXtM]7h|\*M['+rۘ ,sB ˽vʚ6ne2>5?d4;fw@˦(e\~S|>6oכ`z V$q$Cs.ig3;ܸ-+,4pbMZ8yۚ:mm]lmx%iyU.+'Zy&hHыͳeGxa,Mzr3eϤcU S6g;?=]rH^=g՟̮ð[u,+]IZWvԮǫ{;x MܓIgnm*^~ Uç|Bwy~Zh0(Ժuٺ2gFi0U_<i+ܮۭL^-~;InNJfCʹ|s4WD{7iDAzvl;l7}m(7֦sl sƟ nkɺ I7nte//6ilM^he7D gEfh0o؞@O*g͓ˠ3V [#jïo1R\m%u8)7[?f=vm>mR5h>?yeYᆩ4V=MIlƳ f*{ne ?/S!9su<_5eK{uV y"'xB.Ҡid50 y{+{R1'Y6QLz"r'w{eԝ+[$m@eғtGhCkDE&uLz{?[Q3ʱ˕ԟZdEC壟w<(E$w6o]GVPg([笪YZ} ]OZy',sy0'+J6:;ߨ0A=Fz^"}ek zYAcd&mP*MSYѝ:R,²üsYt-7W":mPJWiUPu}z*)SVnny%Z%ϧ~/Zcbe67[߾OQDZ&FU*~/W+&ZǨ5Ug%syYfx :7ZH֕5m}ۧT5RߕY1Dbp6ӷ>2lF65XF6's"u϶ $=;ols=oq nȢxr|Bzuw3C/s똧 [zKh6i]c0+e-vY نlN]zrY,_n}v;_P^ٱVtU=#6 ;ma˚|.uTPvg\?v4]=7vD?9~kGp[hi9c7E|FU yu6wuYe N* / OլeL@3n:?HiwxKA)m%[L@Џ۟LC#>mp ɲ=>Ӗ6Rޟmeʱ'-mbżz6}4s՜+YnaY{k}<{9[M -.~yG_26K=YzqM mnsi^.Z5#E-EQTθܰ~f+:֦VE})e-QaYf)/;m'r|]WoVr&rusKv29^Jka:eui{^6nyѹ;^xQN8]pA?ƖL4aЇGI{vvJ?|i9sw?nafWky~(~`ޖP^:  Z xP /j@-~*9}{3gθw;/LPߐNۡ'86nyѹ;?~E9q[} c@?f;}9sƍ{7Or17VGzeOh f묖Wyek ϼ`.z?"=!xP /j@-~m᧒ T2/U,yژT2G^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^R]CsHlve%/HFDQG v@usۘ{cv4:/VGcY@Bbf"m3[b :"IK_@Q$I =7tK2e=/MUI4#jvgh5o.tX L:$ &>xH:)}iDz&itܳ0p$BKl˰[L#1iz 4[fgp,?x1`ЖWtZxۘo{SŽ4-G<s"xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Z xP /j@-@^ Zظ塇Gl/^'N1ӧ;`9sƽ+wy9v  Z xP /j@-~Kҕf+8hX%nsC6 eô&V#xIv.6ç\ԑh$3(`DEčDKmN.蕁qדc7eEgtkKZ7 kA{jF2it2L)wE˹6ڤ}ԗVQ[ ȱqC.^('Npc.\;~["i[эd0hD"mwUj#A\ZM"[;f?Go$=pjَ4L:WҖQ{QYQI\ U'p3/v 6x*k m626X_FBaɶ6TFXV{R}U?^;ƪ;95xtR訪]AgEFDN:22LΙZy;O[1ewS݌ybkC?N$s`C='3EQ˙%Kw 9xId['|ZfH b҉2NOdh!;|*>:Tq! Xu]4W'RQ,>Kv_͔eʔ,;2ldiOр7x2֑vxC(WS_ՖPZA1=9.})&4ƮF=T?\.N,g򊪕mo7u(*nvTEoeZ.ڔ8$ /Jr[vVy UhԦ&<^_rjd *]6 wdl;Em ̠0cG6xP//j@-@^ Z xP /8}8<ŋr 7V… rq7viA9s{:T;/bBۅxܻݻrϟcǎN>%А#x9z^P:uʍ-ٳgW.x1@&M.߬/| 2e0a0|xp^>~|#}@>Ge4\V  ~>g>y^&x+䕯|n|/WWg']//||wlK/\V עzr{7q{\u=,W*Z%JٕčD h]Y͛oetSIff&.j6Rcb5&-cj%~mijnKoIwKd{ЖkhPlmJ3i˨)|޻E_$_ ` *,3G}imQ?ONE =׆GSٯ{Ukݻrÿ]+Gݷ~뷺wkcz H>i{c8->~~?>DnS㏋g<.Ujml5fw^J4o"?R? O]XʕⶱK䶱!>譈:iJM/jʰ=0 . 3o(M [oݪ>ߦm€.,-ł[;* ^P˲nK/\Mbim6C^4,Un,U%x|>ϼ|hq)mia_ȼߔؾOZoo|H+@lܝCI`a7i4~+02F2n`ͅt٪˦Q[킋{q=Wb~fp5=}_;vlOHk5tZN_X؆vŰӷ${L#?04}~D-v[ң…ѥ;/WN% fdzuϼ‡( p=pϼ==ʇ5eٞK^EԞzvXh2yVew{ 2l\3~#OAZQܿ:z3)?o׿us>$u߷=-p Om`?n;zvƴ_\2ߞiO{ڮ^ 6,z#x9z^Py~#xXa/ ^"xA]hr^V ^词@^!* Q廆/G3/b*x1@^ Z x,nsC6Z}7~K66M8ՕtO2,}V'x'Vs2^Eh$-4z2n,q l-AیCˌX}O,*|~~euKm`E7 =sZYK_N{FcͿgҸ-g=s쎰`J #g4,}*}{ o%s  qόg^qmgqv|*1.sW"ݭɷcif@*eSͣ%-U˻q^D74F"%80S! :f<}pm 9W1ɤ6tK'VG ʛϷZ!`DO(>3 $NUԁo3CLH箾+\oNNv˷D{M`شNbsL;S'Ȩ0 /I"3״j:`U@c~ts6ޒe4>ыzqqyOzϲϼD4L`ꮋpܜ4 әiޘWMui[tI1M?`u@cIak=$`ﲲls׻?[॑mȒ-cJhֱa"IXtl#B`Y5N-};xKm-^jsVW6'6(pKx({E֯bzX"qpR.oo1z~p/<9 G%u>ǻHʮ*'Ѧ']UV=/|oYEuW[zڲLJh̉}Бa`O1e^svy~ɻ dj4i4i6ԥDz׋P[gNCz?Z5P']3ge*DŽU~Ays|~i|o@mãswT?]xQN8]pA?MOͤm{QDM7}WN>-gΜqcj8}gWcf.~1q{y9vɊ<_Q{[:CsϻW6ZCl׿`q}2/kॲH`^_ $˩}>?5@x}Uij gXϼsPϼ8/b*x1@^ Z xP O2~*X1#D(z$xsP @^^4]IBGٲlll[[inl(G@rj 3hБg?MZPn5z^X- #!PҨK"En7*۪66ZHՑ$/!LiAeori7ͺ&;ݕsoܜmM7M_֬<'IPa Fo$Hllw$:24-dݴ^,f'vF3}K4Ҏ^o%lwJ,=awʓm6[g^Y]c؏ڠ۪Lf,Q?ȕeƂ|>-ķm`fhOHȰ(ݰJτ:^zϥl hI]knڲh$I zBy \}~-'7X_Q-gAt!#Qd/IOpoǢM3LLc%y4{{}pyޗ-b;};[6GleWyu ^ =υ ݃Nc2/jK;Noٚ~H=}/S<^O)׬O 4i[Ic^mmʳ.Q}oX7<՛/^'Nr.\Ǐ1ӧO˙3gGGs*]c|kW+lϟcǎN>b FO9Kߨ+*?][_ZRjg^g^@-A;Gף/ N7yɱ3?mG&v@9:qPϼ8u^~alALi@ *L/{V2?pP  e~q,ɟhv?mSh#xX#ar('Q$ 7:N:2Ϩ% :gcU!x|Cq_d/ICv!#ݹtk:>y鋰6•C^q'rwu~-be?<1_9XG<ɂ 0A=O6xA/6VԩS]ugϞuG , =/beeyN*X`1pX__8R^f1:/8JJg@/n y?W;A|)Dy8(.⼱w]#mz^¿֪]/Ш-A{t Uyc8o8@{0uթkEӽM3cMjS7KWʴ˶.HwkG{խ|fK3o)ˬCaz`1G K3o~Le|T^´_dtU˽#^zemJ~{Mt~"X[wt5u˙O7w]+)3J,ғC$DӴX!$yaot4Svn޳1+o;Z]qOi,},c1{*1Eff=iX"//ˡ~9Oܗ'|gŲcMIâo|f9ӼICb< ,y+EU*I=ٲt+`QG1cQ J̗fK<'N 2s1'adoJl'0'H+7 y|T^QKVOF>%+TEQw̟\˥ogUg==Vͧ^nW99Dzd۬yٺήqYe Xyо7P?"ҳ~׎ NCz=.MODmi7&l%3̧]>mY!vg իi㓞[vVej3LSoeuS g_?\ T~adF5zg@EU["$TyƲ'y燢V`,@@^ Z xP /j=&9B/Wcf-o<-#xP /j@-@^ Z xP /#CoWUu:^kʕ+S?S?H~y{k_u\˗/%ַ5\#|;e/{|rW^{w}nlllءw[ҕf+Lև?a'?)ozӛlCIղ髎76/7 3hБawc #zlnn?GɟI'>!O<}q `޶s%CJCye0h=t.d/裏 ^x}ԩS}[_Z͎$]rv^nKo%kZa{Dne4ʹf7GtC7ө;6O7ߢӦMdJW(X)!xg>Ӿ?m_nI.]dKoБ(`4у)Ors%Ilz@aӐ1CP"ioO>4[b2/>O3b[:CiM=xW$v㣁k[ `mizm_t \4 JڱߐH{E h9.X lD-azZ0=ڌ%m9V^^V7F{ ^kKo{o}zk_k_?|~quowkaEaF@+vvvի򖷼EY:=ck:M'IWZ b҉2hxD&eGU"I&i$q< D,uGX[{ĉ򶷽:hG:u{bҏ>ϟ ÇM}՟^2۸塇G/^_U\pA?_/O|c?|[Ŵn7 G1 mOy˱cXu'My7 H~'~B^{^y_-/zы쫎tOp$-a{]{U p`^ Z xP /jJpv8`^UU88QG{d#:s{/NrcqY, UYcu4qKJ4x)khu<`q~3plq|9n#ĭyΞ xP /#CoWUu:^kʕ+S?S?H~y{k_u\˗/%Kҕf+ށ99Y"ln|NU׻ߒtpַ5\#|;e/{|rW^{w}nT~V'x'4sâ'P6ܧL[idԋݔZXhHg`|򓟔7Mvo|m:M^n+"HFnIk5OOP$j`Ж?$He=t{ \T1%dssӍtn숨qJ/;$'%Sd`}tҬ3yi*K?eS~? Lyyyp}#ٌ#Vf=ӿk~Ѝ6U}1f_߿+pZय़ "5_؍^zEtJk-J'>Nõ3U?Z]h`[:w4puk)]m%06nG*ϙn4z/ɳ}nu0Kś|F8O{˗25&0?/|3XnI.]2L#~!k$2,E>:j?pV㙗({;Ŝ؆QG f,^!LGӑ#G=k$Ĝ_`]žfk3 bA#J>E+7JvL<*0'*']wh1sWKqˢ0UhLaSZi;~EQ}XRzDHåeNjuX҇uEz}}U\umE3AhO^%Vn>yƲbK6mWɹY७6E-3WCN_8|qΚj28fOK[[`4Þ{785e^>Wh|_U|p}aN^Upw?yc=0a[LeksΎ\zMɧ5e}Ct"( b˚UjE8H+[5mAGæoҎ͚jN3]o׿`m%˿J xT2lWχI}Bw}W:eXԖv3ml=0l}'h,y{8qBٞ<:]k:MK`)x[&˯bjV9~mãswT?\x~Wq9~a;}9sƍᨛu<<Эy{?ԧܻٳgvc54wRV3ik]u=D<??-!~V1q ox\wun)*]h?9nϟ?/ǎsc՝|4Yi.*joKG{?V4pi Swkh@?׿^ʫ_jyы^d_u\|X}/񯔭Ń̑e`G>%-o=U om_{g^}~[g^p8`^UU88QG{y@. ^ z̬±:%x)_0*[ߨy[~m #xP /j@-}H7+^ ov:PT2v9\rE?g}L677۾x| _ߓ ox\wun)*]ZNǽj:`NT2ַ5\#|;e/{|rW^{w}n 4]Iܪ.r`և?a'?)ozӛdccM7tP@Ⰳ K{TӁ#x^goBmooEm Rf}vcUbFˌyEuP`U֣>*/x XSN?A7Hw#Iґi5Y;uK֒p];5to\5M^:mftrT_XƸ˯xZStѬ7ӽ~ۜcHo$H܁Z49rP+/^gr7t\tɍeEHud{n6 +iciԋ\+-4\j40yd&@jN`:^ʙowc-'XIGVfQ{y^=(8fq:m 8͟n,H/P `miLhS dڮ$hw. eIvL,'M^9lcoph~^:ޟ$1ӰN~;)Y `m`X|?ߍ)hϑI{VHgjxiG `mZ-w\^NӗZjܗwi9m@#*&im~q< D,,F酁t,=?MoU ϶)ƪCX[{V~f?yttPԖ>7WՙY!fzy+iW3 wf3 ;i>ءף>oϝؑ4n1^0}@zrQ>v9\|Y~zozahz̬Be??ϻ词5;07_V|`7k%$x|k-s3qOE/Ї}џC3.zXi Bcf.Us/Wcf-o<^yP /j@-@^ Z xP }@aX / ^m @-@^ Z৒w@uw}{WJsP?L ^N:ƀΞ=K,A/6^ Z xP /4]I67dw`mdc)E7s;n7mYyK~VλQ"xmdoz> bNIwK:zrb-!Er=}S~T>s Hv`_8Z;ăF: E mCd_ֳ/ݎH}cu:1&/yj?d.k#{[<FҐƸKO{|{&=3;2=6ECtZfI7m\n&/MPe}Gl`V>a-t356U~^~V$q$anex󒟑oȋz=z+=fvd|kslyיeL{fH\n_yi5-o6-o%WB2/]#ח%woo):MiǮ>iqOK:Įg6 ffG=7}~Dڶ7Gv :CiMoN^]3,9Jo/ʧ,mzD{|[ʷ%j4L8tcURЏʉ?*}Sw)79b4>vy ^35~ye6/G6]E:iRmoQK\ڇ~crԃ79yR9/??ubVKݖzh*:Mii۞lud|GU#Jq= g|mXOL $;&ENʋ6c$ϫ:dST <Yۢ˛jB&(Wm#yM@wM`y/-azZ0+^)'n[-ޣPlC /㣞aC0ݸ6 EfUXyi6JJy ܭ] p48aOF űI_v"!dƃѦ $*>/K됎Ue {V' *䳫f?+؇J4yH~c0)F~J:'i+_9 D,8^yP/v_-YnNtdrGG3}ywKi_ּ=C؁U^-.ѰqCQ$qE9qڏ>^pA?`X4C;){tLڇZh??r)7vYXUh~Bs}ǎsc՝|4{4j-XA5N/QD JY]/I[Pa`&r xP <}ϼsPϼ8/b*x1@^ Z xP /j@-,*,5x#xP /j@-ԟJ֟AC54;/b4gpTԜ e#xEw /uo6~MTys_FF񯤃.%^jꏾ|/||qxJ}~|` ԕ+WH k०f/g6- KM ^~ϒɓn)V'xIؐVMH[fZ+7dC!Ҽe/+ 8V%%v#trNMF#g@1+t_|Y\Ju><&|v}r΍ rC&y,__7yRKKխAyͬm,8KۑN1QBg?EŽqMHtlON.67dAӗFSޙ<ߋ3N{`tD큌qpu{1yT4mw]Q=o4;wߝ,rKr 4ϝ_Nz/SAS긎{{nKӕuH{Hv̴4fF M@ud0ɠ*ic]db^q[5DۮgҴEoK{wfOT4mr]M[wi 9OQw?={}&H{);oE Suyx=4ntI~\ Q9SArıL*Q$ v.SVDl4 e.3b#I3m}^9iګʽvfZI~{K{_!LV2e }{bZ55w{]ťGt۫tLxp5UG&PK V0x1qD[o MPQCI_esJ" \Ly\f4ޚv噗1ܥFjVB]&Om@T-X>CMnm^}.4]WnթL9&) ^$j!0bb4Ooʑ-рhh;@DyIf:l22IavbȶA t7Ai,񹤍}m?`&gOHV4Pԏ]p=%ظ塇Gkx8q;|˦vA`r;ƪ y?ߍr_O0'GlF1~8jh6i&ml/'fg&4˿~rAgu40؇=@#L*[4Ve=/ Zනԑo1 ^m @-@^ Z xP /j@-@$]inlF&-3ٕč Ioi^2K՗ցzDît}$`0h<4Ho4z&x _]BU>NjvX$q,I~# `%cΠ'~{㚮Ց$؞f]mnȤ/tߥ3-iyg-V : 6nyѹ;7/^('NpcΟ?_9 ܻ|einlKId* &D4b|If `YoHK{U1c Lw]5ѦQ$fps{ϻ[bΤj_kN~ WTSg^MF2Ϧ y}%nű\_ @^ Z xP /j@-@^-=<:wGFgx8q;|.\ǏbNr;{{8H|g@Tm+m;v̾,28hReۼlU^mclllءw[ҕf+Lq`pҗVk(HF spyG0$CJCz\Ԗ-o[UZ}s;K_Z͎$]r^rW-eꅱֲ {,Y6HLkv\nhf:u[Դqڴ+q iʮ` . g}wNߦAZ?ϧ<|A;K,AG#HF~<$6IlN[4zDI{{^33M%f.R4C/E͜ :Cir*'XIGז?..>,,~`? Jڱߐ^%{&IKvL`y/̳'Am-m+//{W=..>,,{8/bw/=dNh< {؅LҕXODC>+`ѦIّnG$&N_8,GBwJa]߷Y6}_vMKsH)x[#.Q[z?>~<0|X=[܍O{4'2v? k*pw,0E鷒Fz0Kw6*Z/Dl $˱ ϼ@^ Z xP /j@-SNw՝={ֽP4:X64]Iܪ.r#/U6v1z^Dm R}v+/t:$inlHퟵSwZauh5p];%:si7&/K_Z>]0qK&/~iME5vDH F#s LddGXZ=1seFϥ\  5[">z/yL7 5jTN^.Hۍ&-n112ؽoDp~2a8'1!INr2ED~e4=/$f^W3 wfL6; ؇Wz;Xc<ŋr 7VNմ.\ǏbN/ϛ8ʪ˕;fxupy|4gϋ$Uv=/#@^ Z xP /j@- ߪ ^A/6^ Z xP /j@-@^ Z xP /jaw>y7[kW5׈NظF̸dbdk)r^wM桯&MeRkV\]5^+ט&Ɣd2u5zv`|]g7My:O4Ӵ/_Ot|5tnSZzՅ7ns8kui~ ?t_Ə>O?ͧc άNW: U-2^SA)[i|~.]utp8oNW.]4,gXˆ۫ק˖z0^eu뫎gyW}vYt4m-[i:hZ׺Uv_p ?t_Fǯ4*sROi~:4<Ӯ\|{Ps|d{YVWOfwuUteӹWFAUp{}Y~9eɮyz>qT.oޫehYU ~]:?Ot~<ϗ{2oOM)MϿ?.:k2~yڼ̫O|?e|}*Nmq|>ʧQt}ͮ:貚\^৅ӕ:eWl>,jhz>/P&ώޤ- l 4N^iWLV5vϚl=gi/<_NFWԟ&+iD~4~\t~y)?/2:l?MS>OKǃȦiZW~}}Z?]eCmC;Ow-fP$ٴ_>\W ~2vU 櫃A$ <]Vi|پ }y<>?ߧ4߯kHWrT_|9J<W:z>t: Sa}|:y>,_Ui/G}=)?/Z~p4O_}:WW_:ͯ*{>/yJ]H櫃o?M4?^JO_*W _R:_yN_u|t۬:唎~i4ԏ/˗_hmX>/M}9/Ii~:/3\/WyUƕnQʯO맫p_y՗?>O?^g6_4~}D>y(MWeU_)qm,_U<4~ܗ򾞔q-?\__f^߯>OmrF<&(z +_~E=?]ӄ OW:^=hZt}Y~=5_74]V]oN{[Gӕ]~Y4ZqWɺ隧piJSuuogm+K)p)?/yy:]4?>LQ~=yS:4.|l(\0/}|޾l!/|q}y{-_2r^i^upyO4a:4|W^2>m?t Oe}=M|/*K)nt~t>_.ۡt9|>/OyzôaSitz<48 {]ºˆu_:r\g_upyO4a:ϋy||O*LO^h>>m?t~p]}:)|hW}9*W_5?O|54׿sۡt9|>/Ok~7ٴaSvtt\i~Pp0/}y*|TJ.|w5i>~9}q}u6MXa=<]||E3/Opt 5SMWy(\^jZ&E.pt~]0QoC_}>O]n@ƭ,bo\'Ƽt2uҲT~_p_/}~^F_?h_ |tQA4tOl~~}uUY~_޿2Ojz_*_M/_?]_c@"|u-SrJ_uYryi0o_Frk:Mp?r4okJ4:|>}i}]e?Ml~~}uKowԅ˗e(?o7>/})}e=Z[üupe4zaJ_'۟!:{z4/M,Wו_&NGXe|~Cj?/Lyt~կ4/zm~Py*M<5_?hz{/t{//aϫ[TXԛʯ_=#\VyxNumC|M0}(f3jF>S?t#Lkz x됮Ӵbu龲tQ/7ϧ |]w?-\ƿ_6L,.pQ2ˮc(S_]:4Fip} e^x,x=>_//_Et~u|YZNiO_u._:A_u.SW:MqKM4/oot:4ߏ+/7^G޿~i<:OOS::M?\.\ƿe>|:?-0O|]x~.ncNit4M:?>rJ|^x,x=~y_//_}yeAE4MW qYxϯt:g>y}~x~}>c?Mt4M3y<||p~ 4~z~YOˮc(S?S:ͧe|?/\p9eu²tq/<kNr踟ie~E|iuPկ]i>U>Osx}M.mHuy@:pN>3Sq^v9 sWwiOW7X\C'9%c>u\ ؀f9Sʛ}C:A^ɺ{=}113s=fvyJ9;N/샜|atz׿7E8qB~s8W٫6%{J|Gռva3< i9cː0O 3'av샜vڹmNbH QPW$MX'Z 8O,>{Ztccx>O ^uw/F9 yUݾKa99זkP9:1Q؃=f=@i=⬫>8'0R>:ƹ~c_;9ز/s0_dڏ߯/1%C` {6_ 7>-؋5&ytFCA 8g0C?0FL+7CnMKu61uoDo1uk+0䲖~4 㑞S^90^1^~_s{vvN uy.׫MƔf|y5 X\{JGnbݛ }ȋ?}\omkL0ЍpW/8g0u0)g0vPj^VNtbY$N.k1GzN񛠬9? cƐ)=?v ,JGY^8vOEMvNLr~?slobA<%~|@㋃ C@<%0W ߙk7.Udag}Ƀn 8YsoMvms|#';G'c K9k֬)rH߹sf>6j ubrzJq]ȴcNb'0c$HҧHg|I=*b"1\ug JgZti\>:7O 2713ߌrs>A;p9o,՜7\uqC_ăGuM1`#ZHk l%>GãN暳.r7= JF.95##ߞ؈Vk |-_oCpOTfw/[^75b(2*>ٞz֓; _{%yfg%xݠ,_\p|v9cK*߾irǷo/ =j|bwN &n&ػFXO`7AspMAB4WZ~\ )9̫')g=۳߰,e_l];YGn`&2?ux<&Zi؈-ev5ҿZZf6ߺ=?2yyx2)2L~eإ+-[M) )rNM5;;MU:9HJٸqcup,\m[_M^qp.HmY\d=0 &\Jno }r &нLk$<ʬ t glAgD6MNBNJk.ǒwư{\"-'P*?w6cqVϕ|O-!oN6ꏮ>K>0z91vܼR[D:WYOЉ}c%WP_s \ }7c/``]5W;Cf7Wi>r7 WWZtWd6ף\#3z> ܾGu! m o<~srO@f=udZz˼!4Ny+_)g`xu9:61X$= #[r A>z!>~/p{-7sn/Ỷ_fR1 mbчD:_xz._~kY08 m o<Ac;kr'tk {Hv15?k n.ҸΥ󷸠=7/`q9 ׿1!ۇ~1Arf/ifNus!sH\H>)q 6::+G붠{"~4kAm"~+m7{֗}󼑜?ڽ)qڋ tϝJ9B?G;]Xz|y9x*N|%sLsg -|*˰o:601[6nV{עOXYn#1F5D~sL荜%J;Yi :|m7q5Qz:k$FMmz"5>qлr;Jپk_?)W]bp}]'_~OY}IeT9fK>P~_Wo^,w\9'h0lwTܸVf_oN}"k [cɃn<0':gL(yMmov:6c)y玑r D;XPyN FJm{@{HEube rd l@G_r &/.~h|ϳ]@^عsmHz@g[JƩ%҆t'7>ϐ~-d=/Sgp^+㵺5YT{|߯>No~Ζjx0#]vEu6pA=;[f-zdj/n7🽴l7.;fo/\ϗ9<‹ 'p[_/߸o\]{[}R.SXYjea5 dH#N0z&OPjrϑO-cﵹXy_ZǥT^~1'`dN b8h^ va]NdX4=U}r='+ۖ,*on8|cMW;6Z޼ܻyI];/)Z\ycV*O{2rܻ};g^z}ݺues@rLZ\} x nKIYチ!x 2&@n櫧#o-ssr6ɉN18 ȁ?9'>k ~F`땗ar2Wi}vzƳ9| kl_-2'xyL:8HnbXٛy{!"XT]73AmtpoG7/>AB~ T:um.e}e]?)%YΟiļzn(}JGHqH`.bx,Ԓҥd'=G){8/k%.iC_=+B˽UUOqz֐ -@??/W=`{9bɦmv8?cYЮS;KvOU*͛ˎ}]z7/mC'g'b}lN5_ayX?ތC$5\ٗ5>R:t?:p*3EH3q뿟C2sAZa.!"n]\C\@0Fvڄr y~k댯!=>s>Zlgi~u' +ZOt7ZBGqco"sO~_ kxtyW{:*(>$}4113&r F֙T/'uo/k޶S/>y\ӊrs@2URChC凉Ui< i-7I>{Wc͜>x3>⌾V}z_`@7mڟƅ7/w}o+_(/-+o,KY]vV;cO/??,S/~lݺog_wef&Hl {nv Rw²}W>rY?W?۳`igyf 5&z NXwM9ԍ|_?qi|\sºC1SoD7?W!b<߷QVܷmEeel_\~?}փ_~DoL9ezQ)OYFm{w 1wMq!|"q w/&a}{<`r;ac? t.שp$[]<׿n ̗dx'Y N~r92:Չ19:9(O>H;=D^\ j҇CZ#szpccy9 c~b'1o ">݋kI}X?{r;Zo&y-jGڋNk?:[/vt{/uq|H؈u-9|Ļ=1^$1އ܎ao&_=R瞁Of?k=$+a$@;I~I $:G9I'1 SG?\lƨS눅˒z';,(v/,m)Xfq 2Ƒ8 s'1L;Aȃto0?PK;?w@W3<}gB;yb]Za}c#cІtk-7&ڟ6iV8ȱO_/K= zZU?}y(R}';ØK01Ź@w sùz߭&8eKKzK~O|\z哟d47'?/W\veO|泟mR:ͳOP>W>/_~y7/~wysr}ҔgQ@3dnPgdw2N,7&Y|Tho5{+;70.D`cط m~˲-kY^mPֱgL1O/ԲwfAgGLˡזvq9ϔ{?=| de8߳ оBV iO޴~}_'uWUxgXۣ=șZů΃džH3 `a/6S宭+ʥw^npXyuOn| 2r}2!٣kN.9H y|w?c vW Жȹ<؆q1m=1Xa1y2O <{I. ! > b8A~N\}I_cɫ Ǖ 0&<1Q |!^Jc6FO$z=h7'<{{cd!p&~¬+chK\l8a̵oZgxɩ_cscqΟKHr=w= Ⳟ _7k~SzIN]\|e,ǰ\IdzVѮqԁ=<iO?fbĺ?\m [y.<*=cT9[7!{7m:lN<ʄHb3WO)7.{ Gʙ5#!OyΈq~$urxw/g"Icmf/t8H.o~v$ uۚt+6{O\'F+"bn~}/n^xohk}{Wj)ի{?ӔU(2=?x;Y^>ɜ=qh vO_AUh6p ,^Zv7uSgww]r|+_(^S^S_,_,_*_~}itB{ۻqSs@? mzbw;vr|n`θԤYYʢ唵Wn,Aü~1-ѧ/Ǔ|Kg:څ9*qq/;k4{_A|ByĢ+57صwTC[[_2،a`3y8C^@ ×p7/9ƤXI&y0؇rqcG!H>Cc`3֯̃asm c| 3 l c[3_9"fG6c|mds095m n11$isM ak,#G,~sŤw۹<3İ ^ =/^τ6Б"Sye$0]0ws'|o_p(ӟ=F@C{@Z 8&`ԍu ܳ}ӻX^5\xj!?6޴`nq'kCRwo?R<|v!|rm&WWW.?ix yv͚+O;Vw~w'טs:w͋E B KIi 7E.|wO}]OrU9+>V+?rUnⲳ>KNզiwlMMFn/`5 1 {V=\+Wa])[7v'`q'ly sSflrC~c1>9=o%#Xsi]~΍7WnHnUs< C%Gp.\v{ ~/R.k25b7>k{(]k rG`lz> {qל*7-W71d>yEr[_ p'#.a-07]ЃkX^U8S?SPN9*xۛ~5הy^3vn`1nZI|]g|翬<){/ʏ,, W'gyr_nZO_vows8 1צr1#V,AyCT}7sC*=ʚc? D4zd}zs~ K׌pӸz?f=t'po9ap:g$ژ8yH165GGE07>Rz\sMn$>=aM:`xQ'esN3r^{M &=1>Rvsr%Al?g\ .2=A,>5<5{rC.0F|>_w%9#e2i c@ZC} }aZĻO }#%@vW,]:q-.ʠa`˖-+^2Sw0?}K,_܇W.r>\Gʶ;7|󪏕hk+w.޸,Y<,ط\8V{xCVݞ8N< |z91Ahrڭ/ ezw=cGݦKN{ٵxU9?b 8%obQbç_n\?O8}'L'j/FxLXxN}e}7l)ݷwIYsst-l]RSm*r e*ώ*];u;=dHkvcKG<d,`2so_Y!̳^/L%p O a>s$q8OnH ̇~.sn^>\~u#>ᇏXanup5=~o}cϠ=q#n {sc[^殓bz91 {X0g߆kzc'F8k#?5G 3\H#098ts'w9&Tk+c 8`>y!x׸'%> wx< I#ww+}/#Xր?|/)}'ڍu&DJioViq<*]s~gN?u8cK`^8bf(v@,7Gw^9s;/yIQc}nuQnb3/?}\uW+wԱdeI+C咿ʱ,Zea3 z9s_zV~&Ag4Hu@'fԳ:jo,OrگuqyWkd5f08_^֭<] kؔO`>kյ{J; 0Ξu9GhC2ep w..tHUo.+۫-ob2C֖u7u_[6a/m#[2I@7>Af.c`1Z$]Áx&6yD'48g:˩T鳶5WwX9qjٳ k (* r0|C} |ٿ}'[sYp ؆ YSi_z˟Wƥ0f-%m ]/6j'] H\k8oOk\W(%ξ񙇎r*Ձ~0`Ƹ_;gt9X p=aC- k (A"qL~! !?:~n6_o铫qԑ9p ʍK9YSi>n3giӖ|+\_7uްw%02^׶oGxׯ705-0έS;O޶uˢrW-|rז Ok'-\KG_Uk̽?i_ͷ{s/: r19ƀ~_1>&?@Z@^}>2ݹ}bN̳cϩ v=f;&Zp2kgY=Գ_uAvۧW\qExʗeݺu*_C/zыom?@D.<3_bxի_=^C9^}:x0仿nԤ&N6o|syݿ/^ wN7_ Lq>efů!Y=]^pξvSǖKiRwyW; 4nㅘ=AkJs𡷏A+#\n:G+['C6'7| l+9<8xRz%_cwK?'8v@ӭe>7:iy'K Ο*g-g,-˧8f=ۦ-[zjz]^W_׿D `΅l6$ܵ ˼0uu2V>`Bӿ8G2w߆5v_d+e"8{K.4\CO`_]i}d6ף/@&*C\9Z^o&)ryc=0('\Ӡ*ԉG4}1cvc1h63M6䨟#_?ۋ X9k>`Յ|i7Cjċyݷa |1x-:|77쾃+27y9mLo=cM~`N<@?g1c;s~bG'}U6ӗ^9:¿xAZr)5>`3}zG={\e~sr>Aϻ:O1N8Y%M'GE?;c 6Y[4͢ŋʒKʧ/T9#;l1Uشgt 75İ''-7QN?Բefuڬ5P2vl;uJ9&]15`(J` sGZ~2^pͭĤz>NW璿__{ sSGN^;a /e zJsmyss B֒sC뾵|'ze*MJmT=msՉW.~*0t$sA;uaHn$?F{wj&ݴ1[lA=|Hd/ZT?TL턽gcƅKqHy SN.+Whx;Ƶ!~hʄ1C1/صtKٸIJs&&ݼLm^SV'"O^wb_0^'+v;%Eډ\CƭZ vJ|+0;ܽm~y`rer3ek=u{`_ x3NX?رc(c:hF}0<"AJ{,00\ cS 6G:x׿؆< c:PnX˜ro1p0|΍猄bs#[x|-ae\ƣ7Yc=7&6t|].uکEj;<FׄB{cvvjM {Kd<=U б y:7YBMk3:)߰?bP{uwlؿ/A<yKI`~`\ƣ#E8}p%pB&)ݥsTwS=kPN:eǾe͆y_Χ҅SeղeaY[_-垻n7.U?G󻓹Uk$I$& Xyz1~ l>䄲9YvŻ7vrǞ|I;`n10bٸ>? SOݯWg7}rQ|4YʕˆٹX|ÆT7`l5q&?Pǎ$Fc}k~Ϡ[O>$5|r!S77ڌAk|sgp.х9G|H5.ss`3؇sk=r:?|Gl#~Xt<]߹0*21}\ƣ^c}ׄi9>ɡ-uV 4_͕Hb<׶lߞ+Y>xh?/Gk}_tGa`Qgƍ5Oc\b?q"ì y|lryYsэՇĆn|HHuC! _?cx z|mƜv6B?Z\j:DrwPp,*opjq䁫Vژ:짴@ u` ,۶֗GǜtJ}aZYru;׬|R?7,W.,U/*Z[@ >ю*)>Љ$شcqؔ ێyVrcNg2sa7^\Bsg7$z&.8 ˜m.Tڋ'7 ϡ^6d=tSe6{qɜ!،ro^ɥ"s]nM_s1Gw0$|u_p?[sC:{Ƀ>ۻOps]77#8=obћݜ v#Rm2|;Y3{\$h<m/ҁ?ZRK?kk68AV[=i{cShl_<epB-^&b퇑3mn!||@[w~:F66bX} 63K=?/ʆ*Kvo-G_de-N@yH~b'3g#+Ygw|Hpg?}~:>z =9[ZsXZ믟s'/Ƥ5賞5:qA3Wni/i7>s@ %oR\EUg_g<`>n}15F1ֳ&Gc\T_^`[;\?Fq G?>o3wc1ghGghg'ěgx y˯X !qq.G{ ̛sZ͑C#30d:!I 鳞5C7: d8sΑ6>}Iq0G{;^2 Xdt3)}ru?7xYhoQuZܼWj, hZX*<[d'@vi>NқnӻmN5W{Z]o\ ft @O<Ƥc^n)9y9˩͸vք޼`AP~F>{n m@` _ЧΰϛO^c-9\/r$9\ڳ/5nXǹ}ea{|Pګ09+g=c:kg6@쁮Xhc˜0 ve?s9y"q;>F. yqy{뿾~׿ {[Nͅiqh>kSH _e %v`/r$v0>A qxqn c[~ ]s"9>eyٳ>a=_]rF]n^^C=]fsrj7;I.PխBҎ 3ȅiuձ3eqJFP{Ӎ $^{ڳ/ڼP#O3w.F7(L"[ǁ>xvly;ZCoksyX&yu\7r9z#:Dt73`ăO,9Gwka?%Zr'u[oR)6b~\4ǝ!Э!noYXw7{D7O>uGL_rC+5r=r[5XC a[~]/+۹z8 ahq? 'Cn>0;~aFI׿?ĕ`1H_Gr&̇/9A=|޸`#V.r07vj&=9߼v0i1|Z" Y8t=ٛqߝc<7}0{H3V_XW'u[a\ˉ|s q <&=Yt?گY_>b/rm֐ :?] >xer7{qWOM,my$m#|aqz!H<`@/ 7ּԅzq3r2f(yy9n6p= OB.x}Iy䠮OYIN%>e~x/yenr 7ccqDc#%5'$nXO[rg>@'{Ki}t6@xϘZey}Ð sr뿏wSÃ=G|Wc3O7⨃^J|\#2䐇yR92XjoT;6ga\;n Ч-W9G `^J ŗ7 c2k7ᯃuy}O`r {N}ɋy'᱾#}@>l'Znn^%qK=sSg̿O-yӊ>A7&x>b酽=؀y).5 dLvIEtsns΃:O`睂.n|@ ʛ~>W"W>':Gk%wGKiֲO}c:gLՉS:Y7p\Ci,8^k<;%" LI @K2g1 jCȬg,zd=aܰs#S+r˟F x'Ҏ_ÞM@dɕ/m 6In.xcetzϹ:qJpn\Xpd?>= {5HI?sy_U`=/5WHr7y11Oث6{c~5bk\?r.no`3yDc7֥;W~{m6c$`'km83<>  X3w)7WuH??kkCi,8^5 { +פ^5PaRsYѳ_~ޑkYcc:\?r.no،~"?RG lH@Eԟ?>BZ̿fFo~20z>obseW$ֺcM~mh%q$،'.V+ǃ9`.H8}ZEIU00_)r>C[vmw^ſ5F`o |Hg}tFy2.9HτvYo6c"_hsXS`ulCX7sbc:r #k4cO:6sG7ʸ3GIDAT9/i]kcߍ Ռެ m `hkq1">y}#{fn} M nCݍf 6[t-K0|mAƵM=}ȁY}YA]<ob|Bu!# kX1~6ɘ:7k̡RؓЧ>gm:X0a}6WxJrr>et?NbPS)S>g'z"cҏԖ1JmPXa 홹} SzrS_WÙ%TɍFSy]%p b<9v?x|5Z nQݼnM9*Z-x#_޿N|x#^4hk=?7xqO@_`>0^)z>kK]w.0G@.%xy͵O!?q֙[ryn$sD=] 9 (׵nu3\k|h}B.щ#jhELh?xxj<;GsCAiں=<`zGغq]5h18%}R\#'=.;WMN d]nw.6Ďu&\c_شgBku-FTui?E}7@.$@\Il9ڬO3n2wgq`ic HG{֮>UcF<'P!:{}7>~tD6~;~=Awy#kѴ9VOd-s#秿:jI0ZĀ qSlc8` !)#ڃCoy4>\<"\8%f>0]Oހ煱5DK| nR?tlo5CfyJ@ kx%FY;y7:嗛amvKWN"~nW`Om`5^7v$|;qer !N=C 10r6i'c28%~͍G./k?Hr_'_~T<cawhX93U dWIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629145128.0 guidata-2.0.2/doc/images/screenshots/editgroupbox.png0000666000000000000000000004560500000000000017631 0ustar00PNG  IHDRA-K sRGBgAMA a pHYsodKIDATx^ x}?-˖0Sۤݻd"78b*/$fwvMijTN/>ކvm+vSiHdWM$fզimʄlMbX 9 @ ߏ=̙3!~spfO. XAWI[]_'׹q1Bb A!<C{PF_G.ȿ{LmO~ՠ\8||VnvE^#Rn[?;\MOCoAmGNg]- dҷ~uW-=W/˕W%7 rEW X׽amZBx>yH,tGԲ-SnYf=fƛumIf'[;! ü)LVj(k>v,'?òCöu׹~S2w /h@2ylugTۦr0B:P>9v[rmwd98IG~~k]k.}SG3ט4RPMeocW yU %Gn?o!eB ^y~13%jLpoGof0AZ?]ZLaЯ7/fW_.>ͣ_,v,W{Y>WuیuD/ SfS'b;{B,^XmXU$̷3ī^ۮj[{L?e7L3r[Vs@N;MY76h(}A"4҅ZE^~J? +?.y便K_[GLX|$D? QUgQy.$m7dV/-mz5lV3b /^}ΗL+U&/0y0)}~Wn"<^~+DnWaTSnF&h'-&ͫB+ZzѺv}'?W ױˊ)0#ʬg[m@UNlmco([)rw>"D._e狗>&r]"/,s}l5jB& Vy3~~,h}ң]MͫԷ#h+Y^ ˶LOv2lx۟|ݞw7ӺMfOL2Us;ת[mI?j [/ZC.kVk߸\FQynyK>%̔O#|{uH\f:bUIy@'"濛=*y|k 'GWu˺}o>_huuyݵ"1Db A!<Cy uWI[-@"1DbhM'~n @=sn>Nvb A!<Cy ZeODHaBzDG:x܎nVȶm#E~Pey9Mј-ωnqŵmFlG 6(i]Z.0M6<9WmGݎz A``e$3%2Ѳ,eT|)!#aלU}99 &LYkDC78.cnƂr f@ym/Zmn-eiVW ן01Qk mrL͍Էz|S#2?(;`Nvs'W7voݍh{΍}X+<Cy @ d#r57V캦 `q :#I$2ɢZhfYlfQ#wuk)IJo#/C(ڱf?GpC"~N;XߜN=u+^ ՞ $'Iy\˫KK>M"rJ,-m{Ֆ r"ɴ$=_reI޼EZ&Rf^&e]Eo trIMHظa(wsԜu"-՟bMam++[u{V3nD}K9\R4oޤrr,ik@u\ߔzi+i+1hx蛊U[}CT]zR{*4S[jۓ4oK:oaP!?LXu죲[>ABZkhkv  «^Ӡyӱޅt`Վk1oHyG1ܑj}ΚA}>mq'z'nCK6wafgTS^ 6hqBm_pކ{m17ܙ`OIYV[]iݮ7-c27m0o*U;dAC0Yzjl>}Ү?Z}/v4lˎkIRjY> [>AݽQkf wJ}3$ '7sN_%N]d̢֎cYc|Fu^yr']9nj=fhV7!dԫ=u77o=?kiwV:Q˗i(kh5JQkw@=ٵ=בVy @ "1Db A!<Cy @ 5eՃnEsSt7݁x-9sƍX{}@Gm|xu7?MX-A5@ "1Db f3Hdw5?)Mʼll1y-Ґʹk)@h#?0]$$KnJF^Zxny'JA6ʙĞg%U2[ΥJ-ņzm}/ূ_"Tq[ldtWv]:;V-ߵO&݌},Z>/²`8i fd8l9-G~Vl r#7#+<mN KOіy?#WO&K|b9O-/u|?.+ƪn:Y9.eFaߌ܋'JDڭI⻓`OhmO|\2vhۥ*Qw'w|,v_thbR{BN,NԣhM;&_ZӄfmH ш-Vw 6u*w*NSXjg-@#y♍Z4K-'lPhYÖr;E9[7y]n3#}njQ=(bk{L Pm!^ú~Eவnc{Mw@i{m 7zEԛE[/PV!ٳhr'_jLtRUVNxn0heu2SuݕEa+~%]~<Ж ߞ5iIPERR/z0O 'Bvj;֫Ik?`ʮc _l/8yRϸye'e-ֺܶ+θaEw7:,f⠻N߾dna˽{Z _&B$'t-*]kVjiNzӭ&laMbU_'w7?@YZӖ ж/?#H6U.u1de;5\H{AKk]m1k 4+^}P.%AZiOdܷ+ЅxPAmg&dd YəKK> ;>69IaZH) sD{$\VlV(S+[&!7Yi`XB<;$n SD͐l 8W\\_TFH#" KK6bz~eY^^s?Q,yM.rwLC-\Fz2e|ڗTy\~.I6vKyT ~6AnByM`eSCRK۔ᢤd2{\䪤fzϵEEם\.Tr-@^Q@2zXy~o;ezzDFF{Gd9)6AcR|/-0|_HLtu/o1]jeU,・TPsd9 2֓^`* apQAüń1#I&L n](97 `c6*6kb|8[*:FH'>o ljQ.5hxIIt[6ZD ,@g"ȣmb(J7-ؘd>|aU1)ͷ*i5ϻj}ekܸg*M?WK]zϵy%}!;!>d$l v6Am_گ#_)L@זIgh'li]_|;hD][K ek@n~Fw"<}r7E6ɤvVX:ϵKM끂9(yj5\֠ᔕGZmy0&?^SG{4v7 P7An kAO:z<ۚ6<$%v V^VfhwNXx]nF*7ӓdYP~C(dxZ2;b5xc^ hwvPDb }QBZh'bC@Nvb A!<Cy j jJ ݍ}-%ͧū6빊IH"Iv)+IWK^ުjM{3Of?oݴ5?n آA^ؔ-cKĠ̅hZʃ &.zz0Tsд̙&k5ZwV\RqV%2IҒϊhMzɘel(.)v`MYNrnT>jl}$ՎVMo|rI׭^ ##2RҽpN Xt-7x NO1[YFtрY^K:](s2!3asi zrm.O&]76\I*.hOB|h)<[Hi)eGzz]$oFd (mXPiV&tΊ|dR du^Ra:6jϤUs|'h<[FA.~nVK}ݲL&x{`[u l+we>BQ4j0uVtٟԭNRy ۭL>"SSe\/Q-ѓ^˘zsrnPoh|Yά|pLNտGv 0_?hKrek7|m@~of(b[玴TMRi䣭+tύV"|7xVثpo +Ol+͸OFZe Ֆײ^sSS7U귝GN,JJ6|0̲~*g gpzijq)XK烾sv{:F`o}k)onDF#GJ-bڵA[΃ē vГ7HfKulH5T e+Ks|>--WFnԁ?7Z -~XD>(+?xAss2i{**Gߪl9l{ scuy KFHMXtW l&b+|A&(2t)W:ahϐLd^Z@nz0 OF'A޲"7ұ*nF JjaF&g܄H .lP[7#Dy>m '2)pݓd:ׄ.KĢ9Gn< ;r`,<) ;%q$v:]/4Z +'C鴤uH g#~,=!LK]XX qg4kz Z l-#7x1G`q 9 ,ݵAAPlq̯o!IPۃٳ0muOΞu ܄E9Sޢ%? slF?Yܵ=9G3ڌZ+Ξh'<90<|4ğ;TQ^z-ʇ}þҞ֜8+]{øro[dHyCu*꯰(.?Y+5 ޺i9sƍݻ׍k+ ?W:1 $YU5A=S6%Ӓܼ`N^nn7nU,<:C^nDn<+_J# @kA/[7Oʾ#"ώ ѭh.[մo>b)+vHIΕQ%aqjoR$ڇnOPB|Z\j/.--5Z}P+_xQN<)=+)2ul_2HvI!4vϕӾn¼tuIet0l| H ̸mgGe} Vك;B&-fy I|{V-"ccc2;;[u\tNB~~NrtʼdRӉH]ϓ!mw:ofAw⪶k7tU&ԛ_H=2U>E2(LЄ$x"@Ċ>eW1A~؄o &hz}U4wj鑞aLyF "44юCi\^ 74 $%#h_wmiKe~.'~Rm}+ Çe訉}?L&4gd^[I4V0߱-2,aJFܬMs J۳{:O/cn 21Tg6ET !W)w]q&Wnl4k0mrwDϝ5ȔJtdP0:F[v|;EywRRxI{)ʰLOv,KW긪n' _(ffRPj8<|y:v9۵{wpaJ}Fs`,zWiu f?쿺W)tZM̙3n f޽nf_ O> =\+5} W+/+3[ Y)zxy&S< >0W| /-Y r5295o~~^N7|z)Q[^7?h MZ-ӵ[^P/c& ȣmzA'U$Bf!@#ȣ}*^N&3*%n~rQҞ[$W`G55/T;WȧLRaVe)߬#'9;Ót>oD1QrfiOoYw%^}~1EɧdS4Zie`q+:B^ڵZuRr;{0mIU+O&$ &g.%<ϭ&@h@A-ڶ{YKyZ_l7+4WlG 8y衇jk@Ay-KofhwGmDg<6XҶ'YۙV{iI{oګW.9nu/']2w, VWLtAM~xOךDF+"mBuE;"LJ>!7^??Z`4xk/iڐ|N0^y(:fE ?-ܘT4{ՈL-O'Buz *n7?8A/[ͻr_~+|A01$CqȲLndY'Oߖu5A~%oߓtq@xiOjxOJ.WR^9`eVnKD„ 9U5kkub"o}p{+AGJ ҥ_eQ+Jw v;])A]_\(AR\LoǽxŚ.43e/>uyg쌀.g+eʋ'7M_B34.¸ =)=c)X wſO=\V.k"8o;,Oىzjiܝ[5a|6[|eY}#f:&\xHFج=؜`dZzאˢ{srP01r=P.~!޼hcg!H$z1aû R[f5,lbS^J5ں_obٲ=Yn(핮7<%@l3]2 c xںY^x.jxa$Hd؊oJh E\Fe ٮnYv\v:sf:Lea3 O1#c2FZпMW .We y: &WJіw{ GMi^5L:Ȯ!NT,;2"#n3 lZs[vپ 55{7C[7+S]Z"\§DD:Wimݟ*N=Z,Ykך!)Ua=YVOT*عC"s2W_ AQoY5\7_s}1eZ@QO'dp2W)wu=)Dh-~36t]쪗ZWFe˻k}o8|ybxI;̕N( /?Y2Z]._QfV]yY]s 75xh˻Ͱ\ͣV| y$ױeV y7@--cÙ>&O18+O}쉭/~\ſo'L~_zv-'ddэxL'7,gĉsHYefdrfMp?)'Tޅ<XF$= ;Z}k7YHtfNvmZhN#>9No}eJߡCR3ˇ!|gB업'%a$NWGa;$'fr0Hn^/4Z ȯ;t>c7&틗NJrl\hr"g6﬜faa= Ub/?Μ9P޽{X};@4o M@_"-~^+7Aa#@?#w6:Gel=\k5?&f2l#< |A7vN~x!V;%r+^*o{\^"ŧ^%9֏ʎ+Z-Vc.]2[ 32aZ3댙wX쀩_\WS }d~Ԅ|Ma`erYoԣM׃j !ǃzc}M_ BZך|C{ղ}KW/7Mȟʍ۪xIw8=2_z`]oE`/[?\:@(>_ux(] :RM^y~y[͠;pEk_AAq ]" ^&|v噛/혗o\5ÿxv7OϘco.eM:UUޜ?/Cn4:g_ȏ"+D^>t<tG<"7}j|ߐ.ɵ" Wy9=3oa9]2n=n:T\e`+v ~s@1Y>rU,5{ozUGy<tGzzyM&Wo#.B1Zf䨌Fex~FNٌ[`ajчaW/630*]n#&ڍ&R.e,nZ׿.?ں Ѕ'O<+~?{R-PmOlu2̬j {YWH7jẢ κee*׍ 244!7 tՂ;?*;x yldқȘ1^\͕ zzl/Mi;yl>?'9I#IP-cVKz[Ȼeq[^0%#c277&]\ab(8p@\rE^It~.'LK%W+IJo8h뽖LJR0^,NǔL.1SE^ˊC2Q}plNul/xUn'!ȣmZw㓞6ԯ93edi {0'"S M _gvP]Bn~r_^sS帳+:J^3 Idt^$KIoƓuX̄DfHyYVϖ?WkF&.YIY˖ˤV?~%'W%jI.gB{DSeoy:%gMdV QylM' ^0ImqQLGZmy0Zmo,[ቪ\pdZLҒz KS[5eՃnEsS^7[Zrcߍ4ZOO:Vk)ᓒ W+/+3[g.[g]n$Izl%'}T5];|IpSynjuk>S ȇ+5/qc4Z ݫA5@ kCך:^~Ei]kt/oJ~xuK5د6ll>WPW|.I3<2m z ^FI YqдI鼻yFnSlO 滮7Te.Ӟ {z*r2/.7u0'F#oR|{iI$r.7Gnז &|s mry=5$KٯIghPORڗ=ԝ\G~f5f݆Pߙ3gjٻwo-+m!T#ɬ,mvI\v(R}Wn%7n_-PM?޶ ߞ>/=i ]l8U\hSLlӪAа~VpQ$܊yYXMۂ<аfk'--%gJ˦ U[ͼbUwEmzn/I~2aغLVZK&'ޠXߕ:ݛ i|k]Ru%cA7R|iRnڝC偟xw=ʹ++w_c~ ]A=A[T^޷'7VB%%}+ՠv]K' ԯrjM`7MLSeRܓg_|FQO_/}ڕjwo}NØ{3-o[?sLrMiaߌn6 N@ǦyM!d2K&gW "#\Иg_xF|lBڷd/S>ʕro___|~͗nm~O7pk(?=#2<*2&y۲~XkK`8l,aWZ^bb}2na-;lָh/rǮ/.>&~Q̩۟חʗv|Ant\ز 薎a9 2y9 LJe ҵhw"]٣қпo^Fo{$ d~Xˏ˰hߒGۏɟpZ%y~[lv<#o~eK՘o G rdAlkYy3m4[Mx7eQ݌R_x42),?-|iǼ<}E<%r!n5\mYth&φ-v؜Jy#G>SEyo[ewUrsYv+ׄffdF[MjK y:-::2/br3: x-g?mw~E>?R/E{v)ԛAWU^f@F>* $U3hW[<*](v92o?lozA7Y)tZѿ^f݆Pߙ3gvSսV.'-7yrZ+Czլw=w^7UZWh@{kB(C@]ܢYNSoEb 'fOzؚSZӓ]Q_Nv%ȣ&>ZQUk"1Db A!<Cy @ P.nljMAή@L"1Db Đj "?Mq;AIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1444211455.0 guidata-2.0.2/doc/index.rst0000666000000000000000000000141600000000000012434 0ustar00.. automodule:: guidata .. only:: html and not htmlhelp .. note:: Windows users may download the :download:`CHM Manual <../guidata.chm.zip>`. After downloading this file, you may see blank pages in the documentation. That's because Windows is blocking CHM files for security reasons. Fixing this problem is easy: * Right-click the CHM file, select properties, then click “Unblock”. * Or compress the CHM file into a zip archive and decompress it in another directory. * Do not open the CHM file on a network drive. Contents: .. toctree:: :maxdepth: 2 :glob: overview installation development examples reference/index Indices and tables: * :ref:`genindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/doc/installation.rst0000666000000000000000000000206000000000000014022 0ustar00Installation ============ Dependencies ------------ Requirements: * Python >=3.2 * `PyQt5`_ >=5.5 or `PySide2`_ >=5.11 * `QtPy`_ >= 1.3 Optional Python modules: * `h5py`_ (HDF5 files I/O) * `cx_Freeze`_ or `py2exe`_ (application deployment on Windows platforms) .. _PyQt5: https://pypi.python.org/pypi/PyQt5 .. _PySide2: https://pypi.org/project/PySide2 .. _qtpy: https://pypi.org/project/QtPy/ .. _h5py: https://pypi.python.org/pypi/h5py .. _cx_Freeze: https://pypi.python.org/pypi/cx_Freeze .. _py2exe: https://pypi.python.org/pypi/py2exe Other optional modules for developers: * gettext (text translation support) Installation ------------ From the source package: `python setup.py install` Help and support ---------------- External resources: * Bug reports and feature requests: `GitHub`_ * Help, support and discussions around the project: `GoogleGroup`_ .. _GitHub: https://github.com/PierreRaybaut/guidata .. _GoogleGroup: http://groups.google.fr/group/guidata_guiqwt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629180317.0 guidata-2.0.2/doc/overview.rst0000666000000000000000000000411500000000000013172 0ustar00Overview ======== When developping scientific software, from the simplest script to the most complex application, one systematically needs to manipulate data sets (e.g. parameters for a data processing feature). These data sets may consist of various data types: real numbers (e.g. physical quantities), integers (e.g. array indexes), strings (e.g. filenames), booleans (e.g. enable/disable an option), and so on. Most of the time, the programmer will need the following features: * allow the user to enter each parameter through a graphical user interface, using widgets which are adapted to data types (e.g. a single combo box or check boxes are suitable for presenting an option selection among multiple choices) * entered values have to be stored by the program with a convention which is again adapted to data types (e.g. when storing a combo box selection value, should we store the option string, the list index or an associated key?) * using the stored values easily (e.g. for data processing) by regrouping parameters in data structures * showing the stored values in a dialog box or within a graphical user interface layout, again with widgets adapted to data types This library aims to provide these features thanks to automatic graphical user interface generation for data set editing and display. Widgets inside GUIs are automatically generated depending on each data item type. The `guidata` library also provides the following features: * :py:mod:`guidata.qthelpers`: Qt helpers * :py:mod:`guidata.disthelpers`: `py2ex` helpers * :py:mod:`guidata.userconfig`: `.ini` configuration management helpers (based on Python standard module :py:mod:`ConfigParser`) * :py:mod:`guidata.configtools`: library/application data management * :py:mod:`guidata.gettext_helpers`: translation helpers (based on the GNU tool `gettext`) * :py:mod:`guidata.guitest`: automatic GUI-based test launcher * :py:mod:`guidata.utils`: miscelleneous utilities ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640626111.8317833 guidata-2.0.2/doc/reference/0000777000000000000000000000000000000000000012527 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1444237246.0 guidata-2.0.2/doc/reference/configtools.rst0000666000000000000000000000006300000000000015606 0ustar00.. automodule:: guidata.configtools :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1444237246.0 guidata-2.0.2/doc/reference/dataset.rst0000666000000000000000000000005700000000000014710 0ustar00.. automodule:: guidata.dataset :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1444237246.0 guidata-2.0.2/doc/reference/disthelpers.rst0000666000000000000000000000006300000000000015606 0ustar00.. automodule:: guidata.disthelpers :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/doc/reference/index.rst0000666000000000000000000000026000000000000014366 0ustar00Reference ========= guidata API: .. toctree:: :maxdepth: 2 dataset qthelpers widgets disthelpers configtools userconfig utils ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1444237246.0 guidata-2.0.2/doc/reference/qthelpers.rst0000666000000000000000000000006100000000000015265 0ustar00.. automodule:: guidata.qthelpers :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1444237246.0 guidata-2.0.2/doc/reference/userconfig.rst0000666000000000000000000000006200000000000015423 0ustar00.. automodule:: guidata.userconfig :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1444237246.0 guidata-2.0.2/doc/reference/utils.rst0000666000000000000000000000005500000000000014421 0ustar00.. automodule:: guidata.utils :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640626111.8630354 guidata-2.0.2/guidata/0000777000000000000000000000000000000000000011442 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640619957.0 guidata-2.0.2/guidata/__init__.py0000666000000000000000000006510000000000000013555 0ustar00# -*- coding: utf-8 -*- """ guidata ======= Based on the Qt library `guidata` is a Python library generating graphical user interfaces for easy dataset editing and display. It also provides helpers and application development tools for Qt. External resources: * Python Package Index: `PyPI`_ * Bug reports and feature requests: `GitHub`_ * Help, support and discussions around the project: `GoogleGroup`_ .. _PyPI: https://pypi.python.org/pypi/guidata .. _GitHub: https://github.com/PierreRaybaut/guidata .. _GoogleGroup: http://groups.google.fr/group/guidata_guiqwt """ __version__ = "2.0.2" # TODO: Add Python module hash utilities (for dependencies checking) # TODO: Investigate the qthelpers test failure (see after if __name__=='__main__') # TODO: Add support for PyQt6 # TODO: Add support for PySide6 # Dear (Debian, RPM, ...) package makers, please feel free to customize the # following path to module's data (images) and translations: DATAPATH = LOCALEPATH = "" # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License (see below) # LICENSE TERMS: # ------------- # # CeCILL FREE SOFTWARE LICENSE AGREEMENT # # # Notice # # This Agreement is a Free Software license agreement that is the result # of discussions between its authors in order to ensure compliance with # the two main principles guiding its drafting: # * firstly, compliance with the principles governing the distribution # of Free Software: access to source code, broad rights granted to # users, # * secondly, the election of a governing law, French law, with which # it is conformant, both as regards the law of torts and # intellectual property law, and the protection that it offers to # both authors and holders of the economic rights over software. # # The authors of the CeCILL (for Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre]) # license are: # # Commissariat à l'Energie Atomique - CEA, a public scientific, technical # and industrial research establishment, having its principal place of # business at 25 rue Leblanc, immeuble Le Ponant D, 75015 Paris, France. # # Centre National de la Recherche Scientifique - CNRS, a public scientific # and technological establishment, having its principal place of business # at 3 rue Michel-Ange, 75794 Paris cedex 16, France. # # Institut National de Recherche en Informatique et en Automatique - # INRIA, a public scientific and technological establishment, having its # principal place of business at Domaine de Voluceau, Rocquencourt, BP # 105, 78153 Le Chesnay cedex, France. # # # Preamble # # The purpose of this Free Software license agreement is to grant users # the right to modify and redistribute the software governed by this # license within the framework of an open source distribution model. # # The exercising of these rights is conditional upon certain obligations # for users so as to preserve this status for all subsequent redistributions. # # In consideration of access to the source code and the rights to copy, # modify and redistribute granted by the license, users are provided only # with a limited warranty and the software's author, the holder of the # economic rights, and the successive licensors only have limited liability. # # In this respect, the risks associated with loading, using, modifying # and/or developing or reproducing the software by the user are brought to # the user's attention, given its Free Software status, which may make it # complicated to use, with the result that its use is reserved for # developers and experienced professionals having in-depth computer # knowledge. Users are therefore encouraged to load and test the # suitability of the software as regards their requirements in conditions # enabling the security of their systems and/or data to be ensured and, # more generally, to use and operate it in the same conditions of # security. This Agreement may be freely reproduced and published, # provided it is not altered, and that no provisions are either added or # removed herefrom. # # This Agreement may apply to any or all software for which the holder of # the economic rights decides to submit the use thereof to its provisions. # # # Article 1 - DEFINITIONS # # For the purpose of this Agreement, when the following expressions # commence with a capital letter, they shall have the following meaning: # # Agreement: means this license agreement, and its possible subsequent # versions and annexes. # # Software: means the software in its Object Code and/or Source Code form # and, where applicable, its documentation, "as is" when the Licensee # accepts the Agreement. # # Initial Software: means the Software in its Source Code and possibly its # Object Code form and, where applicable, its documentation, "as is" when # it is first distributed under the terms and conditions of the Agreement. # # Modified Software: means the Software modified by at least one # Contribution. # # Source Code: means all the Software's instructions and program lines to # which access is required so as to modify the Software. # # Object Code: means the binary files originating from the compilation of # the Source Code. # # Holder: means the holder(s) of the economic rights over the Initial # Software. # # Licensee: means the Software user(s) having accepted the Agreement. # # Contributor: means a Licensee having made at least one Contribution. # # Licensor: means the Holder, or any other individual or legal entity, who # distributes the Software under the Agreement. # # Contribution: means any or all modifications, corrections, translations, # adaptations and/or new functions integrated into the Software by any or # all Contributors, as well as any or all Internal Modules. # # Module: means a set of sources files including their documentation that # enables supplementary functions or services in addition to those offered # by the Software. # # External Module: means any or all Modules, not derived from the # Software, so that this Module and the Software run in separate address # spaces, with one calling the other when they are run. # # Internal Module: means any or all Module, connected to the Software so # that they both execute in the same address space. # # GNU GPL: means the GNU General Public License version 2 or any # subsequent version, as published by the Free Software Foundation Inc. # # Parties: mean both the Licensee and the Licensor. # # These expressions may be used both in singular and plural form. # # # Article 2 - PURPOSE # # The purpose of the Agreement is the grant by the Licensor to the # Licensee of a non-exclusive, transferable and worldwide license for the # Software as set forth in Article 5 hereinafter for the whole term of the # protection granted by the rights over said Software. # # # Article 3 - ACCEPTANCE # # 3.1 The Licensee shall be deemed as having accepted the terms and # conditions of this Agreement upon the occurrence of the first of the # following events: # # * (i) loading the Software by any or all means, notably, by # downloading from a remote server, or by loading from a physical # medium; # * (ii) the first time the Licensee exercises any of the rights # granted hereunder. # # 3.2 One copy of the Agreement, containing a notice relating to the # characteristics of the Software, to the limited warranty, and to the # fact that its use is restricted to experienced users has been provided # to the Licensee prior to its acceptance as set forth in Article 3.1 # hereinabove, and the Licensee hereby acknowledges that it has read and # understood it. # # # Article 4 - EFFECTIVE DATE AND TERM # # # 4.1 EFFECTIVE DATE # # The Agreement shall become effective on the date when it is accepted by # the Licensee as set forth in Article 3.1. # # # 4.2 TERM # # The Agreement shall remain in force for the entire legal term of # protection of the economic rights over the Software. # # # Article 5 - SCOPE OF RIGHTS GRANTED # # The Licensor hereby grants to the Licensee, who accepts, the following # rights over the Software for any or all use, and for the term of the # Agreement, on the basis of the terms and conditions set forth hereinafter. # # Besides, if the Licensor owns or comes to own one or more patents # protecting all or part of the functions of the Software or of its # components, the Licensor undertakes not to enforce the rights granted by # these patents against successive Licensees using, exploiting or # modifying the Software. If these patents are transferred, the Licensor # undertakes to have the transferees subscribe to the obligations set # forth in this paragraph. # # # 5.1 RIGHT OF USE # # The Licensee is authorized to use the Software, without any limitation # as to its fields of application, with it being hereinafter specified # that this comprises: # # 1. permanent or temporary reproduction of all or part of the Software # by any or all means and in any or all form. # # 2. loading, displaying, running, or storing the Software on any or # all medium. # # 3. entitlement to observe, study or test its operation so as to # determine the ideas and principles behind any or all constituent # elements of said Software. This shall apply when the Licensee # carries out any or all loading, displaying, running, transmission # or storage operation as regards the Software, that it is entitled # to carry out hereunder. # # # 5.2 ENTITLEMENT TO MAKE CONTRIBUTIONS # # The right to make Contributions includes the right to translate, adapt, # arrange, or make any or all modifications to the Software, and the right # to reproduce the resulting software. # # The Licensee is authorized to make any or all Contributions to the # Software provided that it includes an explicit notice that it is the # author of said Contribution and indicates the date of the creation thereof. # # # 5.3 RIGHT OF DISTRIBUTION # # In particular, the right of distribution includes the right to publish, # transmit and communicate the Software to the general public on any or # all medium, and by any or all means, and the right to market, either in # consideration of a fee, or free of charge, one or more copies of the # Software by any means. # # The Licensee is further authorized to distribute copies of the modified # or unmodified Software to third parties according to the terms and # conditions set forth hereinafter. # # # 5.3.1 DISTRIBUTION OF SOFTWARE WITHOUT MODIFICATION # # The Licensee is authorized to distribute true copies of the Software in # Source Code or Object Code form, provided that said distribution # complies with all the provisions of the Agreement and is accompanied by: # # 1. a copy of the Agreement, # # 2. a notice relating to the limitation of both the Licensor's # warranty and liability as set forth in Articles 8 and 9, # # and that, in the event that only the Object Code of the Software is # redistributed, the Licensee allows future Licensees unhindered access to # the full Source Code of the Software by indicating how to access it, it # being understood that the additional cost of acquiring the Source Code # shall not exceed the cost of transferring the data. # # # 5.3.2 DISTRIBUTION OF MODIFIED SOFTWARE # # When the Licensee makes a Contribution to the Software, the terms and # conditions for the distribution of the resulting Modified Software # become subject to all the provisions of this Agreement. # # The Licensee is authorized to distribute the Modified Software, in # source code or object code form, provided that said distribution # complies with all the provisions of the Agreement and is accompanied by: # # 1. a copy of the Agreement, # # 2. a notice relating to the limitation of both the Licensor's # warranty and liability as set forth in Articles 8 and 9, # # and that, in the event that only the object code of the Modified # Software is redistributed, the Licensee allows future Licensees # unhindered access to the full source code of the Modified Software by # indicating how to access it, it being understood that the additional # cost of acquiring the source code shall not exceed the cost of # transferring the data. # # # 5.3.3 DISTRIBUTION OF EXTERNAL MODULES # # When the Licensee has developed an External Module, the terms and # conditions of this Agreement do not apply to said External Module, that # may be distributed under a separate license agreement. # # # 5.3.4 COMPATIBILITY WITH THE GNU GPL # # The Licensee can include a code that is subject to the provisions of one # of the versions of the GNU GPL in the Modified or unmodified Software, # and distribute that entire code under the terms of the same version of # the GNU GPL. # # The Licensee can include the Modified or unmodified Software in a code # that is subject to the provisions of one of the versions of the GNU GPL, # and distribute that entire code under the terms of the same version of # the GNU GPL. # # # Article 6 - INTELLECTUAL PROPERTY # # # 6.1 OVER THE INITIAL SOFTWARE # # The Holder owns the economic rights over the Initial Software. Any or # all use of the Initial Software is subject to compliance with the terms # and conditions under which the Holder has elected to distribute its work # and no one shall be entitled to modify the terms and conditions for the # distribution of said Initial Software. # # The Holder undertakes that the Initial Software will remain ruled at # least by this Agreement, for the duration set forth in Article 4.2. # # # 6.2 OVER THE CONTRIBUTIONS # # The Licensee who develops a Contribution is the owner of the # intellectual property rights over this Contribution as defined by # applicable law. # # # 6.3 OVER THE EXTERNAL MODULES # # The Licensee who develops an External Module is the owner of the # intellectual property rights over this External Module as defined by # applicable law and is free to choose the type of agreement that shall # govern its distribution. # # # 6.4 JOINT PROVISIONS # # The Licensee expressly undertakes: # # 1. not to remove, or modify, in any manner, the intellectual property # notices attached to the Software; # # 2. to reproduce said notices, in an identical manner, in the copies # of the Software modified or not. # # The Licensee undertakes not to directly or indirectly infringe the # intellectual property rights of the Holder and/or Contributors on the # Software and to take, where applicable, vis-à-vis its staff, any and all # measures required to ensure respect of said intellectual property rights # of the Holder and/or Contributors. # # # Article 7 - RELATED SERVICES # # 7.1 Under no circumstances shall the Agreement oblige the Licensor to # provide technical assistance or maintenance services for the Software. # # However, the Licensor is entitled to offer this type of services. The # terms and conditions of such technical assistance, and/or such # maintenance, shall be set forth in a separate instrument. Only the # Licensor offering said maintenance and/or technical assistance services # shall incur liability therefor. # # 7.2 Similarly, any Licensor is entitled to offer to its licensees, under # its sole responsibility, a warranty, that shall only be binding upon # itself, for the redistribution of the Software and/or the Modified # Software, under terms and conditions that it is free to decide. Said # warranty, and the financial terms and conditions of its application, # shall be subject of a separate instrument executed between the Licensor # and the Licensee. # # # Article 8 - LIABILITY # # 8.1 Subject to the provisions of Article 8.2, the Licensee shall be # entitled to claim compensation for any direct loss it may have suffered # from the Software as a result of a fault on the part of the relevant # Licensor, subject to providing evidence thereof. # # 8.2 The Licensor's liability is limited to the commitments made under # this Agreement and shall not be incurred as a result of in particular: # (i) loss due the Licensee's total or partial failure to fulfill its # obligations, (ii) direct or consequential loss that is suffered by the # Licensee due to the use or performance of the Software, and (iii) more # generally, any consequential loss. In particular the Parties expressly # agree that any or all pecuniary or business loss (i.e. loss of data, # loss of profits, operating loss, loss of customers or orders, # opportunity cost, any disturbance to business activities) or any or all # legal proceedings instituted against the Licensee by a third party, # shall constitute consequential loss and shall not provide entitlement to # any or all compensation from the Licensor. # # # Article 9 - WARRANTY # # 9.1 The Licensee acknowledges that the scientific and technical # state-of-the-art when the Software was distributed did not enable all # possible uses to be tested and verified, nor for the presence of # possible defects to be detected. In this respect, the Licensee's # attention has been drawn to the risks associated with loading, using, # modifying and/or developing and reproducing the Software which are # reserved for experienced users. # # The Licensee shall be responsible for verifying, by any or all means, # the suitability of the product for its requirements, its good working # order, and for ensuring that it shall not cause damage to either persons # or properties. # # 9.2 The Licensor hereby represents, in good faith, that it is entitled # to grant all the rights over the Software (including in particular the # rights set forth in Article 5). # # 9.3 The Licensee acknowledges that the Software is supplied "as is" by # the Licensor without any other express or tacit warranty, other than # that provided for in Article 9.2 and, in particular, without any warranty # as to its commercial value, its secured, safe, innovative or relevant # nature. # # Specifically, the Licensor does not warrant that the Software is free # from any error, that it will operate without interruption, that it will # be compatible with the Licensee's own equipment and software # configuration, nor that it will meet the Licensee's requirements. # # 9.4 The Licensor does not either expressly or tacitly warrant that the # Software does not infringe any third party intellectual property right # relating to a patent, software or any other property right. Therefore, # the Licensor disclaims any and all liability towards the Licensee # arising out of any or all proceedings for infringement that may be # instituted in respect of the use, modification and redistribution of the # Software. Nevertheless, should such proceedings be instituted against # the Licensee, the Licensor shall provide it with technical and legal # assistance for its defense. Such technical and legal assistance shall be # decided on a case-by-case basis between the relevant Licensor and the # Licensee pursuant to a memorandum of understanding. The Licensor # disclaims any and all liability as regards the Licensee's use of the # name of the Software. No warranty is given as regards the existence of # prior rights over the name of the Software or as regards the existence # of a trademark. # # # Article 10 - TERMINATION # # 10.1 In the event of a breach by the Licensee of its obligations # hereunder, the Licensor may automatically terminate this Agreement # thirty (30) days after notice has been sent to the Licensee and has # remained ineffective. # # 10.2 A Licensee whose Agreement is terminated shall no longer be # authorized to use, modify or distribute the Software. However, any # licenses that it may have granted prior to termination of the Agreement # shall remain valid subject to their having been granted in compliance # with the terms and conditions hereof. # # # Article 11 - MISCELLANEOUS # # # 11.1 EXCUSABLE EVENTS # # Neither Party shall be liable for any or all delay, or failure to # perform the Agreement, that may be attributable to an event of force # majeure, an act of God or an outside cause, such as defective # functioning or interruptions of the electricity or telecommunications # networks, network paralysis following a virus attack, intervention by # government authorities, natural disasters, water damage, earthquakes, # fire, explosions, strikes and labor unrest, war, etc. # # 11.2 Any failure by either Party, on one or more occasions, to invoke # one or more of the provisions hereof, shall under no circumstances be # interpreted as being a waiver by the interested Party of its right to # invoke said provision(s) subsequently. # # 11.3 The Agreement cancels and replaces any or all previous agreements, # whether written or oral, between the Parties and having the same # purpose, and constitutes the entirety of the agreement between said # Parties concerning said purpose. No supplement or modification to the # terms and conditions hereof shall be effective as between the Parties # unless it is made in writing and signed by their duly authorized # representatives. # # 11.4 In the event that one or more of the provisions hereof were to # conflict with a current or future applicable act or legislative text, # said act or legislative text shall prevail, and the Parties shall make # the necessary amendments so as to comply with said act or legislative # text. All other provisions shall remain effective. Similarly, invalidity # of a provision of the Agreement, for any reason whatsoever, shall not # cause the Agreement as a whole to be invalid. # # # 11.5 LANGUAGE # # The Agreement is drafted in both French and English and both versions # are deemed authentic. # # # Article 12 - NEW VERSIONS OF THE AGREEMENT # # 12.1 Any person is authorized to duplicate and distribute copies of this # Agreement. # # 12.2 So as to ensure coherence, the wording of this Agreement is # protected and may only be modified by the authors of the License, who # reserve the right to periodically publish updates or new versions of the # Agreement, each with a separate number. These subsequent versions may # address new issues encountered by Free Software. # # 12.3 Any Software distributed under a given version of the Agreement may # only be subsequently distributed under the same version of the Agreement # or a subsequent version, subject to the provisions of Article 5.3.4. # # # Article 13 - GOVERNING LAW AND JURISDICTION # # 13.1 The Agreement is governed by French law. The Parties agree to # endeavor to seek an amicable solution to any disagreements or disputes # that may arise during the performance of the Agreement. # # 13.2 Failing an amicable solution within two (2) months as from their # occurrence, and unless emergency proceedings are necessary, the # disagreements or disputes shall be referred to the Paris Courts having # jurisdiction, by the more diligent Party. # # # Version 2.0 dated 2006-09-05. import guidata.config def qapplication(): """ Return QApplication instance Creates it if it doesn't already exist """ from qtpy.QtWidgets import QApplication app = QApplication.instance() if not app: app = QApplication([]) install_translator(app) set_color_mode(app) return app QT_TRANSLATOR = None def install_translator(qapp): """Install Qt translator to the QApplication instance""" global QT_TRANSLATOR if QT_TRANSLATOR is None: from qtpy.QtCore import QLocale, QTranslator, QLibraryInfo locale = QLocale.system().name() # Qt-specific translator qt_translator = QTranslator() paths = QLibraryInfo.location(QLibraryInfo.TranslationsPath) if qt_translator.load("qt_" + locale, paths): QT_TRANSLATOR = qt_translator # Keep reference alive if QT_TRANSLATOR is not None: qapp.installTranslator(QT_TRANSLATOR) def set_color_mode(app): """Set color mode (dark or light), depending on OS setting""" from qtpy.QtWidgets import QStyleFactory from qtpy.QtGui import QPalette, QColor from qtpy.QtCore import Qt from guidata.external import darkdetect if darkdetect.isDark(): app.setStyle(QStyleFactory.create("Fusion")) dark_palette = QPalette() dark_color = QColor(45, 45, 45) disabled_color = QColor(127, 127, 127) dpsc = dark_palette.setColor dpsc(QPalette.Window, dark_color) dpsc(QPalette.WindowText, Qt.white) dpsc(QPalette.Base, QColor(18, 18, 18)) dpsc(QPalette.AlternateBase, dark_color) dpsc(QPalette.ToolTipBase, Qt.white) dpsc(QPalette.ToolTipText, Qt.white) dpsc(QPalette.Text, Qt.white) dpsc(QPalette.Disabled, QPalette.Text, disabled_color) dpsc(QPalette.Button, dark_color) dpsc(QPalette.ButtonText, Qt.white) dpsc(QPalette.Disabled, QPalette.ButtonText, disabled_color) dpsc(QPalette.BrightText, Qt.red) dpsc(QPalette.Link, QColor(42, 130, 218)) dpsc(QPalette.Highlight, QColor(42, 130, 218)) dpsc(QPalette.HighlightedText, Qt.black) dpsc(QPalette.Disabled, QPalette.HighlightedText, disabled_color) app.setPalette(dark_palette) app.setStyleSheet( "QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/config.py0000666000000000000000000002670700000000000013275 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ Handle *guidata* module configuration (options, images and icons) """ import os.path as osp from guidata.configtools import add_image_module_path, get_translation from guidata.userconfig import UserConfig from guidata.external import darkdetect IS_DARK = darkdetect.isDark() APP_PATH = osp.dirname(__file__) add_image_module_path("guidata", "images") _ = get_translation("guidata") WINDOWS_MONO_FONTS = ["Cascadia Code", "Consolas", "Courier New"] DEFAULTS = { "arrayeditor": { "font/family/nt": WINDOWS_MONO_FONTS, "font/family/posix": "Bitstream Vera Sans Mono", "font/family/mac": "Monaco", "font/size": 9, }, "dicteditor": { "font/family/nt": WINDOWS_MONO_FONTS, "font/family/posix": "Bitstream Vera Sans Mono", "font/family/mac": "Monaco", "font/size": 9, }, "texteditor": { "font/family/nt": WINDOWS_MONO_FONTS, "font/family/posix": "Bitstream Vera Sans Mono", "font/family/mac": "Monaco", "font/size": 9, }, "codeeditor": { "font/family/nt": WINDOWS_MONO_FONTS, "font/family/posix": "Bitstream Vera Sans Mono", "font/family/mac": "Monaco", "font/size": 10, }, "internal_console": { "max_line_count": 300, "working_dir_history": 30, "working_dir_adjusttocontents": False, "font/family": WINDOWS_MONO_FONTS, "font/size": 9, "wrap": True, "calltips": True, "cursor/width": 2, "codecompletion/size": (300, 180), "codecompletion/auto": False, "codecompletion/enter_key": True, "codecompletion/case_sensitive": True, "codecompletion/show_single": False, "external_editor/path": "SciTE", "external_editor/gotoline": "-goto:", "light_background": not IS_DARK, }, "color_schemes": { "names": [ "emacs", "idle", "monokai", "pydev", "scintilla", "spyder", "spyder/dark", "zenburn", "solarized/light", "solarized/dark", ], "default": "spyder/dark" if IS_DARK else "spyder", # ---- Emacs ---- "emacs/name": "Emacs", # Name Color Bold Italic "emacs/background": "#000000", "emacs/currentline": "#2b2b43", "emacs/currentcell": "#1c1c2d", "emacs/occurrence": "#abab67", "emacs/ctrlclick": "#0000ff", "emacs/sideareas": "#555555", "emacs/matched_p": "#009800", "emacs/unmatched_p": "#c80000", "emacs/normal": ("#ffffff", False, False), "emacs/keyword": ("#3c51e8", False, False), "emacs/builtin": ("#900090", False, False), "emacs/definition": ("#ff8040", True, False), "emacs/comment": ("#005100", False, False), "emacs/string": ("#00aa00", False, True), "emacs/number": ("#800000", False, False), "emacs/instance": ("#ffffff", False, True), # ---- IDLE ---- "idle/name": "IDLE", # Name Color Bold Italic "idle/background": "#ffffff", "idle/currentline": "#f2e6f3", "idle/currentcell": "#feefff", "idle/occurrence": "#e8f2fe", "idle/ctrlclick": "#0000ff", "idle/sideareas": "#efefef", "idle/matched_p": "#99ff99", "idle/unmatched_p": "#ff9999", "idle/normal": ("#000000", False, False), "idle/keyword": ("#ff7700", True, False), "idle/builtin": ("#900090", False, False), "idle/definition": ("#0000ff", False, False), "idle/comment": ("#dd0000", False, True), "idle/string": ("#00aa00", False, False), "idle/number": ("#924900", False, False), "idle/instance": ("#777777", True, True), # ---- Monokai ---- "monokai/name": "Monokai", # Name Color Bold Italic "monokai/background": "#121212", "monokai/currentline": "#484848", "monokai/currentcell": "#3d3d3d", "monokai/occurrence": "#666666", "monokai/ctrlclick": "#0000ff", "monokai/sideareas": "#2a2b24", "monokai/matched_p": "#688060", "monokai/unmatched_p": "#bd6e76", "monokai/normal": ("#ddddda", False, False), "monokai/keyword": ("#f92672", False, False), "monokai/builtin": ("#ae81ff", False, False), "monokai/definition": ("#a6e22e", False, False), "monokai/comment": ("#75715e", False, True), "monokai/string": ("#e6db74", False, False), "monokai/number": ("#ae81ff", False, False), "monokai/instance": ("#ddddda", False, True), # ---- Pydev ---- "pydev/name": "Pydev", # Name Color Bold Italic "pydev/background": "#ffffff", "pydev/currentline": "#e8f2fe", "pydev/currentcell": "#eff8fe", "pydev/occurrence": "#ffff99", "pydev/ctrlclick": "#0000ff", "pydev/sideareas": "#efefef", "pydev/matched_p": "#99ff99", "pydev/unmatched_p": "#ff99992", "pydev/normal": ("#000000", False, False), "pydev/keyword": ("#0000ff", False, False), "pydev/builtin": ("#900090", False, False), "pydev/definition": ("#000000", True, False), "pydev/comment": ("#c0c0c0", False, False), "pydev/string": ("#00aa00", False, True), "pydev/number": ("#800000", False, False), "pydev/instance": ("#000000", False, True), # ---- Scintilla ---- "scintilla/name": "Scintilla", # Name Color Bold Italic "scintilla/background": "#ffffff", "scintilla/currentline": "#e1f0d1", "scintilla/currentcell": "#edfcdc", "scintilla/occurrence": "#ffff99", "scintilla/ctrlclick": "#0000ff", "scintilla/sideareas": "#efefef", "scintilla/matched_p": "#99ff99", "scintilla/unmatched_p": "#ff9999", "scintilla/normal": ("#000000", False, False), "scintilla/keyword": ("#00007f", True, False), "scintilla/builtin": ("#000000", False, False), "scintilla/definition": ("#007f7f", True, False), "scintilla/comment": ("#007f00", False, False), "scintilla/string": ("#7f007f", False, False), "scintilla/number": ("#007f7f", False, False), "scintilla/instance": ("#000000", False, True), # ---- Spyder ---- "spyder/name": "Spyder", # Name Color Bold Italic "spyder/background": "#ffffff", "spyder/currentline": "#f7ecf8", "spyder/currentcell": "#fdfdde", "spyder/occurrence": "#ffff99", "spyder/ctrlclick": "#0000ff", "spyder/sideareas": "#efefef", "spyder/matched_p": "#99ff99", "spyder/unmatched_p": "#ff9999", "spyder/normal": ("#000000", False, False), "spyder/keyword": ("#0000ff", False, False), "spyder/builtin": ("#900090", False, False), "spyder/definition": ("#000000", True, False), "spyder/comment": ("#adadad", False, True), "spyder/string": ("#00aa00", False, False), "spyder/number": ("#800000", False, False), "spyder/instance": ("#924900", False, True), # ---- Spyder/Dark ---- "spyder/dark/name": "Spyder Dark", # Name Color Bold Italic "spyder/dark/background": "#121212", "spyder/dark/currentline": "#2b2b43", "spyder/dark/currentcell": "#31314e", "spyder/dark/occurrence": "#abab67", "spyder/dark/ctrlclick": "#0000ff", "spyder/dark/sideareas": "#282828", "spyder/dark/matched_p": "#009800", "spyder/dark/unmatched_p": "#c80000", "spyder/dark/normal": ("#ffffff", False, False), "spyder/dark/keyword": ("#558eff", False, False), "spyder/dark/builtin": ("#aa00aa", False, False), "spyder/dark/definition": ("#ffffff", True, False), "spyder/dark/comment": ("#7f7f7f", False, False), "spyder/dark/string": ("#11a642", False, True), "spyder/dark/number": ("#c80000", False, False), "spyder/dark/instance": ("#be5f00", False, True), # ---- Zenburn ---- "zenburn/name": "Zenburn", # Name Color Bold Italic "zenburn/background": "#121212", "zenburn/currentline": "#333333", "zenburn/currentcell": "#2c2c2c", "zenburn/occurrence": "#7a738f", "zenburn/ctrlclick": "#0000ff", "zenburn/sideareas": "#3f3f3f", "zenburn/matched_p": "#688060", "zenburn/unmatched_p": "#bd6e76", "zenburn/normal": ("#dcdccc", False, False), "zenburn/keyword": ("#dfaf8f", True, False), "zenburn/builtin": ("#efef8f", False, False), "zenburn/definition": ("#efef8f", False, False), "zenburn/comment": ("#7f9f7f", False, True), "zenburn/string": ("#cc9393", False, False), "zenburn/number": ("#8cd0d3", False, False), "zenburn/instance": ("#dcdccc", False, True), # ---- Solarized Light ---- "solarized/light/name": "Solarized Light", # Name Color Bold Italic "solarized/light/background": "#fdf6e3", "solarized/light/currentline": "#f5efdB", "solarized/light/currentcell": "#eee8d5", "solarized/light/occurrence": "#839496", "solarized/light/ctrlclick": "#d33682", "solarized/light/sideareas": "#eee8d5", "solarized/light/matched_p": "#586e75", "solarized/light/unmatched_p": "#dc322f", "solarized/light/normal": ("#657b83", False, False), "solarized/light/keyword": ("#859900", False, False), "solarized/light/builtin": ("#6c71c4", False, False), "solarized/light/definition": ("#268bd2", True, False), "solarized/light/comment": ("#93a1a1", False, True), "solarized/light/string": ("#2aa198", False, False), "solarized/light/number": ("#cb4b16", False, False), "solarized/light/instance": ("#b58900", False, True), # ---- Solarized Dark ---- "solarized/dark/name": "Solarized Dark", # Name Color Bold Italic "solarized/dark/background": "#121212", "solarized/dark/currentline": "#083f4d", "solarized/dark/currentcell": "#073642", "solarized/dark/occurrence": "#657b83", "solarized/dark/ctrlclick": "#d33682", "solarized/dark/sideareas": "#073642", "solarized/dark/matched_p": "#93a1a1", "solarized/dark/unmatched_p": "#dc322f", "solarized/dark/normal": ("#839496", False, False), "solarized/dark/keyword": ("#859900", False, False), "solarized/dark/builtin": ("#6c71c4", False, False), "solarized/dark/definition": ("#268bd2", True, False), "solarized/dark/comment": ("#586e75", False, True), "solarized/dark/string": ("#2aa198", False, False), "solarized/dark/number": ("#cb4b16", False, False), "solarized/dark/instance": ("#b58900", False, True), }, } CONF = UserConfig(DEFAULTS) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/configtools.py0000666000000000000000000002125600000000000014350 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ configtools ----------- The ``guidata.configtools`` module provides configuration related tools. """ import os import os.path as osp import sys import gettext from guidata.utils import get_module_path, decode_fs_string IMG_PATH = [] def get_module_data_path(modname, relpath=None): """Return module *modname* data path Handles py2exe/cx_Freeze distributions""" datapath = getattr(sys.modules[modname], "DATAPATH", "") if not datapath: datapath = get_module_path(modname) parentdir = osp.normpath(osp.join(datapath, osp.pardir)) if osp.isfile(parentdir): # Parent directory is not a directory but the 'library.zip' file: # this is either a py2exe or a cx_Freeze distribution datapath = osp.abspath(osp.join(osp.join(parentdir, osp.pardir), modname)) if relpath is not None: datapath = osp.abspath(osp.join(datapath, relpath)) return datapath def get_translation(modname, dirname=None): """Return translation callback for module *modname*""" if dirname is None: dirname = modname # fixup environment var LANG in case it's unknown if "LANG" not in os.environ: import locale # Warning: 2to3 false alarm ('import' fixer) lang = locale.getdefaultlocale()[0] if lang is not None: os.environ["LANG"] = lang try: _trans = gettext.translation( modname, get_module_locale_path(dirname), codeset="utf-8" ) lgettext = _trans.gettext def translate_gettext(x): y = lgettext(x) if isinstance(y, str): return y else: return str(y, "utf-8") return translate_gettext except IOError as _e: # print "Not using translations (%s)" % _e def translate_dumb(x): if not isinstance(x, str): return str(x, "utf-8") return x return translate_dumb def get_module_locale_path(modname): """Return module *modname* gettext translation path""" localepath = getattr(sys.modules[modname], "LOCALEPATH", "") if not localepath: localepath = get_module_data_path(modname, relpath="locale") return localepath def add_image_path(path, subfolders=True): """Append image path (opt. with its subfolders) to global list IMG_PATH""" if not isinstance(path, str): path = decode_fs_string(path) global IMG_PATH IMG_PATH.append(path) if subfolders: for fileobj in os.listdir(path): pth = osp.join(path, fileobj) if osp.isdir(pth): IMG_PATH.append(pth) def add_image_module_path(modname, relpath, subfolders=True): """ Appends image data path relative to a module name. Used to add module local data that resides in a module directory but will be shipped under sys.prefix / share/ ... modname must be the name of an already imported module as found in sys.modules """ add_image_path(get_module_data_path(modname, relpath=relpath), subfolders) def get_image_file_path(name, default="not_found.png"): """ Return the absolute path to image with specified name name, default: filenames with extensions """ for pth in IMG_PATH: full_path = osp.join(pth, name) if osp.isfile(full_path): return osp.abspath(full_path) if default is not None: try: return get_image_file_path(default, None) except RuntimeError: raise RuntimeError("Image file %r not found" % name) else: raise RuntimeError() ICON_CACHE = {} def get_icon(name, default="not_found.png"): """ Construct a QIcon from the file with specified name name, default: filenames with extensions """ try: return ICON_CACHE[name] except KeyError: from qtpy import QtGui as QG icon = QG.QIcon(get_image_file_path(name, default)) ICON_CACHE[name] = icon return icon def get_image_label(name, default="not_found.png"): """ Construct a QLabel from the file with specified name name, default: filenames with extensions """ from qtpy import QtGui as QG from qtpy import QtWidgets as QW label = QW.QLabel() pixmap = QG.QPixmap(get_image_file_path(name, default)) label.setPixmap(pixmap) return label def get_image_layout(imagename, text="", tooltip="", alignment=None): """ Construct a QHBoxLayout including image from the file with specified name, left-aligned text [with specified tooltip] Return (layout, label) """ from qtpy import QtWidgets as QW from qtpy import QtCore as QC if alignment is None: alignment = QC.Qt.AlignLeft layout = QW.QHBoxLayout() if alignment in (QC.Qt.AlignCenter, QC.Qt.AlignRight): layout.addStretch() layout.addWidget(get_image_label(imagename)) label = QW.QLabel(text) label.setToolTip(tooltip) layout.addWidget(label) if alignment in (QC.Qt.AlignCenter, QC.Qt.AlignLeft): layout.addStretch() return (layout, label) def font_is_installed(font): """Check if font is installed""" from qtpy import QtGui as QG return [fam for fam in QG.QFontDatabase().families() if str(fam) == font] MONOSPACE = [ "Cascadia Code PL", "Cascadia Mono PL", "Cascadia Code", "Cascadia Mono", "Consolas", "Courier New", "Bitstream Vera Sans Mono", "Andale Mono", "Liberation Mono", "Monaco", "Courier", "monospace", "Fixed", "Terminal", ] def get_family(families): """Return the first installed font family in family list""" if not isinstance(families, list): families = [families] for family in families: if font_is_installed(family): return family else: print("Warning: None of the following fonts is installed: %r" % families) return "" def get_font(conf, section, option=""): """ Construct a QFont from the specified configuration file entry conf: UserConfig instance section [, option]: configuration entry """ from qtpy import QtGui as QG if not option: option = "font" if "font" not in option: option += "/font" font = QG.QFont() if conf.has_option(section, option + "/family/nt"): families = conf.get(section, option + "/family/" + os.name) elif conf.has_option(section, option + "/family"): families = conf.get(section, option + "/family") else: families = None if families is not None: if not isinstance(families, list): families = [families] family = None for family in families: if font_is_installed(family): break font.setFamily(family) if conf.has_option(section, option + "/size"): font.setPointSize(conf.get(section, option + "/size")) if conf.get(section, option + "/bold", False): font.setWeight(QG.QFont.Bold) else: font.setWeight(QG.QFont.Normal) return font def get_pen(conf, section, option="", color="black", width=1, style="SolidLine"): """ Construct a QPen from the specified configuration file entry conf: UserConfig instance section [, option]: configuration entry [color]: default color [width]: default width [style]: default style """ from qtpy import QtGui as QG from qtpy import QtCore as QC if "pen" not in option: option += "/pen" color = conf.get(section, option + "/color", color) color = QG.QColor(color) width = conf.get(section, option + "/width", width) style_name = conf.get(section, option + "/style", style) style = getattr(QC.Qt, style_name) return QG.QPen(color, width, style) def get_brush(conf, section, option="", color="black", alpha=1.0): """ Construct a QBrush from the specified configuration file entry conf: UserConfig instance section [, option]: configuration entry [color]: default color [alpha]: default alpha-channel """ from qtpy import QtGui as QG if "brush" not in option: option += "/brush" color = conf.get(section, option + "/color", color) color = QG.QColor(color) alpha = conf.get(section, option + "/alphaF", alpha) color.setAlphaF(alpha) return QG.QBrush(color) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640626111.8942835 guidata-2.0.2/guidata/dataset/0000777000000000000000000000000000000000000013067 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/dataset/__init__.py0000666000000000000000000000114300000000000015177 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ dataset ======= The ``guidata.dataset`` package provides the core features for data set display and editing with automatically generated graphical user interfaces. .. automodule:: guidata.dataset.dataitems :members: .. automodule:: guidata.dataset.datatypes :members: .. automodule:: guidata.dataset.qtitemwidgets :members: .. automodule:: guidata.dataset.qtwidgets :members: """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640617576.0 guidata-2.0.2/guidata/dataset/dataitems.py0000666000000000000000000006514700000000000015431 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ dataset.dataitems ================= The ``guidata.dataset.dataitems`` module contains implementation for concrete DataItems. """ import os import re import datetime import collections.abc from guidata.dataset.datatypes import DataItem, ItemProperty from guidata.utils import utf8_to_unicode, add_extension from guidata.config import _ class NumericTypeItem(DataItem): """ Numeric data item """ type = None def __init__( self, label, default=None, min=None, max=None, nonzero=None, unit="", help="", check=True, ): DataItem.__init__(self, label, default=default, help=help) self.set_prop("data", min=min, max=max, nonzero=nonzero, check_value=check) self.set_prop("display", unit=unit) def get_auto_help(self, instance): """Override DataItem method""" auto_help = {int: _("integer"), float: _("float")}[self.type] _min = self.get_prop_value("data", instance, "min") _max = self.get_prop_value("data", instance, "max") nonzero = self.get_prop_value("data", instance, "nonzero") unit = self.get_prop_value("display", instance, "unit") if _min is not None and _max is not None: auto_help += _(" between ") + str(_min) + _(" and ") + str(_max) elif _min is not None: auto_help += _(" higher than ") + str(_min) elif _max is not None: auto_help += _(" lower than ") + str(_max) if nonzero: auto_help += ", " + _("non zero") if unit: auto_help += ", %s %s" % (_("unit:"), unit) return auto_help def format_string(self, instance, value, fmt, func): """Override DataItem method""" text = fmt % (func(value),) # We add directly the unit to 'text' (instead of adding it # to 'fmt') to avoid string formatting error if '%' is in unit unit = self.get_prop_value("display", instance, "unit", "") if unit: text += " " + unit return text def check_value(self, value): """Override DataItem method""" if not self.get_prop("data", "check_value", True): return True if not isinstance(value, self.type): return False if self.get_prop("data", "nonzero") and value == 0: return False _min = self.get_prop("data", "min") if _min is not None: if value < _min: return False _max = self.get_prop("data", "max") if _max is not None: if value > _max: return False return True def from_string(self, value): """Override DataItem method""" # String may contains numerical operands: if re.match(r"^([\d\(\)\+/\-\*.]|e)+$", value): try: return self.type(eval(value)) except: pass return None class FloatItem(NumericTypeItem): """ Construct a float data item * label [string]: name * default [float]: default value (optional) * min [float]: minimum value (optional) * max [float]: maximum value (optional) * slider [bool]: if True, shows a slider widget right after the line edit widget (default is False) * step [float]: step between tick values with a slider widget (optional) * nonzero [bool]: if True, zero is not a valid value (optional) * unit [string]: physical unit (optional) * help [string]: text shown in tooltip (optional) * check [bool]: if False, value is not checked (optional, default=True) """ type = float def __init__( self, label, default=None, min=None, max=None, nonzero=None, unit="", step=0.1, slider=False, help="", check=True, ): super(FloatItem, self).__init__( label, default=default, min=min, max=max, nonzero=nonzero, unit=unit, help=help, check=check, ) self.set_prop("display", slider=slider) self.set_prop("data", step=step) def get_value_from_reader(self, reader): """Reads value from the reader object, inside the try...except statement defined in the base item `deserialize` method""" return reader.read_float() class IntItem(NumericTypeItem): """ Construct an integer data item * label [string]: name * default [int]: default value (optional) * min [int]: minimum value (optional) * max [int]: maximum value (optional) * nonzero [bool]: if True, zero is not a valid value (optional) * unit [string]: physical unit (optional) * even [bool]: if True, even values are valid, if False, odd values are valid if None (default), ignored (optional) * slider [bool]: if True, shows a slider widget right after the line edit widget (default is False) * help [string]: text shown in tooltip (optional) * check [bool]: if False, value is not checked (optional, default=True) """ type = int def __init__( self, label, default=None, min=None, max=None, nonzero=None, unit="", even=None, slider=False, help="", check=True, ): super(IntItem, self).__init__( label, default=default, min=min, max=max, nonzero=nonzero, unit=unit, help=help, check=check, ) self.set_prop("data", even=even) self.set_prop("display", slider=slider) def get_auto_help(self, instance): """Override DataItem method""" auto_help = super(IntItem, self).get_auto_help(instance) even = self.get_prop_value("data", instance, "even") if even is not None: if even: auto_help += ", " + _("even") else: auto_help += ", " + _("odd") return auto_help def check_value(self, value): """Override DataItem method""" if not self.get_prop("data", "check_value", True): return True valid = super(IntItem, self).check_value(value) if not valid: return False even = self.get_prop("data", "even") if even is not None: is_even = value // 2 == value / 2.0 if (even and not is_even) or (not even and is_even): return False return True def get_value_from_reader(self, reader): """Reads value from the reader object, inside the try...except statement defined in the base item `deserialize` method""" return reader.read_int() class StringItem(DataItem): """ Construct a string data item * label [string]: name * default [string]: default value (optional) * help [string]: text shown in tooltip (optional) * notempty [bool]: if True, empty string is not a valid value (opt.) * wordwrap [bool]: toggle word wrapping (optional) """ type = (str,) def __init__(self, label, default=None, notempty=None, wordwrap=False, help=""): DataItem.__init__(self, label, default=default, help=help) self.set_prop("data", notempty=notempty) self.set_prop("display", wordwrap=wordwrap) def check_value(self, value): """Override DataItem method""" notempty = self.get_prop("data", "notempty") if notempty and not value: return False return True def from_string(self, value): """Override DataItem method""" return value def get_value_from_reader(self, reader): """Reads value from the reader object, inside the try...except statement defined in the base item `deserialize` method""" return reader.read_unicode() class TextItem(StringItem): """ Construct a text data item (multiline string) * label [string]: name * default [string]: default value (optional) * help [string]: text shown in tooltip (optional) * notempty [bool]: if True, empty string is not a valid value (opt.) * wordwrap [bool]: toggle word wrapping (optional) """ def __init__(self, label, default=None, notempty=None, wordwrap=True, help=""): StringItem.__init__( self, label, default=default, notempty=notempty, wordwrap=wordwrap, help=help, ) class BoolItem(DataItem): """ Construct a boolean data item * text [string]: form's field name (optional) * label [string]: name * default [string]: default value (optional) * help [string]: text shown in tooltip (optional) * check [bool]: if False, value is not checked (optional, default=True) """ type = bool def __init__(self, text="", label="", default=None, help="", check=True): DataItem.__init__(self, label, default=default, help=help, check=check) self.set_prop("display", text=text) def get_value_from_reader(self, reader): """Reads value from the reader object, inside the try...except statement defined in the base item `deserialize` method""" return reader.read_bool() class DateItem(DataItem): """ Construct a date data item. * text [string]: form's field name (optional) * label [string]: name * default [datetime.date]: default value (optional) * help [string]: text shown in tooltip (optional) """ type = datetime.date class DateTimeItem(DateItem): pass class ColorItem(StringItem): """ Construct a color data item * label [string]: name * default [string]: default value (optional) * help [string]: text shown in tooltip (optional) * check [bool]: if False, value is not checked (optional, default=True) Color values are encoded as hexadecimal strings or Qt color names """ def check_value(self, value): """Override DataItem method""" if not self.get_prop("data", "check_value", True): return True if not isinstance(value, self.type): return False from guidata.qthelpers import text_to_qcolor return text_to_qcolor(value).isValid() def get_value_from_reader(self, reader): """Reads value from the reader object, inside the try...except statement defined in the base item `deserialize` method""" # Using read_str converts `numpy.string_` to `str` -- otherwise, # when passing the string to a QColor Qt object, any numpy.string_ will # be interpreted as no color (black) return reader.read_str() class FileSaveItem(StringItem): """ Construct a path data item for a file to be saved * label [string]: name * formats [string (or string list)]: wildcard filter * default [string]: default value (optional) * basedir [string]: default base directory (optional) * help [string]: text shown in tooltip (optional) * check [bool]: if False, value is not checked (optional, default=True) """ def __init__( self, label, formats="*", default=None, basedir=None, all_files_first=False, help="", check=True, ): DataItem.__init__(self, label, default=default, help=help, check=check) if isinstance(formats, str): formats = [formats] self.set_prop("data", formats=formats) self.set_prop("data", basedir=basedir) self.set_prop("data", all_files_first=all_files_first) def get_auto_help(self, instance): """Override DataItem method""" formats = self.get_prop("data", "formats") return ( _("all file types") if formats == ["*"] else _("supported file types:") + " *.%s" % ", *.".join(formats) ) def check_value(self, value): """Override DataItem method""" if not self.get_prop("data", "check_value", True): return True if not isinstance(value, self.type): return False return len(value) > 0 def from_string(self, value): """Override DataItem method""" return add_extension(self, value) class FileOpenItem(FileSaveItem): """ Construct a path data item for a file to be opened * label [string]: name * formats [string (or string list)]: wildcard filter * default [string]: default value (optional) * basedir [string]: default base directory (optional) * help [string]: text shown in tooltip (optional) * check [bool]: if False, value is not checked (optional, default=True) """ def check_value(self, value): """Override DataItem method""" if not self.get_prop("data", "check_value", True): return True if not isinstance(value, self.type): return False return os.path.exists(value) and os.path.isfile(value) class FilesOpenItem(FileSaveItem): """ Construct a path data item for multiple files to be opened. * label [string]: name * formats [string (or string list)]: wildcard filter * default [string]: default value (optional) * basedir [string]: default base directory (optional) * help [string]: text shown in tooltip (optional) * check [bool]: if False, value is not checked (optional, default=True) """ type = list def __init__( self, label, formats="*", default=None, basedir=None, all_files_first=False, help="", check=True, ): if isinstance(default, str): default = [default] FileSaveItem.__init__( self, label, formats=formats, default=default, basedir=basedir, all_files_first=all_files_first, help=help, check=check, ) def check_value(self, value): """Override DataItem method""" if not self.get_prop("data", "check_value", True): return True if value is None: return False allexist = True for path in value: allexist = allexist and os.path.exists(path) and os.path.isfile(path) return allexist def from_string(self, value): """Override DataItem method""" if value.endswith("']") or value.endswith('"]'): value = eval(value) else: value = [value] return [add_extension(self, path) for path in value] def serialize(self, instance, writer): """Serialize this item""" value = self.get_value(instance) writer.write_sequence([fname.encode("utf-8") for fname in value]) def get_value_from_reader(self, reader): """Reads value from the reader object, inside the try...except statement defined in the base item `deserialize` method""" return [fname for fname in reader.read_sequence()] class DirectoryItem(StringItem): """ Construct a path data item for a directory. * label [string]: name * default [string]: default value (optional) * help [string]: text shown in tooltip (optional) * check [bool]: if False, value is not checked (optional, default=True) """ def check_value(self, value): """Override DataItem method""" if not self.get_prop("data", "check_value", True): return True if not isinstance(value, self.type): return False return os.path.exists(value) and os.path.isdir(value) class FirstChoice(object): pass class ChoiceItem(DataItem): """ Construct a data item for a list of choices. * label [string]: name * choices [list, tuple or function]: string list or (key, label) list function of two arguments (item, value) returning a list of tuples (key, label, image) where image is an icon path, a QIcon instance or a function of one argument (key) returning a QIcon instance * default [-]: default label or default key (optional) * help [string]: text shown in tooltip (optional) * check [bool]: if False, value is not checked (optional, default=True) * radio [bool]: if True, shows radio buttons instead of a combo box (default is False) """ def __init__( self, label, choices, default=FirstChoice, help="", check=True, radio=False ): if isinstance(choices, collections.abc.Callable): _choices_data = ItemProperty(choices) else: _choices_data = [] for idx, choice in enumerate(choices): _choices_data.append(self._normalize_choice(idx, choice)) if default is FirstChoice and not isinstance(choices, collections.abc.Callable): default = _choices_data[0][0] elif default is FirstChoice: default = None DataItem.__init__(self, label, default=default, help=help, check=check) self.set_prop("data", choices=_choices_data) self.set_prop("display", radio=radio) def _normalize_choice(self, idx, choice_tuple): if isinstance(choice_tuple, tuple): key, value = choice_tuple else: key = idx value = choice_tuple if isinstance(value, str): value = utf8_to_unicode(value) return (key, value, None) # def _choices(self, item): # _choices_data = self.get_prop("data", "choices") # if callable(_choices_data): # return _choices_data(self, item) # return _choices_data def get_string_value(self, instance): """Override DataItem method""" value = self.get_value(instance) choices = self.get_prop_value("data", instance, "choices") # print "ShowChoiceWidget:", choices, value for choice in choices: if choice[0] == value: return str(choice[1]) else: return DataItem.get_string_value(self, instance) class MultipleChoiceItem(ChoiceItem): """ Construct a data item for a list of choices -- multiple choices can be selected * label [string]: name * choices [list or tuple]: string list or (key, label) list * default [-]: default label or default key (optional) * help [string]: text shown in tooltip (optional) * check [bool]: if False, value is not checked (optional, default=True) """ def __init__(self, label, choices, default=(), help="", check=True): ChoiceItem.__init__(self, label, choices, default, help, check=check) self.set_prop("display", shape=(1, -1)) def horizontal(self, row_nb=1): """ Method to arange choice list horizontally on `n` rows Example: nb = MultipleChoiceItem("Number", ['1', '2', '3'] ).horizontal(2) """ self.set_prop("display", shape=(row_nb, -1)) return self def vertical(self, col_nb=1): """ Method to arange choice list vertically on `n` columns Example: nb = MultipleChoiceItem("Number", ['1', '2', '3'] ).vertical(2) """ self.set_prop("display", shape=(-1, col_nb)) return self def serialize(self, instance, writer): """Serialize this item""" value = self.get_value(instance) seq = [] _choices = self.get_prop_value("data", instance, "choices") for key, _label, _img in _choices: seq.append(key in value) writer.write_sequence(seq) def deserialize(self, instance, reader): """Deserialize this item""" flags = reader.read_sequence() # We could have trouble with objects providing their own choice # function which depend on not yet deserialized values _choices = self.get_prop_value("data", instance, "choices") value = [] for idx, flag in enumerate(flags): if flag: value.append(_choices[idx][0]) self.__set__(instance, value) class ImageChoiceItem(ChoiceItem): """ Construct a data item for a list of choices with images * label [string]: name * choices [list, tuple or function]: (label, image) list or (key, label, image) list function of two arguments (item, value) returning a list of tuples (key, label, image) where image is an icon path, a QIcon instance or a function of one argument (key) returning a QIcon instance * default [-]: default label or default key (optional) * help [string]: text shown in tooltip (optional) * check [bool]: if False, value is not checked (optional, default=True) """ def _normalize_choice(self, idx, choice_tuple): assert isinstance(choice_tuple, tuple) if len(choice_tuple) == 3: key, value, img = choice_tuple else: key = idx value, img = choice_tuple if isinstance(value, str): value = utf8_to_unicode(value) return (key, value, img) class FloatArrayItem(DataItem): """ Construct a float array data item * label [string]: name * default [numpy.ndarray]: default value (optional) * help [string]: text shown in tooltip (optional) * format [string]: formatting string (example: '%.3f') (optional) * transpose [bool]: transpose matrix (display only) * large [bool]: view all float of the array * minmax [string]: "all" (default), "columns", "rows" * check [bool]: if False, value is not checked (optional, default=True) """ def __init__( self, label, default=None, help="", format="%.3f", transpose=False, minmax="all", check=True, ): DataItem.__init__(self, label, default=default, help=help, check=check) self.set_prop("display", format=format, transpose=transpose, minmax=minmax) def format_string(self, instance, value, fmt, func): """Override DataItem method""" larg = self.get_prop_value("display", instance, "large", False) fmt = self.get_prop_value("display", instance, "format", "%s") unit = self.get_prop_value("display", instance, "unit", "") v = func(value) if larg: text = "= [" for flt in v[:-1]: text += fmt % flt + "; " text += fmt % v[-1] + "]" else: text = "~= " + fmt % v.mean() text += " [" + fmt % v.min() text += " .. " + fmt % v.max() text += "]" text += " %s" % unit return str(text) def serialize(self, instance, writer): """Serialize this item""" value = self.get_value(instance) writer.write_array(value) def get_value_from_reader(self, reader): """Reads value from the reader object, inside the try...except statement defined in the base item `deserialize` method""" return reader.read_array() class ButtonItem(DataItem): """ Construct a simple button that calls a method when hit * label [string]: text shown on the button * callback [function]: function with four parameters (dataset, item, value, parent) - dataset [DataSet]: instance of the parent dataset - item [DataItem]: instance of ButtonItem (i.e. self) - value [unspecified]: value of ButtonItem (default ButtonItem value or last value returned by the callback) - parent [QObject]: button's parent widget * icon [QIcon or string]: icon show on the button (optional) (string: icon filename as in guidata/guiqwt image search paths) * default [unspecified]: default value passed to the callback (optional) * help [string]: text shown in button's tooltip (optional) * check [bool]: if False, value is not checked (optional, default=True) The value of this item is unspecified but is passed to the callback along with the whole dataset. The value is assigned the callback`s return value. """ def __init__(self, label, callback, icon=None, default=None, help="", check=True): DataItem.__init__(self, label, default=default, help=help, check=check) self.set_prop("display", callback=callback) self.set_prop("display", icon=icon) def serialize(self, instance, writer): pass def deserialize(self, instance, reader): pass class DictItem(ButtonItem): """ Construct a dictionary data item * label [string]: name * default [dict]: default value (optional) * help [string]: text shown in tooltip (optional) * check [bool]: if False, value is not checked (optional, default=True) """ def __init__(self, label, default=None, help="", check=True): def dictedit(instance, item, value, parent): editor = CollectionsEditor(parent) value_was_none = value is None if value_was_none: value = {} editor.setup(value) if editor.exec_(): return editor.get_value() else: if value_was_none: return return value ButtonItem.__init__( self, label, dictedit, icon="dictedit.png", default=default, help=help, check=check, ) class FontFamilyItem(StringItem): """ Construct a font family name item * label [string]: name * default [string]: default value (optional) * help [string]: text shown in tooltip (optional) """ pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640617632.0 guidata-2.0.2/guidata/dataset/datatypes.py0000666000000000000000000007254000000000000015447 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ dataset.datatypes ================= The ``guidata.dataset.datatypes`` module contains implementation for DataSets (DataSet, DataSetGroup, ...) and related objects (ItemProperty, ValueProp, ...). """ # pylint: disable-msg=W0622 # pylint: disable-msg=W0212 import sys import re import collections.abc from guidata.utils import utf8_to_unicode, update_dataset DEBUG_DESERIALIZE = False class NoDefault: pass class ItemProperty(object): def __init__(self, callable=None): self.callable = callable def __call__(self, instance, item, value): """Evaluate the value of the property given, the instance, the item and the value maintained in the instance by the item""" return self.callable(instance, item, value) def set(self, instance, item, value): """Sets the value of the property given an instance, item and value Depending on implementation the value will be stored either on the instance, item or self """ raise NotImplementedError FMT_GROUPS = re.compile(r"(?dict containing realm-specific properties self.set_prop( "display", col=0, colspan=None, row=None, label=utf8_to_unicode(label) ) self.set_prop("data", check_value=check) def get_prop(self, realm, name, default=NoDefault): """Get one property of this item""" prop = self._props.get(realm) if not prop: prop = {} if default is NoDefault: return prop[name] return prop.get(name, default) def get_prop_value(self, realm, instance, name, default=NoDefault): value = self.get_prop(realm, name, default) if isinstance(value, ItemProperty): return value(instance, self, self.get_value(instance)) else: return value def set_prop(self, realm, **kwargs): """Set one or several properties using the syntax set_prop(name1=value1, ..., nameX=valueX) it returns self so that we can assign to the result like this: item = Item().set_prop(x=y) """ prop = self._props.get(realm) if not prop: prop = {} self._props[realm] = prop prop.update(kwargs) return self def set_pos(self, col=0, colspan=None, row=None): """ Set data item's position on a GUI layout """ self.set_prop("display", col=col, colspan=colspan, row=row) return self def __str__(self): return self._name + ": " + self.__class__.__name__ def get_help(self, instance): """ Return data item's tooltip """ auto_help = utf8_to_unicode(self.get_auto_help(instance)) help = self._help if auto_help: if help: help = help + "\n(" + auto_help + ")" else: help = auto_help.capitalize() return help def get_auto_help(self, instance): """ Return the automatically generated part of data item's tooltip """ return "" def format_string(self, instance, value, fmt, func): """Apply format to string representation of the item's value""" return fmt % (func(value),) def get_string_value(self, instance): """ Return a formatted unicode representation of the item's value obeying 'display' or 'repr' properties """ value = self.get_value(instance) repval = self.get_prop_value("display", instance, "repr", None) if repval is not None: return repval else: fmt = self.get_prop_value("display", instance, "format", "%s") func = self.get_prop_value("display", instance, "func", lambda x: x) if isinstance(fmt, collections.abc.Callable) and value is not None: return fmt(func(value)) if value is not None: text = self.format_string(instance, value, fmt, func) else: text = "" return text def set_name(self, new_name): """ Set data item's name """ self._name = new_name def set_help(self, new_help): """ Set data item's help text """ self._help = new_help def set_from_string(self, instance, string_value): """ Set data item's value from specified string """ value = self.from_string(string_value) self.__set__(instance, value) def set_default(self, instance): """ Set data item's value to default """ self.__set__(instance, self._default) def accept(self, visitor): """ This is the visitor pattern's accept function. It calls the corresponding visit_MYCLASS method of the visitor object. Python's allow a generic base class implementation of this method so there's no need to write an accept function for each derived class unless you need to override the default behavior """ funcname = "visit_" + self.__class__.__name__ func = getattr(visitor, funcname) func(self) def __set__(self, instance, value): setattr(instance, "_" + self._name, value) def __get__(self, instance, klass): if instance is not None: return getattr(instance, "_" + self._name, self._default) else: return self def get_value(self, instance): """ Return data item's value """ return self.__get__(instance, instance.__class__) def check_item(self, instance): """ Check data item's current value (calling method check_value) """ value = getattr(instance, "_" + self._name) return self.check_value(value) def check_value(self, instance, value): """ Check if `value` is valid for this data item """ raise NotImplementedError() def from_string(self, instance, string_value): """ Transform string into valid data item's value """ raise NotImplementedError() def bind(self, instance): """ Return a DataItemVariable instance bound to the data item """ return DataItemVariable(self, instance) def serialize(self, instance, writer): """Serialize this item using the writer object this is a default implementation that should work for everything but new datatypes """ value = self.get_value(instance) writer.write(value) def get_value_from_reader(self, reader): """Reads value from the reader object, inside the try...except statement defined in the base item `deserialize` method This method is reimplemented in some child classes""" return reader.read_any() def deserialize(self, instance, reader): """Deserialize this item using the reader object Default base implementation supposes the reader can detect expected datatype from the stream """ try: value = self.get_value_from_reader(reader) except RuntimeError as e: if DEBUG_DESERIALIZE: import traceback print("DEBUG_DESERIALIZE enabled in datatypes.py", file=sys.stderr) traceback.print_stack() print(e, file=sys.stderr) self.set_default(instance) return self.__set__(instance, value) class Obj(object): """An object that helps build default instances for ObjectItems""" def __init__(self, **kwargs): self.__dict__.update(kwargs) class ObjectItem(DataItem): """Simple helper class implementing default for composite objects""" klass = None def set_default(self, instance): """Make a copy of the default value""" value = self.klass() if self._default is not None: update_dataset(value, self._default) self.__set__(instance, value) def deserialize(self, instance, reader): """Deserialize this item using the reader object We build a new default object and deserialize it """ value = self.klass() value.deserialize(reader) self.__set__(instance, value) class DataItemProxy(object): """ Proxy for DataItem objects This class is needed to construct GroupItem class (see module guidata.qtwidgets) """ def __init__(self, item): self.item = item def __str__(self): return self.item._name + "_proxy: " + self.__class__.__name__ def get_help(self, instance): """DataItem method proxy""" return self.item.get_help(instance) def get_auto_help(self, instance): """DataItem method proxy""" return self.item.get_auto_help(instance) def get_string_value(self, instance): """DataItem method proxy""" return self.item.get_string_value(instance) def set_from_string(self, instance, string_value): """DataItem method proxy""" return self.item.set_from_string(instance, string_value) def set_default(self, instance): """DataItem method proxy""" return self.item.set_default(instance) def accept(self, visitor): """DataItem method proxy""" return self.item.accept(visitor) def get_value(self, instance): """DataItem method proxy""" return self.item.get_value(instance) def check_item(self, instance): """DataItem method proxy""" return self.item.check_item(instance) def check_value(self, instance, value): """DataItem method proxy""" return self.item.check_value(instance, value) def from_string(self, instance, string_value): """DataItem method proxy""" return self.item.from_string(instance, string_value) def get_prop(self, realm, name, default=NoDefault): """DataItem method proxy""" return self.item.get_prop(realm, name, default) def get_prop_value(self, realm, instance, name, default=NoDefault): """DataItem method proxy""" return self.item.get_prop_value(realm, instance, name, default) def set_prop(self, realm, **kwargs): """DataItem method proxy""" return self.item.set_prop(realm, **kwargs) def bind(self, instance): """DataItem method proxy""" return DataItemVariable(self, instance) # def __getattr__(self, name): # assert name in ["min_equals_max", "get_min", "get_max", # "_formats", "_text", "_choices", "_shape", # "_format", "_label", "_xy"] # val = getattr(self.item, name) # if callable(val): # return bind(val, self.instance) # else: # return val class DataItemVariable(object): """An instance of a DataItemVariable represent a binding between an item and a dataset. could be called a bound property. since DataItem instances are class attributes they need to have a DataSet instance to store their value. This class binds the two together. """ def __init__(self, item, instance): self.item = item self.instance = instance def get_prop_value(self, realm, name, default=NoDefault): """DataItem method proxy""" return self.item.get_prop_value(realm, self.instance, name, default) def get_prop(self, realm, name, default=NoDefault): """DataItem method proxy""" return self.item.get_prop(realm, name, default) # def set_prop(self, realm, **kwargs): # """DataItem method proxy""" # self.item.set_prop(realm, **kwargs) # # def __getattr__(self, name): # assert name in ["min_equals_max", "get_min", "get_max", # "_formats","_text", "_choices", "_shape", # "_format", "_label", "_xy"] # val = getattr(self.item, name) # if callable(val): # return bind(val, self.instance) # else: # return val def get_help(self): """Re-implement DataItem method""" return self.item.get_help(self.instance) def get_auto_help(self): """Re-implement DataItem method""" # XXX incohérent ? return self.item.get_auto_help(self.instance) def get_string_value(self): """ Return a unicode representation of the item's value obeying 'display' or 'repr' properties """ return self.item.get_string_value(self.instance) def set_default(self): """Re-implement DataItem method""" return self.item.set_default(self.instance) def get(self): """Re-implement DataItem method""" return self.item.get_value(self.instance) def set(self, value): """Re-implement DataItem method""" return self.item.__set__(self.instance, value) def set_from_string(self, string_value): """Re-implement DataItem method""" return self.item.set_from_string(self.instance, string_value) def check_item(self): """Re-implement DataItem method""" return self.item.check_item(self.instance) def check_value(self, value): """Re-implement DataItem method""" return self.item.check_value(value) def from_string(self, string_value): """Re-implement DataItem method""" return self.item.from_string(string_value) def label(self): """Re-implement DataItem method""" return self.item.get_prop("display", "label") class DataSetMeta(type): """ DataSet metaclass Create class attribute `_items`: list of the DataSet class attributes, created in the same order as these attributes were written """ def __new__(cls, name, bases, dct): items = {} for base in bases: if getattr(base, "__metaclass__", None) is DataSetMeta: for item in base._items: items[item._name] = item for attrname, value in list(dct.items()): if isinstance(value, DataItem): value.set_name(attrname) if attrname in items: value._order = items[attrname]._order items[attrname] = value items_list = list(items.values()) items_list.sort(key=lambda x: x._order) dct["_items"] = items_list return type.__new__(cls, name, bases, dct) Meta_Py3Compat = DataSetMeta("Meta_Py3Compat", (object,), {}) class DataSet(Meta_Py3Compat): """ Construct a DataSet object is a set of DataItem objects * title [string] * comment [string]: text shown on the top of the first data item * icon [QIcon or string]: icon show on the button (optional) (string: icon filename as in guidata/guiqwt image search paths) """ __metaclass__ = DataSetMeta # keep it even with Python 3 (see DataSetMeta) def __init__(self, title=None, comment=None, icon=""): self.__title = title self.__comment = comment self.__icon = icon comp_title, comp_comment = self._compute_title_and_comment() if title is None: self.__title = comp_title if comment is None: self.__comment = comp_comment self.__changed = False # Set default values self.set_defaults() def _get_translation(self): """We try to find the translation function (_) from the module this class was created in This function is unused but could be useful to translate strings that cannot be translated at the time they are created. """ module = sys.modules[self.__class__.__module__] if hasattr(module, "_"): return module._ else: return lambda x: x def _compute_title_and_comment(self): """ Private method to compute title and comment of the data set """ comp_title = self.__class__.__name__ comp_comment = None if self.__doc__: doc_lines = utf8_to_unicode(self.__doc__).splitlines() # Remove empty lines at the begining of comment while doc_lines and not doc_lines[0].strip(): del doc_lines[0] if doc_lines: comp_title = doc_lines.pop(0).strip() if doc_lines: comp_comment = "\n".join([x.strip() for x in doc_lines]) return comp_title, comp_comment def get_title(self): """ Return data set title """ return self.__title def get_comment(self): """ Return data set comment """ return self.__comment def get_icon(self): """ Return data set icon """ return self.__icon def set_defaults(self): """Set default values""" for item in self._items: item.set_default(self) def __str__(self): return self.to_string(debug=False) def check(self): """ Check the dataset item values """ errors = [] for item in self._items: if not item.check_item(self): errors.append(item._name) return errors def text_edit(self): """ Edit data set with text input only """ from guidata.dataset import textedit self.accept(textedit.TextEditVisitor(self)) def edit(self, parent=None, apply=None, size=None): """ Open a dialog box to edit data set * parent: parent widget (default is None, meaning no parent) * apply: apply callback (default is None) * size: dialog size (QSize object or integer tuple (width, height)) """ from guidata.dataset.qtwidgets import DataSetEditDialog win = DataSetEditDialog( self, icon=self.__icon, parent=parent, apply=apply, size=size ) return win.exec_() def view(self, parent=None, size=None): """ Open a dialog box to view data set * parent: parent widget (default is None, meaning no parent) * size: dialog size (QSize object or integer tuple (width, height)) """ from guidata.dataset.qtwidgets import DataSetShowDialog win = DataSetShowDialog(self, icon=self.__icon, parent=parent, size=size) return win.exec_() def to_string(self, debug=False, indent=None, align=False): """ Return readable string representation of the data set If debug is True, add more details on data items """ if indent is None: indent = "\n " txt = self.__title + ":" def _get_label(item): if debug: return item._name else: return item.get_prop_value("display", self, "label") length = 0 if align: for item in self._items: item_length = len(_get_label(item)) if item_length > length: length = item_length for item in self._items: if isinstance(item, ObjectItem): composite_dataset = item.get_value(self) txt += indent + composite_dataset.to_string( debug=debug, indent=indent + " " ) continue elif isinstance(item, BeginGroup): txt += indent + item._name + ":" indent += " " continue elif isinstance(item, EndGroup): indent = indent[:-2] continue value = getattr(self, "_" + item._name) if value is None: value_str = "-" else: value_str = item.get_string_value(self) if debug: label = item._name else: label = item.get_prop_value("display", self, "label") if length: label = label.ljust(length) txt += indent + label + ": " + value_str if debug: txt += " (" + item.__class__.__name__ + ")" return txt def accept(self, vis): """ helper function that passes the visitor to the accept methods of all the items in this dataset """ for item in self._items: item.accept(vis) def serialize(self, writer): for item in self._items: with writer.group(item._name): item.serialize(self, writer) def deserialize(self, reader): for item in self._items: with reader.group(item._name): try: item.deserialize(self, reader) except RuntimeError as error: if DEBUG_DESERIALIZE: import traceback print( "DEBUG_DESERIALIZE enabled in datatypes.py", file=sys.stderr ) traceback.print_stack() print(error, file=sys.stderr) item.set_default(self) def read_config(self, conf, section, option): from guidata.userconfigio import UserConfigReader reader = UserConfigReader(conf, section, option) self.deserialize(reader) def write_config(self, conf, section, option): from guidata.userconfigio import UserConfigWriter writer = UserConfigWriter(conf, section, option) self.serialize(writer) @classmethod def set_global_prop(klass, realm, **kwargs): for item in klass._items: item.set_prop(realm, **kwargs) class ActivableDataSet(DataSet): """ An ActivableDataSet instance must have an "enable" class attribute which will set the active state of the dataset instance (see example in: tests/activable_dataset.py) """ _ro = True # default *instance* attribute value _active = True _ro_prop = GetAttrProp("_ro") _active_prop = GetAttrProp("_active") def __init__(self, title=None, comment=None, icon=""): DataSet.__init__(self, title, comment, icon) # self.set_readonly() @classmethod def active_setup(klass): """ This class method must be called after the child class definition in order to setup the dataset active state """ klass.set_global_prop("display", active=klass._active_prop) klass.enable.set_prop( "display", active=True, hide=klass._ro_prop, store=klass._active_prop ) def set_readonly(self): """ The dataset is now in read-only mode, i.e. all data items are disabled """ self._ro = True self._active = self.enable def set_writeable(self): """ The dataset is now in read/write mode, i.e. all data items are enabled """ self._ro = False self._active = self.enable class DataSetGroup(object): """ Construct a DataSetGroup object, used to group several datasets together * datasets [list of DataSet objects] * title [string] * icon [QIcon or string]: icon show on the button (optional) (string: icon filename as in guidata/guiqwt image search paths) This class tries to mimics the DataSet interface. The GUI should represent it as a notebook with one page for each contained dataset. """ def __init__(self, datasets, title=None, icon=""): self.__icon = icon self.datasets = datasets if title: self.__title = title else: self.__title = self.__class__.__name__ def __str__(self): return "\n".join([dataset.__str__() for dataset in self.datasets]) def get_title(self): """ Return data set group title """ return self.__title def get_comment(self): """ Return data set group comment --> not implemented (will return None) """ return None def check(self): """ Check data set group items """ return [dataset.check() for dataset in self.datasets] def text_edit(self): """ Edit data set with text input only """ raise NotImplementedError() def edit(self, parent=None, apply=None): """ Open a dialog box to edit data set """ from guidata.dataset.qtwidgets import DataSetGroupEditDialog win = DataSetGroupEditDialog(self, icon=self.__icon, parent=parent, apply=apply) return win.exec_() def accept(self, vis): """ helper function that passes the visitor to the accept methods of all the items in this dataset """ for dataset in self.datasets: dataset.accept(vis) class GroupItem(DataItemProxy): """GroupItem proxy""" def __init__(self, item): DataItemProxy.__init__(self, item) self.group = [] class BeginGroup(DataItem): """ Data item which does not represent anything but a begin flag to define a data set group """ def serialize(self, instance, writer): pass def deserialize(self, instance, reader): pass def get_group(self): return GroupItem(self) class EndGroup(DataItem): """ Data item which does not represent anything but an end flag to define a data set group """ def serialize(self, instance, writer): pass def deserialize(self, instance, reader): pass class TabGroupItem(GroupItem): pass class BeginTabGroup(BeginGroup): def get_group(self): return TabGroupItem(self) class EndTabGroup(EndGroup): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640619485.0 guidata-2.0.2/guidata/dataset/qtitemwidgets.py0000666000000000000000000010714100000000000016337 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ dataset.qtitemwidgets ===================== Widget factories used to edit data items (factory registration is done in guidata.dataset.qtwidgets) (data item types are defined in guidata.dataset.datatypes) There is one widget type for each data item type. Example: ChoiceWidget <--> ChoiceItem, ImageChoiceItem """ import os import os.path as osp import sys import numpy import collections.abc import datetime from qtpy.QtWidgets import ( QHBoxLayout, QGridLayout, QColorDialog, QPushButton, QLineEdit, QCheckBox, QComboBox, QTabWidget, QGroupBox, QDateTimeEdit, QLabel, QTextEdit, QFrame, QDateEdit, QSlider, QRadioButton, QVBoxLayout, ) from qtpy.QtGui import QColor, QIcon, QPixmap from qtpy.QtCore import Qt, Signal from qtpy.compat import getexistingdirectory from guidata.utils import update_dataset, restore_dataset, utf8_to_unicode from guidata.qthelpers import text_to_qcolor, get_std_icon from guidata.configtools import get_icon, get_image_layout, get_image_file_path from guidata.config import _ from guidata.widgets.arrayeditor import ArrayEditor # ========================== IMPORTANT ================================= # # In this module, `item` is an instance of DataItemVariable (not DataItem) # (see guidata.datatypes for details) # # ========================== IMPORTANT ================================= # XXX: consider providing an interface here... class AbstractDataSetWidget(object): """ Base class for 'widgets' handled by `DataSetEditLayout` and it's derived classes. This is a generic representation of an input (or display) widget that has a label and one or more entry field. `DataSetEditLayout` uses a registry of *Item* to *Widget* mapping in order to automatically create a GUI for a `DataSet` structure """ READ_ONLY = False def __init__(self, item, parent_layout): """Derived constructors should create the necessary widgets The base class keeps a reference to item and parent """ self.item = item self.parent_layout = parent_layout self.group = None # Layout/Widget grouping items self.label = None self.build_mode = False def place_label(self, layout, row, column): """ Place item label on layout at specified position (row, column) """ label_text = self.item.get_prop_value("display", "label") unit = self.item.get_prop_value("display", "unit", "") if unit and not self.READ_ONLY: label_text += " (%s)" % unit self.label = QLabel(label_text) self.label.setToolTip(self.item.get_help()) layout.addWidget(self.label, row, column) def place_on_grid( self, layout, row, label_column, widget_column, row_span=1, column_span=1 ): """ Place widget on layout at specified position """ self.place_label(layout, row, label_column) layout.addWidget(self.group, row, widget_column, row_span, column_span) def is_active(self): """ Return True if associated item is active """ return self.item.get_prop_value("display", "active", True) def check(self): """ Item validator """ return True def set(self): """ Update data item value from widget contents """ # XXX: consider using item.set instead of item.set_from_string... self.item.set_from_string(self.value()) def get(self): """ Update widget contents from data item value """ pass def value(self): """ Returns the widget's current value """ return None def set_state(self): """ Update the visual status of the widget """ active = self.is_active() if self.group: self.group.setEnabled(active) if self.label: self.label.setEnabled(active) def notify_value_change(self): """ Notify parent layout that widget value has changed """ if not self.build_mode: self.parent_layout.widget_value_changed() class GroupWidget(AbstractDataSetWidget): """ GroupItem widget """ def __init__(self, item, parent_layout): super(GroupWidget, self).__init__(item, parent_layout) embedded = item.get_prop_value("display", "embedded", False) if not embedded: self.group = QGroupBox(item.get_prop_value("display", "label")) else: self.group = QFrame() self.layout = QGridLayout() EditLayoutClass = parent_layout.__class__ self.edit = EditLayoutClass( self.group, item.instance, self.layout, item.item.group, change_callback=self.notify_value_change, ) self.group.setLayout(self.layout) def get(self): """Override AbstractDataSetWidget method""" self.edit.update_widgets() def set(self): """Override AbstractDataSetWidget method""" self.edit.accept_changes() def check(self): """Override AbstractDataSetWidget method""" return self.edit.check_all_values() def place_on_grid( self, layout, row, label_column, widget_column, row_span=1, column_span=1 ): """Override AbstractDataSetWidget method""" layout.addWidget(self.group, row, label_column, row_span, column_span + 1) class TabGroupWidget(AbstractDataSetWidget): def __init__(self, item, parent_layout): super(TabGroupWidget, self).__init__(item, parent_layout) self.tabs = QTabWidget() items = item.item.group self.widgets = [] for item in items: if item.get_prop_value("display", parent_layout.instance, "hide", False): continue item.set_prop("display", embedded=True) widget = parent_layout.build_widget(item) frame = QFrame() label = widget.item.get_prop_value("display", "label") icon = widget.item.get_prop_value("display", "icon", None) if icon is not None: self.tabs.addTab(frame, get_icon(icon), label) else: self.tabs.addTab(frame, label) layout = QGridLayout() layout.setAlignment(Qt.AlignTop) frame.setLayout(layout) widget.place_on_grid(layout, 0, 0, 1) try: widget.get() except Exception: print("Error building item :", item.item._name) raise self.widgets.append(widget) def get(self): """Override AbstractDataSetWidget method""" for widget in self.widgets: widget.get() def set(self): """Override AbstractDataSetWidget method""" for widget in self.widgets: widget.set() def check(self): """Override AbstractDataSetWidget method""" return True def place_on_grid( self, layout, row, label_column, widget_column, row_span=1, column_span=1 ): """Override AbstractDataSetWidget method""" layout.addWidget(self.tabs, row, label_column, row_span, column_span + 1) class LineEditWidget(AbstractDataSetWidget): """ QLineEdit-based widget """ def __init__(self, item, parent_layout): super(LineEditWidget, self).__init__(item, parent_layout) self.edit = self.group = QLineEdit() self.edit.setToolTip(item.get_help()) if hasattr(item, "min_equals_max") and item.min_equals_max(): if item.check_item(): self.edit.setEnabled(False) self.edit.setToolTip(_("Value is forced to %d") % item.get_max()) self.edit.textChanged.connect(self.line_edit_changed) def get(self): """Override AbstractDataSetWidget method""" value = self.item.get() old_value = str(self.value()) if value is not None: if isinstance(value, QColor): # if item is a ColorItem object value = value.name() uvalue = utf8_to_unicode(value) if uvalue != old_value: self.edit.setText(utf8_to_unicode(value)) else: self.line_edit_changed(value) def line_edit_changed(self, qvalue): """QLineEdit validator""" if qvalue is not None: value = self.item.from_string(str(qvalue)) else: value = None if not self.item.check_value(value): self.edit.setStyleSheet("background-color:rgb(255, 175, 90);") else: self.edit.setStyleSheet("") cb = self.item.get_prop_value("display", "callback", None) if cb is not None: if self.build_mode: self.set() else: self.parent_layout.update_dataitems() cb(self.item.instance, self.item.item, value) self.parent_layout.update_widgets(except_this_one=self) self.update(value) self.notify_value_change() def update(self, value): """Override AbstractDataSetWidget method""" cb = self.item.get_prop_value("display", "value_callback", None) if cb is not None: cb(value) def value(self): return str(self.edit.text()) def check(self): """Override AbstractDataSetWidget method""" value = self.item.from_string(str(self.edit.text())) return self.item.check_value(value) class TextEditWidget(AbstractDataSetWidget): """ QTextEdit-based widget """ def __init__(self, item, parent_layout): super(TextEditWidget, self).__init__(item, parent_layout) self.edit = self.group = QTextEdit() self.edit.setToolTip(item.get_help()) if hasattr(item, "min_equals_max") and item.min_equals_max(): if item.check_item(): self.edit.setEnabled(False) self.edit.setToolTip(_("Value is forced to %d") % item.get_max()) self.edit.textChanged.connect(self.text_changed) def __get_text(self): """Get QTextEdit text, replacing UTF-8 EOL chars by os.linesep""" return str(self.edit.toPlainText()).replace("\u2029", os.linesep) def get(self): """Override AbstractDataSetWidget method""" value = self.item.get() if value is not None: self.edit.setPlainText(utf8_to_unicode(value)) self.text_changed() def text_changed(self): """QLineEdit validator""" value = self.item.from_string(self.__get_text()) if not self.item.check_value(value): self.edit.setStyleSheet("background-color:rgb(255, 175, 90);") else: self.edit.setStyleSheet("") self.update(value) self.notify_value_change() def update(self, value): """Override AbstractDataSetWidget method""" pass def value(self): return self.edit.toPlainText() def check(self): """Override AbstractDataSetWidget method""" value = self.item.from_string(self.__get_text()) return self.item.check_value(value) class CheckBoxWidget(AbstractDataSetWidget): """ BoolItem widget """ def __init__(self, item, parent_layout): super(CheckBoxWidget, self).__init__(item, parent_layout) self.checkbox = QCheckBox(self.item.get_prop_value("display", "text")) self.checkbox.setToolTip(item.get_help()) self.group = self.checkbox self.store = self.item.get_prop("display", "store", None) self.checkbox.stateChanged.connect(self.state_changed) def get(self): """Override AbstractDataSetWidget method""" value = self.item.get() if value is not None: self.checkbox.setChecked(value) def set(self): """Override AbstractDataSetWidget method""" self.item.set(self.value()) def value(self): return self.checkbox.isChecked() def place_on_grid( self, layout, row, label_column, widget_column, row_span=1, column_span=1 ): """Override AbstractDataSetWidget method""" if not self.item.get_prop_value("display", "label"): widget_column = label_column column_span += 1 else: self.place_label(layout, row, label_column) layout.addWidget(self.group, row, widget_column, row_span, column_span) def state_changed(self, state): self.notify_value_change() if self.store: self.do_store(state) def do_store(self, state): self.store.set(self.item.instance, self.item.item, state) self.parent_layout.refresh_widgets() class DateWidget(AbstractDataSetWidget): """ DateItem widget """ def __init__(self, item, parent_layout): super(DateWidget, self).__init__(item, parent_layout) self.dateedit = self.group = QDateEdit() self.dateedit.setToolTip(item.get_help()) self.dateedit.dateTimeChanged.connect(lambda value: self.notify_value_change()) def get(self): """Override AbstractDataSetWidget method""" value = self.item.get() if value: if not isinstance(value, datetime.date): value = datetime.date.fromordinal(value) self.dateedit.setDate(value) def set(self): """Override AbstractDataSetWidget method""" self.item.set(self.value()) def value(self): try: return self.dateedit.date().toPyDate() except AttributeError: return self.dateedit.dateTime().toPython().date() # PySide class DateTimeWidget(AbstractDataSetWidget): """ DateTimeItem widget """ def __init__(self, item, parent_layout): super(DateTimeWidget, self).__init__(item, parent_layout) self.dateedit = self.group = QDateTimeEdit() self.dateedit.setCalendarPopup(True) self.dateedit.setToolTip(item.get_help()) self.dateedit.dateTimeChanged.connect(lambda value: self.notify_value_change()) def get(self): """Override AbstractDataSetWidget method""" value = self.item.get() if value: if not isinstance(value, datetime.datetime): value = datetime.datetime.fromtimestamp(value) self.dateedit.setDateTime(value) def set(self): """Override AbstractDataSetWidget method""" self.item.set(self.value()) def value(self): try: return self.dateedit.dateTime().toPyDateTime() except AttributeError: return self.dateedit.dateTime().toPython() # PySide class GroupLayout(QHBoxLayout): def __init__(self): QHBoxLayout.__init__(self) self.widgets = [] def addWidget(self, widget): QHBoxLayout.addWidget(self, widget) self.widgets.append(widget) def setEnabled(self, state): for widget in self.widgets: widget.setEnabled(state) class HLayoutMixin(object): def __init__(self, item, parent_layout): super(HLayoutMixin, self).__init__(item, parent_layout) old_group = self.group self.group = GroupLayout() self.group.addWidget(old_group) def place_on_grid( self, layout, row, label_column, widget_column, row_span=1, column_span=1 ): """Override AbstractDataSetWidget method""" self.place_label(layout, row, label_column) layout.addLayout(self.group, row, widget_column, row_span, column_span) class ColorWidget(HLayoutMixin, LineEditWidget): """ ColorItem widget """ def __init__(self, item, parent_layout): super(ColorWidget, self).__init__(item, parent_layout) self.button = QPushButton("") self.button.setMaximumWidth(32) self.button.clicked.connect(self.select_color) self.group.addWidget(self.button) def update(self, value): """Reimplement LineEditWidget method""" LineEditWidget.update(self, value) color = text_to_qcolor(value) if color.isValid(): bitmap = QPixmap(16, 16) bitmap.fill(color) icon = QIcon(bitmap) else: icon = get_icon("not_found") self.button.setIcon(icon) def select_color(self): """Open a color selection dialog box""" color = text_to_qcolor(self.edit.text()) if not color.isValid(): color = Qt.gray color = QColorDialog.getColor(color, self.parent_layout.parent) if color.isValid(): value = color.name() self.edit.setText(value) self.update(value) self.notify_value_change() class SliderWidget(HLayoutMixin, LineEditWidget): """ IntItem with Slider """ DATA_TYPE = int def __init__(self, item, parent_layout): super(SliderWidget, self).__init__(item, parent_layout) self.slider = self.vmin = self.vmax = None if item.get_prop_value("display", "slider"): self.vmin = item.get_prop_value("data", "min") self.vmax = item.get_prop_value("data", "max") assert ( self.vmin is not None and self.vmax is not None ), "SliderWidget requires that item min/max have been defined" self.slider = QSlider() self.slider.setOrientation(Qt.Horizontal) self.setup_slider(item) self.slider.valueChanged.connect(self.value_changed) self.group.addWidget(self.slider) def value_to_slider(self, value): return value def slider_to_value(self, value): return value def setup_slider(self, item): self.slider.setRange(self.vmin, self.vmax) def update(self, value): """Reimplement LineEditWidget method""" LineEditWidget.update(self, value) if self.slider is not None and isinstance(value, self.DATA_TYPE): self.slider.blockSignals(True) self.slider.setValue(self.value_to_slider(value)) self.slider.blockSignals(False) def value_changed(self, ivalue): """Update the lineedit""" value = str(self.slider_to_value(ivalue)) self.edit.setText(value) self.update(value) class FloatSliderWidget(SliderWidget): """ FloatItem with Slider """ DATA_TYPE = float def value_to_slider(self, value): return (value - self.vmin) * 100 / (self.vmax - self.vmin) def slider_to_value(self, value): return value * (self.vmax - self.vmin) / 100 + self.vmin def setup_slider(self, item): self.slider.setRange(0, 100) def _get_child_title_func(ancestor): previous_ancestor = None while True: try: if previous_ancestor is ancestor: break return ancestor.child_title except AttributeError: previous_ancestor = ancestor ancestor = ancestor.parent() return lambda item: "" class FileWidget(HLayoutMixin, LineEditWidget): """ File path item widget """ def __init__(self, item, parent_layout, filedialog): super(FileWidget, self).__init__(item, parent_layout) self.filedialog = filedialog button = QPushButton() fmt = item.get_prop_value("data", "formats") button.setIcon(get_icon("%s.png" % fmt[0].lower(), default="file.png")) button.clicked.connect(self.select_file) self.group.addWidget(button) self.basedir = item.get_prop_value("data", "basedir") self.all_files_first = item.get_prop_value("data", "all_files_first") def select_file(self): """Open a file selection dialog box""" fname = self.item.from_string(str(self.edit.text())) if isinstance(fname, list): fname = osp.dirname(fname[0]) parent = self.parent_layout.parent _temp = sys.stdout sys.stdout = None if len(fname) == 0: fname = self.basedir _formats = self.item.get_prop_value("data", "formats") formats = [str(format).lower() for format in _formats] filter_lines = [ (_("%s files") + " (*.%s)") % (format.upper(), format) for format in formats ] all_filter = _("All supported files") + " (*.%s)" % " *.".join(formats) if len(formats) > 1: if self.all_files_first: filter_lines.insert(0, all_filter) else: filter_lines.append(all_filter) if fname is None: fname = "" child_title = _get_child_title_func(parent) fname, _filter = self.filedialog( parent, child_title(self.item), fname, "\n".join(filter_lines) ) sys.stdout = _temp if fname: if isinstance(fname, list): fname = str(fname) self.edit.setText(fname) class DirectoryWidget(HLayoutMixin, LineEditWidget): """ Directory path item widget """ def __init__(self, item, parent_layout): super(DirectoryWidget, self).__init__(item, parent_layout) button = QPushButton() button.setIcon(get_std_icon("DirOpenIcon")) button.clicked.connect(self.select_directory) self.group.addWidget(button) def select_directory(self): """Open a directory selection dialog box""" value = self.item.from_string(str(self.edit.text())) parent = self.parent_layout.parent child_title = _get_child_title_func(parent) dname = getexistingdirectory(parent, child_title(self.item), value) if dname: self.edit.setText(dname) class ChoiceWidget(AbstractDataSetWidget): """ Choice item widget """ def __init__(self, item, parent_layout): super(ChoiceWidget, self).__init__(item, parent_layout) self._first_call = True self.is_radio = item.get_prop_value("display", "radio") self.store = self.item.get_prop("display", "store", None) if self.is_radio: self.group = QGroupBox() self.group.setToolTip(item.get_help()) self.vbox = QVBoxLayout() self.group.setLayout(self.vbox) self._buttons = [] else: self.combobox = self.group = QComboBox() self.combobox.setToolTip(item.get_help()) self.combobox.currentIndexChanged.connect(self.index_changed) def index_changed(self, index): if self.store: self.store.set(self.item.instance, self.item.item, self.value()) self.parent_layout.refresh_widgets() cb = self.item.get_prop_value("display", "callback", None) if cb is not None: if self.build_mode: self.set() else: self.parent_layout.update_dataitems() cb(self.item.instance, self.item.item, self.value()) self.parent_layout.update_widgets(except_this_one=self) self.notify_value_change() def initialize_widget(self): if self.is_radio: for button in self._buttons: button.toggled.disconnect(self.index_changed) self.vbox.removeWidget(button) button.deleteLater() self._buttons = [] else: self.combobox.blockSignals(True) while self.combobox.count(): self.combobox.removeItem(0) _choices = self.item.get_prop_value("data", "choices") for key, lbl, img in _choices: if self.is_radio: button = QRadioButton(lbl, self.group) if img: if isinstance(img, str): if not osp.isfile(img): img = get_image_file_path(img) img = QIcon(img) elif isinstance(img, collections.abc.Callable): img = img(key) if self.is_radio: button.setIcon(img) else: self.combobox.addItem(img, lbl) elif not self.is_radio: self.combobox.addItem(lbl) if self.is_radio: self._buttons.append(button) self.vbox.addWidget(button) button.toggled.connect(self.index_changed) if not self.is_radio: self.combobox.blockSignals(False) def set_widget_value(self, idx): if self.is_radio: for button in self._buttons: button.blockSignals(True) self._buttons[idx].setChecked(True) for button in self._buttons: button.blockSignals(False) else: self.combobox.blockSignals(True) self.combobox.setCurrentIndex(idx) self.combobox.blockSignals(False) def get_widget_value(self): if self.is_radio: for index, widget in enumerate(self._buttons): if widget.isChecked(): return index else: return self.combobox.currentIndex() def get(self): """Override AbstractDataSetWidget method""" self.initialize_widget() value = self.item.get() if value is not None: idx = 0 _choices = self.item.get_prop_value("data", "choices") for key, _val, _img in _choices: if key == value: break idx += 1 self.set_widget_value(idx) if self._first_call: self.index_changed(idx) self._first_call = False def set(self): """Override AbstractDataSetWidget method""" try: value = self.value() except IndexError: return self.item.set(value) def value(self): index = self.get_widget_value() choices = self.item.get_prop_value("data", "choices") return choices[index][0] class MultipleChoiceWidget(AbstractDataSetWidget): """ Multiple choice item widget """ def __init__(self, item, parent_layout): super(MultipleChoiceWidget, self).__init__(item, parent_layout) self.groupbox = self.group = QGroupBox(item.get_prop_value("display", "label")) layout = QGridLayout() self.boxes = [] nx, ny = item.get_prop_value("display", "shape") cx, cy = 0, 0 _choices = item.get_prop_value("data", "choices") for _, choice, _img in _choices: checkbox = QCheckBox(choice) layout.addWidget(checkbox, cx, cy) if nx < 0: cy += 1 if cy >= ny: cy = 0 cx += 1 else: cx += 1 if cx >= nx: cx = 0 cy += 1 self.boxes.append(checkbox) self.groupbox.setLayout(layout) def get(self): """Override AbstractDataSetWidget method""" value = self.item.get() _choices = self.item.get_prop_value("data", "choices") for (i, _choice, _img), checkbox in zip(_choices, self.boxes): if value is not None and i in value: checkbox.setChecked(True) def set(self): """Override AbstractDataSetWidget method""" _choices = self.item.get_prop_value("data", "choices") choices = [_choices[i][0] for i in self.value()] self.item.set(choices) def value(self): return [i for i, w in enumerate(self.boxes) if w.isChecked()] def place_on_grid( self, layout, row, label_column, widget_column, row_span=1, column_span=1 ): """Override AbstractDataSetWidget method""" layout.addWidget(self.group, row, label_column, row_span, column_span + 1) class FloatArrayWidget(AbstractDataSetWidget): """ FloatArrayItem widget """ def __init__(self, item, parent_layout): super(FloatArrayWidget, self).__init__(item, parent_layout) _label = item.get_prop_value("display", "label") self.groupbox = self.group = QGroupBox(_label) self.layout = QGridLayout() self.layout.setAlignment(Qt.AlignLeft) self.groupbox.setLayout(self.layout) self.first_line, self.dim_label = get_image_layout( "shape.png", _("Number of rows x Number of columns") ) edit_button = QPushButton(get_icon("arredit.png"), "") edit_button.setToolTip(_("Edit array contents")) edit_button.setMaximumWidth(32) self.first_line.addWidget(edit_button) self.layout.addLayout(self.first_line, 0, 0) self.min_line, self.min_label = get_image_layout( "min.png", _("Smallest element in array") ) self.layout.addLayout(self.min_line, 1, 0) self.max_line, self.max_label = get_image_layout( "max.png", _("Largest element in array") ) self.layout.addLayout(self.max_line, 2, 0) edit_button.clicked.connect(self.edit_array) self.arr = None # le tableau si il a été modifié self.instance = None def edit_array(self): """Open an array editor dialog""" parent = self.parent_layout.parent label = self.item.get_prop_value("display", "label") editor = ArrayEditor(parent) if editor.setup_and_check(self.arr, title=label): if editor.exec_(): self.update(self.arr) def get(self): """Override AbstractDataSetWidget method""" self.arr = numpy.array(self.item.get(), copy=False) if self.item.get_prop_value("display", "transpose"): self.arr = self.arr.T self.update(self.arr) def update(self, arr): """Override AbstractDataSetWidget method""" shape = arr.shape if len(shape) == 1: shape = (1,) + shape dim = " x ".join([str(d) for d in shape]) self.dim_label.setText(dim) format = self.item.get_prop_value("display", "format") minmax = self.item.get_prop_value("display", "minmax") try: if minmax == "all": mint = format % arr.min() maxt = format % arr.max() elif minmax == "columns": mint = ", ".join( [format % arr[r, :].min() for r in range(arr.shape[0])] ) maxt = ", ".join( [format % arr[r, :].max() for r in range(arr.shape[0])] ) else: mint = ", ".join( [format % arr[:, r].min() for r in range(arr.shape[1])] ) maxt = ", ".join( [format % arr[:, r].max() for r in range(arr.shape[1])] ) except (TypeError, IndexError): mint, maxt = "-", "-" self.min_label.setText(mint) self.max_label.setText(maxt) def set(self): """Override AbstractDataSetWidget method""" if self.item.get_prop_value("display", "transpose"): value = self.value().T else: value = self.value() self.item.set(value) def value(self): return self.arr def place_on_grid( self, layout, row, label_column, widget_column, row_span=1, column_span=1 ): """Override AbstractDataSetWidget method""" layout.addWidget(self.group, row, label_column, row_span, column_span + 1) class ButtonWidget(AbstractDataSetWidget): """ BoolItem widget """ def __init__(self, item, parent_layout): super(ButtonWidget, self).__init__(item, parent_layout) _label = self.item.get_prop_value("display", "label") self.button = self.group = QPushButton(_label) self.button.setToolTip(item.get_help()) _icon = self.item.get_prop_value("display", "icon") if _icon is not None: if isinstance(_icon, str): _icon = get_icon(_icon) self.button.setIcon(_icon) self.button.clicked.connect(self.clicked) self.cb_value = None def get(self): """Override AbstractDataSetWidget method""" self.cb_value = self.item.get() def set(self): """Override AbstractDataSetWidget method""" self.item.set(self.value()) def value(self): return self.cb_value def place_on_grid( self, layout, row, label_column, widget_column, row_span=1, column_span=1 ): """Override AbstractDataSetWidget method""" layout.addWidget(self.group, row, label_column, row_span, column_span + 1) def clicked(self, *args): self.parent_layout.update_dataitems() callback = self.item.get_prop_value("display", "callback") self.cb_value = callback( self.item.instance, self.item.item, self.cb_value, self.button.parent() ) self.set() self.parent_layout.update_widgets() class DataSetWidget(AbstractDataSetWidget): """ DataSet widget """ def __init__(self, item, parent_layout): super(DataSetWidget, self).__init__(item, parent_layout) self.dataset = self.klass() # Création du layout contenant les champs d'édition du signal embedded = item.get_prop_value("display", "embedded", False) if not embedded: self.group = QGroupBox(item.get_prop_value("display", "label")) else: self.group = QFrame() self.layout = QGridLayout() self.group.setLayout(self.layout) EditLayoutClass = parent_layout.__class__ self.edit = EditLayoutClass( self.parent_layout.parent, self.dataset, self.layout ) def get(self): """Override AbstractDataSetWidget method""" self.get_dataset() for widget in self.edit.widgets: widget.get() def set(self): """Override AbstractDataSetWidget method""" for widget in self.edit.widgets: widget.set() self.set_dataset() def get_dataset(self): """update's internal parameter representation from the item's stored value default behavior uses update_dataset and assumes internal dataset class is the same as item's value class""" item = self.item.get() update_dataset(self.dataset, item) def set_dataset(self): """update the item's value from the internal data representation default behavior uses restore_dataset and assumes internal dataset class is the same as item's value class""" item = self.item.get() restore_dataset(self.dataset, item) def place_on_grid( self, layout, row, label_column, widget_column, row_span=1, column_span=1 ): """Override AbstractDataSetWidget method""" layout.addWidget(self.group, row, label_column, row_span, column_span + 1) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/dataset/qtwidgets.py0000666000000000000000000005063400000000000015464 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ dataset.qtwidgets ================= Dialog boxes used to edit data sets: DataSetEditDialog DataSetGroupEditDialog DataSetShowDialog ...and layouts: GroupItem DataSetEditLayout DataSetShowLayout """ from qtpy.QtWidgets import ( QDialog, QMessageBox, QDialogButtonBox, QWidget, QVBoxLayout, QGridLayout, QLabel, QSpacerItem, QTabWidget, QApplication, QGroupBox, QPushButton, ) from qtpy.QtGui import QColor, QIcon, QPainter, QPicture, QBrush from qtpy.QtCore import Qt, QRect, QSize, Signal from qtpy.compat import getopenfilename, getopenfilenames, getsavefilename from guidata.configtools import get_icon from guidata.config import _ from guidata.dataset.datatypes import BeginGroup, EndGroup, GroupItem, TabGroupItem from guidata.qthelpers import win32_fix_title_bar_background class DataSetEditDialog(QDialog): """ Dialog box for DataSet editing """ def __init__( self, instance, icon="", parent=None, apply=None, wordwrap=True, size=None ): QDialog.__init__(self, parent) win32_fix_title_bar_background(self) self.wordwrap = wordwrap self.apply_func = apply self.layout = QVBoxLayout() if instance.get_comment(): label = QLabel(instance.get_comment()) label.setWordWrap(wordwrap) self.layout.addWidget(label) self.instance = instance self.edit_layout = [] self.setup_instance(instance) if apply is not None: apply_button = QDialogButtonBox.Apply else: apply_button = QDialogButtonBox.NoButton bbox = QDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel | apply_button ) self.bbox = bbox bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) bbox.clicked.connect(self.button_clicked) self.layout.addWidget(bbox) self.setLayout(self.layout) if parent is None: if not isinstance(icon, QIcon): icon = get_icon(icon, default="guidata.svg") self.setWindowIcon(icon) self.setModal(True) self.setWindowTitle(instance.get_title()) if size is not None: if isinstance(size, QSize): self.resize(size) else: self.resize(*size) def button_clicked(self, button): role = self.bbox.buttonRole(button) if role == QDialogButtonBox.ApplyRole and self.apply_func is not None: if self.check(): for edl in self.edit_layout: edl.accept_changes() self.apply_func(self.instance) def setup_instance(self, instance): """Construct main layout""" grid = QGridLayout() grid.setAlignment(Qt.AlignTop) self.layout.addLayout(grid) self.edit_layout.append(self.layout_factory(instance, grid)) def layout_factory(self, instance, grid): """A factory method that produces instances of DataSetEditLayout or derived classes (see DataSetShowDialog) """ return DataSetEditLayout(self, instance, grid) def child_title(self, item): """Return data item title combined with QApplication title""" app_name = QApplication.applicationName() if not app_name: app_name = self.instance.get_title() return "%s - %s" % (app_name, item.label()) def check(self): is_ok = True for edl in self.edit_layout: if not edl.check_all_values(): is_ok = False if not is_ok: QMessageBox.warning( self, self.instance.get_title(), _("Some required entries are incorrect") + "\n" + _("Please check highlighted fields."), ) return False return True def accept(self): """Validate inputs""" if self.check(): for edl in self.edit_layout: edl.accept_changes() QDialog.accept(self) class DataSetGroupEditDialog(DataSetEditDialog): """ Tabbed dialog box for DataSet editing """ def setup_instance(self, instance): """Override DataSetEditDialog method""" from guidata.dataset.datatypes import DataSetGroup assert isinstance(instance, DataSetGroup) tabs = QTabWidget() # tabs.setUsesScrollButtons(False) self.layout.addWidget(tabs) for dataset in instance.datasets: layout = QVBoxLayout() layout.setAlignment(Qt.AlignTop) if dataset.get_comment(): label = QLabel(dataset.get_comment()) label.setWordWrap(self.wordwrap) layout.addWidget(label) grid = QGridLayout() self.edit_layout.append(self.layout_factory(dataset, grid)) layout.addLayout(grid) page = QWidget() page.setLayout(layout) if dataset.get_icon(): tabs.addTab(page, get_icon(dataset.get_icon()), dataset.get_title()) else: tabs.addTab(page, dataset.get_title()) class DataSetEditLayout(object): """ Layout in which data item widgets are placed """ _widget_factory = {} @classmethod def register(cls, item_type, factory): """Register a factory for a new item_type""" cls._widget_factory[item_type] = factory def __init__( self, parent, instance, layout, items=None, first_line=0, change_callback=None ): self.parent = parent self.instance = instance self.layout = layout self.first_line = first_line self.change_callback = change_callback self.widgets = [] self.linenos = {} # prochaine ligne à remplir par colonne self.items_pos = {} if not items: items = self.instance._items items = self.transform_items(items) self.setup_layout(items) def transform_items(self, items): """ Handle group of items: transform items into a GroupItem instance if they are located between BeginGroup and EndGroup """ item_lists = [[]] for item in items: if isinstance(item, BeginGroup): item = item.get_group() item_lists[-1].append(item) item_lists.append(item.group) elif isinstance(item, EndGroup): item_lists.pop() else: item_lists[-1].append(item) assert len(item_lists) == 1 return item_lists[-1] def check_all_values(self): """Check input of all widgets""" for widget in self.widgets: if widget.is_active() and not widget.check(): return False return True def accept_changes(self): """Accept changes made to widget inputs""" self.update_dataitems() def setup_layout(self, items): """Place items on layout""" def last_col(col, span): """Return last column (which depends on column span)""" if not span: return col else: return col + span - 1 colmax = max( [ last_col( item.get_prop("display", "col"), item.get_prop("display", "colspan") ) for item in items ] ) # Check if specified rows are consistent sorted_items = [None] * len(items) rows = [] other_items = [] for item in items: row = item.get_prop("display", "row") if row is not None: if row in rows: raise ValueError( "Duplicate row index (%d) for item %r" % (row, item._name) ) if row < 0 or row >= len(items): raise ValueError( "Out of range row index (%d) for item %r" % (row, item._name) ) rows.append(row) sorted_items[row] = item else: other_items.append(item) for idx, line in enumerate(sorted_items[:]): if line is None: sorted_items[idx] = other_items.pop(0) self.items_pos = {} line = self.first_line - 1 last_item = [-1, 0, colmax] for item in sorted_items: col = item.get_prop("display", "col") colspan = item.get_prop("display", "colspan") if colspan is None: colspan = colmax - col + 1 if col <= last_item[1]: # on passe à la ligne si la colonne de debut de cet item # est avant la colonne de debut de l'item précédent line += 1 else: last_item[2] = col - last_item[1] last_item = [line, col, colspan] self.items_pos[item] = last_item for item in items: hide = item.get_prop_value("display", self.instance, "hide", False) if hide: continue widget = self.build_widget(item) self.add_row(widget) self.refresh_widgets() def build_widget(self, item): factory = self._widget_factory[type(item)] widget = factory(item.bind(self.instance), self) self.widgets.append(widget) return widget def add_row(self, widget): """Add widget to row""" item = widget.item line, col, span = self.items_pos[item.item] if col > 0: self.layout.addItem(QSpacerItem(20, 1), line, col * 3 - 1) widget.place_on_grid(self.layout, line, col * 3, col * 3 + 1, 1, 3 * span - 2) try: widget.get() except Exception: print("Error building item :", item.item._name) raise def refresh_widgets(self): """Refresh the status of all widgets""" for widget in self.widgets: widget.set_state() def update_dataitems(self): """Refresh the content of all data items""" for widget in self.widgets: if widget.is_active(): widget.set() def update_widgets(self, except_this_one=None): """Refresh the content of all widgets""" for widget in self.widgets: if widget is not except_this_one: widget.get() def widget_value_changed(self): """Method called when any widget's value has changed""" if self.change_callback is not None: self.change_callback() # Enregistrement des correspondances avec les widgets from guidata.dataset.qtitemwidgets import ( LineEditWidget, TextEditWidget, CheckBoxWidget, ColorWidget, FileWidget, DirectoryWidget, ChoiceWidget, MultipleChoiceWidget, FloatArrayWidget, GroupWidget, AbstractDataSetWidget, ButtonWidget, TabGroupWidget, DateWidget, DateTimeWidget, SliderWidget, FloatSliderWidget, ) from guidata.dataset.dataitems import ( FloatItem, StringItem, TextItem, IntItem, BoolItem, ColorItem, FileOpenItem, FilesOpenItem, FileSaveItem, DirectoryItem, ChoiceItem, ImageChoiceItem, MultipleChoiceItem, FloatArrayItem, ButtonItem, DateItem, DateTimeItem, DictItem, ) DataSetEditLayout.register(GroupItem, GroupWidget) DataSetEditLayout.register(TabGroupItem, TabGroupWidget) DataSetEditLayout.register(FloatItem, LineEditWidget) DataSetEditLayout.register(StringItem, LineEditWidget) DataSetEditLayout.register(TextItem, TextEditWidget) DataSetEditLayout.register(IntItem, SliderWidget) DataSetEditLayout.register(FloatItem, FloatSliderWidget) DataSetEditLayout.register(BoolItem, CheckBoxWidget) DataSetEditLayout.register(DateItem, DateWidget) DataSetEditLayout.register(DateTimeItem, DateTimeWidget) DataSetEditLayout.register(ColorItem, ColorWidget) DataSetEditLayout.register( FileOpenItem, lambda item, parent: FileWidget(item, parent, getopenfilename) ) DataSetEditLayout.register( FilesOpenItem, lambda item, parent: FileWidget(item, parent, getopenfilenames) ) DataSetEditLayout.register( FileSaveItem, lambda item, parent: FileWidget(item, parent, getsavefilename) ) DataSetEditLayout.register(DirectoryItem, DirectoryWidget) DataSetEditLayout.register(ChoiceItem, ChoiceWidget) DataSetEditLayout.register(ImageChoiceItem, ChoiceWidget) DataSetEditLayout.register(MultipleChoiceItem, MultipleChoiceWidget) DataSetEditLayout.register(FloatArrayItem, FloatArrayWidget) DataSetEditLayout.register(ButtonItem, ButtonWidget) DataSetEditLayout.register(DictItem, ButtonWidget) LABEL_CSS = """ QLabel { font-weight: bold; color: blue } QLabel:disabled { font-weight: bold; color: grey } """ class DataSetShowWidget(AbstractDataSetWidget): """Read-only base widget""" READ_ONLY = True def __init__(self, item, parent_layout): AbstractDataSetWidget.__init__(self, item, parent_layout) self.group = QLabel() wordwrap = item.get_prop_value("display", "wordwrap", False) self.group.setWordWrap(wordwrap) self.group.setToolTip(item.get_help()) self.group.setStyleSheet(LABEL_CSS) self.group.setTextInteractionFlags(Qt.TextSelectableByMouse) # self.group.setEnabled(False) def get(self): """Override AbstractDataSetWidget method""" self.set_state() text = self.item.get_string_value() self.group.setText(text) def set(self): """Read only...""" pass class ShowColorWidget(DataSetShowWidget): """Read-only color item widget""" def __init__(self, item, parent_layout): DataSetShowWidget.__init__(self, item, parent_layout) self.picture = None def get(self): """Override AbstractDataSetWidget method""" value = self.item.get() if value is not None: color = QColor(value) self.picture = QPicture() painter = QPainter() painter.begin(self.picture) painter.fillRect(QRect(0, 0, 60, 20), QBrush(color)) painter.end() self.group.setPicture(self.picture) class ShowBooleanWidget(DataSetShowWidget): """Read-only bool item widget""" def place_on_grid( self, layout, row, label_column, widget_column, row_span=1, column_span=1 ): """Override AbstractDataSetWidget method""" if not self.item.get_prop_value("display", "label"): widget_column = label_column column_span += 1 else: self.place_label(layout, row, label_column) layout.addWidget(self.group, row, widget_column, row_span, column_span) def get(self): """Override AbstractDataSetWidget method""" DataSetShowWidget.get(self) text = self.item.get_prop_value("display", "text") self.group.setText(text) font = self.group.font() value = self.item.get() state = bool(value) font.setStrikeOut(not state) self.group.setFont(font) self.group.setEnabled(state) class DataSetShowLayout(DataSetEditLayout): """Read-only layout""" _widget_factory = {} class DataSetShowDialog(DataSetEditDialog): """Read-only dialog box""" def layout_factory(self, instance, grid): """Override DataSetEditDialog method""" return DataSetShowLayout(self, instance, grid) DataSetShowLayout.register(GroupItem, GroupWidget) DataSetShowLayout.register(TabGroupItem, TabGroupWidget) DataSetShowLayout.register(FloatItem, DataSetShowWidget) DataSetShowLayout.register(StringItem, DataSetShowWidget) DataSetShowLayout.register(TextItem, DataSetShowWidget) DataSetShowLayout.register(IntItem, DataSetShowWidget) DataSetShowLayout.register(BoolItem, ShowBooleanWidget) DataSetShowLayout.register(DateItem, DataSetShowWidget) DataSetShowLayout.register(DateTimeItem, DataSetShowWidget) DataSetShowLayout.register(ColorItem, ShowColorWidget) DataSetShowLayout.register(FileOpenItem, DataSetShowWidget) DataSetShowLayout.register(FilesOpenItem, DataSetShowWidget) DataSetShowLayout.register(FileSaveItem, DataSetShowWidget) DataSetShowLayout.register(DirectoryItem, DataSetShowWidget) DataSetShowLayout.register(ChoiceItem, DataSetShowWidget) DataSetShowLayout.register(ImageChoiceItem, DataSetShowWidget) DataSetShowLayout.register(MultipleChoiceItem, DataSetShowWidget) DataSetShowLayout.register(FloatArrayItem, DataSetShowWidget) class DataSetShowGroupBox(QGroupBox): """Group box widget showing a read-only DataSet""" def __init__(self, label, klass, wordwrap=False, **kwargs): QGroupBox.__init__(self, label) self.apply_button = None self.klass = klass self.dataset = klass(**kwargs) self.layout = QVBoxLayout() if self.dataset.get_comment(): label = QLabel(self.dataset.get_comment()) label.setWordWrap(wordwrap) self.layout.addWidget(label) self.grid_layout = QGridLayout() self.layout.addLayout(self.grid_layout) self.setLayout(self.layout) self.edit = self.get_edit_layout() def get_edit_layout(self): """Return edit layout""" return DataSetShowLayout(self, self.dataset, self.grid_layout) def get(self): """Update group box contents from data item values""" for widget in self.edit.widgets: widget.build_mode = True widget.get() widget.build_mode = False class DataSetEditGroupBox(DataSetShowGroupBox): """ Group box widget including a DataSet label: group box label (string) klass: guidata.DataSet class button_text: action button text (default: "Apply") button_icon: QIcon object or string (default "apply.png") """ #: Signal emitted when Apply button is clicked SIG_APPLY_BUTTON_CLICKED = Signal() def __init__( self, label, klass, button_text=None, button_icon=None, show_button=True, wordwrap=False, **kwargs ): DataSetShowGroupBox.__init__(self, label, klass, wordwrap=wordwrap, **kwargs) if show_button: if button_text is None: button_text = _("Apply") if button_icon is None: button_icon = get_icon("apply.png") elif isinstance(button_icon, str): button_icon = get_icon(button_icon) self.apply_button = applyb = QPushButton(button_icon, button_text, self) applyb.clicked.connect(self.set) layout = self.edit.layout layout.addWidget(applyb, layout.rowCount(), 0, 1, -1, Qt.AlignRight) def get_edit_layout(self): """Return edit layout""" return DataSetEditLayout( self, self.dataset, self.grid_layout, change_callback=self.change_callback ) def change_callback(self): """Method called when any widget's value has changed""" self.set_apply_button_state(True) def set(self): """Update data item values from layout contents""" for widget in self.edit.widgets: if widget.is_active() and widget.check(): widget.set() self.SIG_APPLY_BUTTON_CLICKED.emit() self.set_apply_button_state(False) def set_apply_button_state(self, state): """Set apply button enable/disable state""" if self.apply_button is not None: self.apply_button.setEnabled(state) def child_title(self, item): """Return data item title combined with QApplication title""" app_name = QApplication.applicationName() if not app_name: app_name = str(self.title()) return "%s - %s" % (app_name, item.label()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/dataset/textedit.py0000666000000000000000000000155200000000000015276 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ Text visitor for DataItem objects (for test purpose only) """ def prompt(item): """Get item value""" return input(item.get_prop("display", "label") + " ? ") class TextEditVisitor: """Text visitor""" def __init__(self, instance): self.instance = instance def visit_generic(self, item): """Generic visitor""" while True: value = prompt(item) item.set_from_string(self.instance, value) if item.check_item(self.instance): break print("Incorrect value!") visit_FloatItem = visit_generic visit_IntItem = visit_generic visit_StringItem = visit_generic ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/disthelpers.py0000666000000000000000000011134600000000000014350 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2011 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) # pylint: disable=W0613 """ disthelpers ----------- The ``guidata.disthelpers`` module provides helper functions for Python package distribution on Microsoft Windows platforms with ``py2exe`` or on all platforms thanks to ``cx_Freeze``. """ import sys import os import os.path as osp import shutil import traceback import atexit import imp from subprocess import Popen, PIPE import warnings # Local imports from guidata.configtools import get_module_path # ============================================================================== # Dependency management # ============================================================================== def get_changeset(path, rev=None): """Return Mercurial repository *path* revision number""" args = ["hg", "parent"] if rev is not None: args += ["--rev", str(rev)] process = Popen(args, stdout=PIPE, stderr=PIPE, cwd=path, shell=True) try: return process.stdout.read().splitlines()[0].split()[1] except IndexError: raise RuntimeError(process.stderr.read()) def prepend_module_to_path(module_path): """ Prepend to sys.path module located in *module_path* Return string with module infos: name, revision, changeset Use this function: 1) In your application to import local frozen copies of internal libraries 2) In your py2exe distributed package to add a text file containing the returned string """ if not osp.isdir(module_path): # Assuming py2exe distribution return sys.path.insert(0, osp.abspath(module_path)) changeset = get_changeset(module_path) name = osp.basename(module_path) prefix = "Prepending module to sys.path" message = prefix + ("%s [revision %s]" % (name, changeset)).rjust( 80 - len(prefix), "." ) print(message, file=sys.stderr) if name in sys.modules: sys.modules.pop(name) nbsp = 0 for modname in sys.modules.keys(): if modname.startswith(name + "."): sys.modules.pop(modname) nbsp += 1 warning = "(removed %s from sys.modules" % name if nbsp: warning += " and %d subpackages" % nbsp warning += ")" print(warning.rjust(80), file=sys.stderr) return message def prepend_modules_to_path(module_base_path): """Prepend to sys.path all modules located in *module_base_path*""" if not osp.isdir(module_base_path): # Assuming py2exe distribution return fnames = [osp.join(module_base_path, name) for name in os.listdir(module_base_path)] messages = [ prepend_module_to_path(dirname) for dirname in fnames if osp.isdir(dirname) ] return os.linesep.join(messages) # ============================================================================== # Distribution helpers # ============================================================================== def _remove_later(fname): """Try to remove file later (at exit)""" def try_to_remove(fname): if osp.exists(fname): os.remove(fname) atexit.register(try_to_remove, osp.abspath(fname)) def get_msvc_version(python_version): """Return Microsoft Visual C++ version used to build this Python version""" if python_version is None: python_version = "%s.%s" % (sys.version_info.major, sys.version_info.minor) warnings.warn("Assuming Python %s target" % python_version) if python_version in ("2.6", "2.7", "3.0", "3.1", "3.2"): # Python 2.6-2.7, 3.0-3.2 were built with Visual Studio 9.0.21022.8 # (i.e. Visual C++ 2008, not Visual C++ 2008 SP1!) return "9.0.21022.8" elif python_version in ("3.3", "3.4"): # Python 3.3+ were built with Visual Studio 10.0.30319.1 # (i.e. Visual C++ 2010) return "10.0" else: raise RuntimeError("Unsupported Python version %s" % python_version) def get_dll_architecture(path): """Return DLL architecture (32 or 64bit) using Microsoft dumpbin.exe""" os.environ[ "PATH" ] += r";C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\IDE\;C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\BIN;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\;C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\BIN" process = Popen( ["dumpbin", "/HEADERS", osp.basename(path)], stdout=PIPE, stderr=PIPE, cwd=osp.dirname(path), shell=True, ) output = process.stdout.read() error = process.stderr.read() if error: raise RuntimeError(error) elif "x86" in output: return 32 elif "x64" in output: return 64 else: raise ValueError("Unable to get DLL architecture") def get_msvc_dlls(msvc_version, architecture=None, check_architecture=False): """Get the list of Microsoft Visual C++ DLLs associated to architecture and Python version, create the manifest file. architecture: integer (32 or 64) -- if None, take the Python build arch python_version: X.Y""" current_architecture = 64 if sys.maxsize > 2 ** 32 else 32 if architecture is None: architecture = current_architecture assert architecture in (32, 64) filelist = [] msvc_major = msvc_version.split(".")[0] msvc_minor = msvc_version.split(".")[1] if msvc_major == "9": key = "1fc8b3b9a1e18e3b" atype = "" if architecture == 64 else "win32" arch = "amd64" if architecture == 64 else "x86" groups = { "CRT": ("msvcr90.dll", "msvcp90.dll", "msvcm90.dll"), # 'OPENMP': ('vcomp90.dll',) } for group, dll_list in groups.items(): dlls = "" for dll in dll_list: dlls += ' %s' % (dll, os.linesep) manifest = """ %(dlls)s """ % dict( version=msvc_version, key=key, atype=atype, arch=arch, group=group, dlls=dlls, ) vc90man = "Microsoft.VC90.%s.manifest" % group open(vc90man, "w").write(manifest) _remove_later(vc90man) filelist += [vc90man] winsxs = osp.join(os.environ["windir"], "WinSxS") vcstr = "%s_Microsoft.VC90.%s_%s_%s" % (arch, group, key, msvc_version) for fname in os.listdir(winsxs): path = osp.join(winsxs, fname) if osp.isdir(path) and fname.lower().startswith(vcstr.lower()): for dllname in os.listdir(path): filelist.append(osp.join(path, dllname)) break else: raise RuntimeError( "Microsoft Visual C++ %s DLLs version %s " "were not found" % (group, msvc_version) ) elif msvc_major == "10": namelist = [ name % (msvc_major + msvc_minor) for name in ( "msvcp%s.dll", "msvcr%s.dll", "vcomp%s.dll", ) ] windir = os.environ["windir"] is_64bit_windows = osp.isdir(osp.join(windir, "SysWOW64")) # Reminder: WoW64 (*W*indows 32-bit *o*n *W*indows *64*-bit) is a # subsystem of the Windows operating system capable of running 32-bit # applications and is included on all 64-bit versions of Windows # (source: http://en.wikipedia.org/wiki/WoW64) # # In other words, "SysWOW64" contains 32-bit DLL and applications, # whereas "System32" contains 64-bit DLL and applications on a 64-bit # system. if architecture == 64: # 64-bit DLLs are located in... if is_64bit_windows: sysdir = "System32" # on a 64-bit OS else: # ...no directory to be found! raise RuntimeError("Can't find 64-bit DLLs on a 32-bit OS") else: # 32-bit DLLs are located in... if is_64bit_windows: sysdir = "SysWOW64" # on a 64-bit OS else: sysdir = "System32" # on a 32-bit OS for dllname in namelist: fname = osp.join(windir, sysdir, dllname) if osp.exists(fname): filelist.append(fname) else: raise RuntimeError( "Microsoft Visual C++ DLLs version %s " "were not found" % msvc_version ) else: raise RuntimeError("Unsupported MSVC version %s" % msvc_version) if check_architecture: for path in filelist: if path.endswith(".dll"): try: arch = get_dll_architecture(path) except RuntimeError: return if arch != architecture: raise RuntimeError( "%s: expecting %dbit, found %dbit" % (path, architecture, arch) ) return filelist def create_msvc_data_files(architecture=None, python_version=None, verbose=False): """Including Microsoft Visual C++ DLLs""" msvc_version = get_msvc_version(python_version) filelist = get_msvc_dlls(msvc_version, architecture=architecture) print(create_msvc_data_files.__doc__) if verbose: for name in filelist: print(" ", name) msvc_major = msvc_version.split(".")[0] if msvc_major == "9": return [ ("Microsoft.VC90.CRT", filelist), ] else: return [ ("", filelist), ] def to_include_files(data_files): """Convert data_files list to include_files list data_files: * this is the ``py2exe`` data files format * list of tuples (dest_dirname, (src_fname1, src_fname2, ...)) include_files: * this is the ``cx_Freeze`` data files format * list of tuples ((src_fname1, dst_fname1), (src_fname2, dst_fname2), ...)) """ include_files = [] for dest_dir, fnames in data_files: for source_fname in fnames: dest_fname = osp.join(dest_dir, osp.basename(source_fname)) include_files.append((source_fname, dest_fname)) return include_files def strip_version(version): """Return version number with digits only (Windows does not support strings in version numbers)""" return version.split("beta")[0].split("alpha")[0].split("rc")[0].split("dev")[0] def remove_dir(dirname): """Remove directory *dirname* and all its contents Print details about the operation (progress, success/failure)""" print("Removing directory '%s'..." % dirname, end=" ") try: shutil.rmtree(dirname, ignore_errors=True) print("OK") except Exception: print("Failed!") traceback.print_exc() class Distribution(object): """Distribution object Help creating an executable using ``py2exe`` or ``cx_Freeze`` """ DEFAULT_EXCLUDES = [ "Tkconstants", "Tkinter", "tcl", "tk", "wx", "_imagingtk", "curses", "PIL._imagingtk", "ImageTk", "PIL.ImageTk", "FixTk", "bsddb", "email", "pywin.debugger", "pywin.debugger.dbgcon", "matplotlib", ] if sys.version_info.major == 2: # Fixes compatibility issue with IPython (more specifically with one # of its dependencies: `jsonschema`) on Python 2.7 DEFAULT_EXCLUDES += ["collections.abc"] DEFAULT_INCLUDES = [] DEFAULT_BIN_EXCLUDES = [ "MSVCP100.dll", "MSVCP90.dll", "w9xpopen.exe", "MSVCP80.dll", "MSVCR80.dll", ] DEFAULT_BIN_INCLUDES = [] DEFAULT_BIN_PATH_INCLUDES = [] DEFAULT_BIN_PATH_EXCLUDES = [] def __init__(self): self.name = None self.version = None self.description = None self.script = None self.target_name = None self._target_dir = None self.icon = None self.data_files = [] self.includes = self.DEFAULT_INCLUDES self.excludes = self.DEFAULT_EXCLUDES self.bin_includes = self.DEFAULT_BIN_INCLUDES self.bin_excludes = self.DEFAULT_BIN_EXCLUDES self.bin_path_includes = self.DEFAULT_BIN_PATH_INCLUDES self.bin_path_excludes = self.DEFAULT_BIN_PATH_EXCLUDES self.msvc = os.name == "nt" self._py2exe_is_loaded = False self._pyqt_added = False self._pyside_added = False # Attributes relative to cx_Freeze: self.executables = [] @property def target_dir(self): """Return target directory (default: 'dist')""" dirname = self._target_dir if dirname is None: return "dist" else: return dirname @target_dir.setter # analysis:ignore def target_dir(self, value): self._target_dir = value def setup( self, name, version, description, script, target_name=None, target_dir=None, icon=None, data_files=None, includes=None, excludes=None, bin_includes=None, bin_excludes=None, bin_path_includes=None, bin_path_excludes=None, msvc=None, ): """Setup distribution object Notes: * bin_path_excludes is specific to cx_Freeze (ignored if it's None) * if msvc is None, it's set to True by default on Windows platforms, False on non-Windows platforms """ self.name = name self.version = strip_version(version) if os.name == "nt" else version self.description = description assert osp.isfile(script) self.script = script self.target_name = target_name self.target_dir = target_dir self.icon = icon if data_files is not None: self.data_files += data_files if includes is not None: self.includes += includes if excludes is not None: self.excludes += excludes if bin_includes is not None: self.bin_includes += bin_includes if bin_excludes is not None: self.bin_excludes += bin_excludes if bin_path_includes is not None: self.bin_path_includes += bin_path_includes if bin_path_excludes is not None: self.bin_path_excludes += bin_path_excludes if msvc is not None: self.msvc = msvc if self.msvc: try: self.data_files += create_msvc_data_files() except IOError: print( "Setting the msvc option to False " "will avoid this error", file=sys.stderr, ) raise # cx_Freeze: self.add_executable(self.script, self.target_name, icon=self.icon) def add_text_data_file(self, filename, contents): """Create temporary data file *filename* with *contents* and add it to *data_files*""" open(filename, "wb").write(bytes(contents, "utf-8")) self.data_files += [("", (filename,))] _remove_later(filename) def add_data_file(self, filename, destdir=""): self.data_files += [(destdir, (filename,))] # ------ Adding packages def add_pyqt(self): """Include module PyQt5 to the distribution""" # TODO: Add PyQt6 support if self._pyqt_added: return self._pyqt_added = True import PyQt5 as PyQt qtver = 5 self.includes += [ "sip", "PyQt%d.Qt" % qtver, "PyQt%d.QtSvg" % qtver, "PyQt%d.QtNetwork" % qtver, ] pyqt_path = osp.dirname(PyQt.__file__) # Configuring PyQt conf = os.linesep.join(["[Paths]", "Prefix = .", "Binaries = ."]) self.add_text_data_file("qt.conf", conf) # Including plugins (.svg icons support, QtDesigner support, ...) if self.msvc: vc90man = "Microsoft.VC90.CRT.manifest" pyqt_tmp = "pyqt_tmp" if osp.isdir(pyqt_tmp): shutil.rmtree(pyqt_tmp) os.mkdir(pyqt_tmp) vc90man_pyqt = osp.join(pyqt_tmp, vc90man) if osp.isfile(vc90man): man = ( open(vc90man, "r") .read() .replace('= 10: # Checks if running Windows 10 version 10.0.14393 (Anniversary Update) OR HIGHER. The getwindowsversion method returns a tuple. # The third item is the build number that we can use to check if the user has a new enough version of Windows. winver = int(platform.version().split('.')[2]) if winver >= 14393: from ._windows_detect import * else: from ._dummy import * elif sys.platform == "linux": from ._linux_detect import * else: from ._dummy import * del sys, platform././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/external/darkdetect/_dummy.py0000666000000000000000000000057100000000000017245 0ustar00#----------------------------------------------------------------------------- # Copyright (C) 2019 Alberto Sottile # # Distributed under the terms of the 3-clause BSD License. #----------------------------------------------------------------------------- def theme(): return None def isDark(): return None def isLight(): return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/external/darkdetect/_linux_detect.py0000666000000000000000000000157200000000000020603 0ustar00#----------------------------------------------------------------------------- # Copyright (C) 2019 Alberto Sottile, Eric Larson # # Distributed under the terms of the 3-clause BSD License. #----------------------------------------------------------------------------- import subprocess def theme(): # Here we just triage to GTK settings for now try: out = subprocess.run( ['gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-theme'], capture_output=True) stdout = out.stdout.decode() except Exception: return 'Light' # we have a string, now remove start and end quote theme = stdout.lower().strip()[1:-1] if theme.endswith('-dark'): return 'Dark' else: return 'Light' def isDark(): return theme() == 'Dark' def isLight(): return theme() == 'Light' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/external/darkdetect/_mac_detect.py0000666000000000000000000000424400000000000020203 0ustar00#----------------------------------------------------------------------------- # Copyright (C) 2019 Alberto Sottile # # Distributed under the terms of the 3-clause BSD License. #----------------------------------------------------------------------------- import ctypes import ctypes.util try: # macOS Big Sur+ use "a built-in dynamic linker cache of all system-provided libraries" appkit = ctypes.cdll.LoadLibrary('AppKit.framework/AppKit') objc = ctypes.cdll.LoadLibrary('libobjc.dylib') except OSError: # revert to full path for older OS versions and hardened programs appkit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('AppKit')) objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc')) void_p = ctypes.c_void_p ull = ctypes.c_uint64 objc.objc_getClass.restype = void_p objc.sel_registerName.restype = void_p # See https://docs.python.org/3/library/ctypes.html#function-prototypes for arguments description MSGPROTOTYPE = ctypes.CFUNCTYPE(void_p, void_p, void_p, void_p) msg = MSGPROTOTYPE(('objc_msgSend', objc), ((1 ,'', None), (1, '', None), (1, '', None))) def _utf8(s): if not isinstance(s, bytes): s = s.encode('utf8') return s def n(name): return objc.sel_registerName(_utf8(name)) def C(classname): return objc.objc_getClass(_utf8(classname)) def theme(): NSAutoreleasePool = objc.objc_getClass('NSAutoreleasePool') pool = msg(NSAutoreleasePool, n('alloc')) pool = msg(pool, n('init')) NSUserDefaults = C('NSUserDefaults') stdUserDef = msg(NSUserDefaults, n('standardUserDefaults')) NSString = C('NSString') key = msg(NSString, n("stringWithUTF8String:"), _utf8('AppleInterfaceStyle')) appearanceNS = msg(stdUserDef, n('stringForKey:'), void_p(key)) appearanceC = msg(appearanceNS, n('UTF8String')) if appearanceC is not None: out = ctypes.string_at(appearanceC) else: out = None msg(pool, n('release')) if out is not None: return out.decode('utf-8') else: return 'Light' def isDark(): return theme() == 'Dark' def isLight(): return theme() == 'Light' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/external/darkdetect/_windows_detect.py0000666000000000000000000000235100000000000021132 0ustar00from winreg import HKEY_CURRENT_USER as hkey, QueryValueEx as getSubkeyValue, OpenKey as getKey def theme(): """ Uses the Windows Registry to detect if the user is using Dark Mode """ # Registry will return 0 if Windows is in Dark Mode and 1 if Windows is in Light Mode. This dictionary converts that output into the text that the program is expecting. valueMeaning = {0: "Dark", 1: "Light"} # In HKEY_CURRENT_USER, get the Personalisation Key. try: key = getKey(hkey, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize") # In the Personalisation Key, get the AppsUseLightTheme subkey. This returns a tuple. # The first item in the tuple is the result we want (0 or 1 indicating Dark Mode or Light Mode); the other value is the type of subkey e.g. DWORD, QWORD, String, etc. subkey = getSubkeyValue(key, "AppsUseLightTheme")[0] except FileNotFoundError: # some headless Windows instances (e.g. GitHub Actions or Docker images) do not have this key return None return valueMeaning[subkey] def isDark(): if theme() is not None: return theme() == 'Dark' def isLight(): if theme() is not None: return theme() == 'Light'././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/gettext_helpers.py0000666000000000000000000000775700000000000015242 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) import sys import os import os.path as osp import subprocess if os.name == "nt": # Find pygettext.py source on a windows install pygettext = ["python", osp.join(sys.prefix, "Tools", "i18n", "pygettext.py")] msgfmt = ["python", osp.join(sys.prefix, "Tools", "i18n", "msgfmt.py")] else: pygettext = ["pygettext"] msgfmt = ["msgfmt"] def get_files(modname): if not osp.isdir(modname): return [modname] files = [] for dirname, _dirnames, filenames in os.walk(modname): files += [ osp.join(dirname, f) for f in filenames if f.endswith(".py") or f.endswith(".pyw") ] for dirname, _dirnames, filenames in os.walk("tests"): files += [ osp.join(dirname, f) for f in filenames if f.endswith(".py") or f.endswith(".pyw") ] return files def get_lang(modname): localedir = osp.join(modname, "locale") for _dirname, dirnames, _filenames in os.walk(localedir): break # we just want the list of first level directories return dirnames def do_rescan(modname): files = get_files(modname) dirname = modname do_rescan_files(files, modname, dirname) def do_rescan_files(files, modname, dirname): localedir = osp.join(dirname, "locale") potfile = modname + ".pot" subprocess.call( pygettext + [ ##"-D", # Extract docstrings "-o", potfile, # Nom du fichier pot "-p", localedir, # dest ] + files ) for lang in get_lang(dirname): pofilepath = osp.join(localedir, lang, "LC_MESSAGES", modname + ".po") potfilepath = osp.join(localedir, potfile) print("Updating...", pofilepath) if not osp.exists(osp.join(localedir, lang, "LC_MESSAGES")): os.mkdir(osp.join(localedir, lang, "LC_MESSAGES")) if not osp.exists(pofilepath): outf = open(pofilepath, "w") outf.write("# -*- coding: utf-8 -*-\n") data = open(potfilepath).read() data = data.replace("charset=CHARSET", "charset=utf-8") data = data.replace( "Content-Transfer-Encoding: ENCODING", "Content-Transfer-Encoding: utf-8", ) outf.write(data) else: print("merge...") subprocess.call(["msgmerge", "-o", pofilepath, pofilepath, potfilepath]) def do_compile(modname, dirname=None): if dirname is None: dirname = modname localedir = osp.join(dirname, "locale") for lang in get_lang(dirname): pofilepath = osp.join(localedir, lang, "LC_MESSAGES", modname + ".po") subprocess.call(msgfmt + [pofilepath]) def main(modname): if len(sys.argv) < 2: cmd = "help" else: cmd = sys.argv[1] # lang = get_lang( modname ) if cmd == "help": print("Available commands:") print(" help : this message") print(" help_gettext : pygettext --help") print(" help_msgfmt : msgfmt --help") print(" scan : rescan .py files and updates existing .po files") print(" compile : recompile .po files") print() print("Pour fonctionner ce programme doit être lancé depuis") print("la racine du module") print("Traductions disponibles:") for i in get_lang(modname): print(i) elif cmd == "help_gettext": subprocess.call(pygettext + ["--help"]) elif cmd == "help_msgfmt": subprocess.call(msgfmt + ["--help"]) elif cmd == "scan": print("Updating pot files") do_rescan(modname) elif cmd == "compile": print("Builtin .mo files") do_compile(modname) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/guitest.py0000666000000000000000000001631000000000000013501 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ GUI-based test launcher """ import sys import os import os.path as osp import subprocess import traceback # Local imports from qtpy.QtWidgets import ( QWidget, QVBoxLayout, QSplitter, QListWidget, QPushButton, QLabel, QGroupBox, QHBoxLayout, QShortcut, QMainWindow, QFrame, ) from qtpy.QtGui import QKeySequence, QColor from qtpy.QtCore import Qt, QSize from guidata.config import _ from guidata.configtools import get_icon from guidata.qthelpers import get_std_icon, win32_fix_title_bar_background from guidata.widgets.codeeditor import PythonCodeEditor def get_test_package(package): """Return test package for package""" test_package_name = "%s.tests" % package.__name__ _temp = __import__(test_package_name) return sys.modules[test_package_name] def get_tests(package): """Retrieve test scripts from test package""" tests = [] test_package = get_test_package(package) test_path = osp.dirname(osp.realpath(test_package.__file__)) for fname in sorted(os.listdir(test_path)): path = osp.join(test_path, fname) if fname.endswith((".py", ".pyw")) and not fname.startswith("_"): test = TestModule(test_package, path) if test.is_visible(): tests.append(test) return tests class TestModule(object): """Object representing a test module (Python script)""" def __init__(self, test_package, path): self.path = path module_name, _ext = osp.splitext(osp.basename(path)) try: self.error_msg = "" _temp = __import__(test_package.__name__, fromlist=[module_name]) self.module = getattr(_temp, module_name) except ImportError: self.error_msg = traceback.format_exc() self.module = None def is_visible(self): """Returns True if this script is intended to be shown in test launcher""" return self.module is None or ( hasattr(self.module, "SHOW") and self.module.SHOW ) def is_valid(self): """Returns True if test module is valid and can be executed""" return self.module is not None def get_description(self): """Returns test module description""" if self.is_valid(): doc = self.module.__doc__ if doc is None or not doc.strip(): return _("No description available") lines = doc.strip().splitlines() fmt = "%s" lines[0] = fmt % lines[0] return "
".join(lines) return self.error_msg def run(self, args=""): """Run test script""" # Keep the same sys.path environment in child process: # (useful when the program is executed from Spyder, for example) os.environ["PYTHONPATH"] = os.pathsep.join(sys.path) command = [sys.executable, '"' + self.path + '"'] if args: command.append(args) subprocess.Popen(" ".join(command), shell=True) class TestPropertiesWidget(QWidget): """Test module properties panel""" def __init__(self, parent): QWidget.__init__(self, parent) self.lbl_icon = QLabel() self.lbl_icon.setFixedWidth(32) self.desc_label = QLabel() self.desc_label.setTextInteractionFlags(Qt.TextSelectableByMouse) self.desc_label.setWordWrap(True) group_desc = QGroupBox(_("Description"), self) layout = QHBoxLayout() for label in (self.lbl_icon, self.desc_label): label.setAlignment(Qt.AlignTop) layout.addWidget(label) group_desc.setLayout(layout) self.editor = PythonCodeEditor(self, columns=85, rows=30) self.editor.setReadOnly(True) self.desc_label.setFont(self.editor.font()) vlayout = QVBoxLayout() vlayout.addWidget(group_desc) vlayout.addWidget(self.editor) self.setLayout(vlayout) def set_item(self, test): """Set current item""" self.desc_label.setText(test.get_description()) self.editor.set_text_from_file(test.path) txt = "Information" if test.is_valid() else "Critical" self.lbl_icon.setPixmap(get_std_icon("MessageBox" + txt).pixmap(24, 24)) class TestMainView(QSplitter): """Test launcher main view""" def __init__(self, package, parent=None): QSplitter.__init__(self, parent) self.tests = get_tests(package) listgroup = QFrame() self.addWidget(listgroup) self.props = TestPropertiesWidget(self) font = self.props.editor.font() self.addWidget(self.props) vlayout = QVBoxLayout() self.run_button = self.create_run_button(font) self.listw = self.create_test_listwidget(font) vlayout.addWidget(self.listw) vlayout.addWidget(self.run_button) listgroup.setLayout(vlayout) self.setStretchFactor(1, 1) self.props.set_item(self.tests[0]) def create_test_listwidget(self, font): """Create and setup test list widget""" listw = QListWidget(self) listw.addItems([osp.basename(test.path) for test in self.tests]) for index in range(listw.count()): item = listw.item(index) item.setSizeHint(QSize(1, 25)) if not self.tests[index].is_valid(): item.setForeground(QColor("#FF3333")) listw.setFont(font) listw.currentRowChanged.connect(self.current_row_changed) listw.itemActivated.connect(self.run_current_script) listw.setCurrentRow(0) return listw def create_run_button(self, font): """Create and setup run button""" btn = QPushButton(get_icon("apply.png"), _("Run this script"), self) btn.setFont(font) btn.clicked.connect(self.run_current_script) return btn def current_row_changed(self, row): """Current list widget row has changed""" current_test = self.tests[row] self.props.set_item(current_test) self.run_button.setEnabled(current_test.is_valid()) def run_current_script(self): """Run current script""" self.tests[self.listw.currentRow()].run() class TestLauncherWindow(QMainWindow): """Test launcher main window""" def __init__(self, package, parent=None): QMainWindow.__init__(self, parent) win32_fix_title_bar_background(self) self.setWindowTitle(_("Tests - %s module") % package.__name__) self.setWindowIcon(get_icon("%s.svg" % package.__name__, "guidata.svg")) self.mainview = TestMainView(package, self) self.setCentralWidget(self.mainview) QShortcut(QKeySequence("Escape"), self, self.close) def run_testlauncher(package): """Run test launcher""" from guidata import qapplication app = qapplication() win = TestLauncherWindow(package) win.show() app.exec_() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/hdf5io.py0000666000000000000000000003052500000000000013177 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ Reader and Writer for the serialization of DataSets into HDF5 files """ import sys from uuid import uuid1 import h5py import numpy as np from guidata.utils import utf8_to_unicode from guidata.userconfigio import BaseIOHandler, WriterMixin class TypeConverter(object): def __init__(self, to_type, from_type=None): self._to_type = to_type if from_type: self._from_type = from_type else: self._from_type = to_type def to_hdf(self, value): try: return self._to_type(value) except: print("ERR", repr(value), file=sys.stderr) raise def from_hdf(self, value): return self._from_type(value) unicode_hdf = TypeConverter(lambda x: x.encode("utf-8"), lambda x: str(x, "utf-8")) int_hdf = TypeConverter(int) class Attr(object): """Helper class representing class attribute that should be saved/restored to/from a corresponding HDF5 attribute hdf_name : name of the attribute in the HDF5 file struct_name : name of the attribute in the object (default to hdf_name) type : attribute type (guess it if None) optional : indicates whether we should fail if the attribute is not present """ def __init__(self, hdf_name, struct_name=None, type=None, optional=False): self.hdf_name = hdf_name if struct_name is None: struct_name = hdf_name self.struct_name = struct_name self.type = type self.optional = optional def get_value(self, struct): if self.optional: return getattr(struct, self.struct_name, None) else: return getattr(struct, self.struct_name) def set_value(self, struct, value): setattr(struct, self.struct_name, value) def save(self, group, struct): value = self.get_value(struct) if self.optional and value is None: # print ".-", self.hdf_name, value if self.hdf_name in group.attrs: del group.attrs[self.hdf_name] return if self.type is not None: value = self.type.to_hdf(value) # print ".", self.hdf_name, value, self.optional try: group.attrs[self.hdf_name] = value except: print("ERROR saving:", repr(value), "into", self.hdf_name, file=sys.stderr) raise def load(self, group, struct): # print "LoadAttr:", group, self.hdf_name if self.optional: if self.hdf_name not in group.attrs: self.set_value(struct, None) return try: value = group.attrs[self.hdf_name] except KeyError: raise KeyError("Unable to locate attribute %s" % self.hdf_name) if self.type is not None: value = self.type.from_hdf(value) self.set_value(struct, value) def createdset(group, name, value): group.create_dataset( name, compression=None, # compression_opts=3, data=value, ) class Dset(Attr): """ Generic load/save for an hdf5 dataset: scalar=float -> used to convert the value when it is scalar """ def __init__( self, hdf_name, struct_name=None, type=None, scalar=None, optional=False ): Attr.__init__(self, hdf_name, struct_name, type, optional) self.scalar = scalar def save(self, group, struct): value = self.get_value(struct) if isinstance(value, float): value = np.float64(value) elif isinstance(value, int): value = np.int32(value) if value is None or value.size == 0: value = np.array([0.0]) if value.shape == (): value = value.reshape((1,)) group.require_dataset( self.hdf_name, shape=value.shape, dtype=value.dtype, data=value, compression="gzip", compression_opts=1, ) def load(self, group, struct): if self.optional: if self.hdf_name not in group: self.set_value(struct, None) return try: value = group[self.hdf_name][...] except KeyError: raise KeyError("Unable to locate dataset %s" % self.hdf_name) if self.scalar is not None: value = self.scalar(value) self.set_value(struct, value) class Dlist(Dset): def get_value(self, struct): return np.array(getattr(struct, self.struct_name)) def set_value(self, struct, value): setattr(struct, self.struct_name, list(value)) # ============================================================================== # Base HDF5 Store object: do not break API compatibility here as this class is # used in various critical projects for saving/loading application data # ============================================================================== class H5Store(object): def __init__(self, filename): self.filename = filename self.h5 = None def open(self, mode="a"): """Open an hdf5 file""" if self.h5: return self.h5 try: self.h5 = h5py.File(self.filename, mode=mode) except Exception: print( "Error trying to load:", self.filename, "in mode:", mode, file=sys.stderr, ) raise return self.h5 def close(self): if self.h5: self.h5.close() self.h5 = None def generic_save(self, parent, source, structure): """save the data from source into the file using 'structure' as a descriptor. structure is a list of Attribute Descriptor (Attr, Dset, Dlist or anything with a save interface) that describe the conversion of data and the name of the attribute in the source and in the file """ for instr in structure: instr.save(parent, source) def generic_load(self, parent, dest, structure): """load the data from the file into dest using 'structure' as a descriptor. structure is the same as in generic_save """ for instr in structure: try: instr.load(parent, dest) except Exception: print("Error loading HDF5 item:", instr.hdf_name, file=sys.stderr) raise # ============================================================================== # HDF5 reader/writer: do not break API compatibility here as this class is # used in various critical projects for saving/loading application data and # in guiqwt for saving/loading plot items. # ============================================================================== class HDF5Handler(H5Store, BaseIOHandler): """Base HDF5 I/O Handler object""" def __init__(self, filename): H5Store.__init__(self, filename) self.option = [] def get_parent_group(self): parent = self.h5 for option in self.option[:-1]: parent = parent.require_group(option) return parent class HDF5Writer(HDF5Handler, WriterMixin): """Writer for HDF5 files""" def __init__(self, filename): super(HDF5Writer, self).__init__(filename) self.open("w") def write_any(self, val): group = self.get_parent_group() group.attrs[self.option[-1]] = val write_int = write_float = write_any def write_bool(self, val): self.write_int(int(val)) write_str = write_any def write_unicode(self, val): group = self.get_parent_group() group.attrs[self.option[-1]] = val.encode("utf-8") write_unicode = write_str def write_array(self, val): group = self.get_parent_group() group[self.option[-1]] = val write_sequence = write_any def write_none(self): group = self.get_parent_group() group.attrs[self.option[-1]] = "" def write_object_list(self, seq, group_name): """Write object sequence in group. Objects must implement the DataSet-like `serialize` method""" with self.group(group_name): if seq is None: self.write_none() else: ids = [] for obj in seq: guid = bytes(str(uuid1()), "utf-8") ids.append(guid) with self.group(guid): if obj is None: self.write_none() else: obj.serialize(self) self.write(ids, "IDs") class HDF5Reader(HDF5Handler): """Reader for HDF5 files""" def __init__(self, filename): super(HDF5Reader, self).__init__(filename) self.open("r") def read(self, group_name=None, func=None, instance=None): """Read value within current group or group_name. Optional argument `instance` is an object which implements the DataSet-like `deserialize` method.""" if group_name: self.begin(group_name) if instance is None: if func is None: func = self.read_any val = func() else: group = self.get_parent_group() if group_name in group.attrs: # This is an attribute (not a group), meaning that # the object was None when deserializing it val = None else: instance.deserialize(self) val = instance if group_name: self.end(group_name) return val def read_any(self): group = self.get_parent_group() value = group.attrs[self.option[-1]] if isinstance(value, bytes): return value.decode("utf-8") else: return value def read_bool(self): val = self.read_any() if val != "": return bool(val) def read_int(self): val = self.read_any() if val != "": return int(val) def read_float(self): val = self.read_any() if val != "": return float(val) read_unicode = read_str = read_any def read_array(self): group = self.get_parent_group() return group[self.option[-1]][...] def read_sequence(self): group = self.get_parent_group() return list(group.attrs[self.option[-1]]) def read_object_list(self, group_name, klass, progress_callback=None): """Read object sequence in group. Objects must implement the DataSet-like `deserialize` method. `klass` is the object class which constructor requires no argument. progress_callback: if not None, this function is called with an integer argument (progress: 0 --> 100). Function returns the `cancel` state (True: progress dialog has been canceled, False otherwise) """ with self.group(group_name): try: ids = self.read("IDs", func=self.read_sequence) except ValueError: # None was saved instead of list of objects self.end("IDs") return seq = [] count = len(ids) for idx, name in enumerate(ids): if progress_callback is not None: if progress_callback(int(100 * float(idx) / count)): break with self.group(name): group = self.get_parent_group() if name in group.attrs: # This is an attribute (not a group), meaning that # the object was None when deserializing it obj = None else: obj = klass() obj.deserialize(self) seq.append(obj) return seq read_none = read_any ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640626111.9880278 guidata-2.0.2/guidata/images/0000777000000000000000000000000000000000000012707 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/apply.png0000666000000000000000000000142100000000000014540 0ustar00PNG  IHDR(-SgAMA a cHRMz&u0`:pQ<5PLTE v] xMwE wS H:4D?  JF ID5/"LHRf](#%} WfO0$ bO0$l +T@6rI:XWN"t rd _ #gY׺]V׶ZRA:QIܴZQ `U% ݵکj`7(/"yoߝߘtiI9=1_S䈖厥|qYJL?^OxygXVJo`xieXw;tRNS ͷ д Ѳ5 A <   #lbKGDHtIMEURIDATc`L,(|V6kv$>9:9 00 00{K00HJyzIy+*** S ԂP )9 &f(4Dnd X%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/arredit.png0000666000000000000000000000214300000000000015047 0ustar00PNG  IHDR DgAMA7 cHRMz&u0`:pQ<nPLTE~{|z{zzyyywxxuwwtvvsuurttq}}|ssrookmmillhkkgjjfiieiifmmkxxxooliighhehhfhhdggcdd`eeaYYTXXSVVQXXT\\[YYmm4eOKtRNSk%&($i}jbKGDKi PtIMEURwIDAT8˅WSP`aLB`/DJ n!od6Ei?084<2:6>19s$KU  0O#qD(ф e!!, & B]Xi"U edOcѼPPḛۻB0`ǀPS^y^61пI$Q1,fi @Ƞ JPP0ڧꠤ*ak[l6 @ %m 2(w+|@QJho )>ˍ.x]Ʀqy=hI%nZ<s/j}%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErtEXtSoftwarePaint.NET v3.30@GIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/busy.png0000666000000000000000000000132100000000000014374 0ustar00PNG  IHDR(-SgAMA|Q cHRMR@}y<seघ?s U-U}3 b9a%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErtEXtSoftwarePaint.NET v3.36%IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/dictedit.png0000666000000000000000000000161700000000000015213 0ustar00PNG  IHDR DgAMA7 cHRMz&u0`:pQ<PLTE~{|z{zzyyywxxuwwtvvsuurttq}}|ssrookmmillhkkgjjfiieiifmmkxxxooliighhehhfhhdggcdd`eeaYYTXXSVVQXXT\\[m%vKtRNSk%&($i}jbKGDYtIMEURIDAT8˅kO0NE@P@T. 0&L/ M??~|}eee FFFܰأߜ󳱱ߡб񰰳ċ5tRNS; o[<@CCCCCCCCF8cbKGDH pHYs  tIMEURIDAT-gWP UT{p EU?ǴoyΛ7PQYeFuM$R[W+?&RCsEDH- Yen"a(shuMpKxҹnvpexn~wo\} O^zN/.uN9)[UUww𨋚3!<=^K7" GMKFF?}LNM%Tjaqi`46%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640626112.0036533 guidata-2.0.2/guidata/images/editors/0000777000000000000000000000000000000000000014360 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/editors/edit.png0000666000000000000000000000164100000000000016015 0ustar00PNG  IHDR(-SgAMA|Q cHRMz%u0`:o_F\PLTEĠssrQPQ@>?~|}eee FFFܰأߜ󳱱ߡб񰰳ċ5tRNS; o[<@CCCCCCCCF8cbKGDH pHYs  tIMEURIDAT-gWP UT{p EU?ǴoyΛ7PQYeFuM$R[W+?&RCsEDH- Yen"a(shuMpKxҹnvpexn~wo\} O^zN/.uN9)[UUww𨋚3!<=^K7" GMKFF?}LNM%Tjaqi`46%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/editors/edit_add.png0000666000000000000000000000175100000000000016627 0ustar00PNG  IHDR(-SgAMA|Q cHRMz%u0`:o_FPLTE;r>:q=?yBFGGJ@yC8m;7n:4f83i75j84f76h:;q>;l?:e>5f93f7-Y1b_QP3b86k95j94j87m;:q=9z:h^YR@yA1{1C~C0v1@@7v8 <1>y?7h:6h:8m<>lA;g?>jA:f>:f?;q>9x:2254:{;MMeakhRR_\r|ed^]ympg\[d`xk_XUtaR95E=[Vmiif`NN?SP[W_\eb='bRp`VD=/;.G7**(A:*)"@733+<8?tRNS#,,***##",***LL***,"",***LL**,##***,,#h 7xbKGDi+9 pHYs  tIMEURIDATc`F&fVF`wptbGpvquDpBばyDD%$CBee#"cbSR32sr KJʕ+*kTjT54ut[Z Vwt ô YDs$ILD3,%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/editors/editcopy.png0000666000000000000000000000203000000000000016701 0ustar00PNG  IHDR(-SgAMA|Q cHRMz%u0`:o_FPLTE;;:wwwvvv000eegPPP:::LLLDDEMMLTTUMMM***xxykkk墣NNM444ݡNNNǟRRRAA@&&&bbbfffUUUlllqrtLMNTUU@@A%%%?>?))*GGG677ǵGPtRNS: pU3 Þ|V3 ͖"~θuV72tN1 ݝdhbKGDH pHYs  tIMEURIDATc`F&fV66vN.7 ED@ 8@LA" $4,< $e"cb ^LAN> !1&) RR32 KEA5 ;'7/ ԀEũ%`P*Hͯ*m]Z07``04ohllljln16a`05kmk,*1``0qdk[;{89{x2?d H^L%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/editors/editdelete.png0000666000000000000000000000265500000000000017206 0ustar00PNG  IHDR(-SgAMA|Q cHRMz%u0`:o_FPLTEv]r\[30h=q^mtKFDGH-12$73;852 :6=:ZS^YE>s N<y IBy!F2T@^ /K Sij= Up{iaJbanz ta$_g KZwK_%Muvv mF  !2 ,8hp"#(DddF)%z¼ÿnhlfz{vkd84>;kfwoMHHC:4F>E@MF&"$"805.   ~`d w s        2(E5 72NGPH:3,tRNS^s N[e|CX dAgTm Ȑ fש Ȏ9sS}7ܭQSl>RC|Oz:bKGD۶x pHYs  tIMEURIDAT()*+,-./0123 4567  89:;<=>? @ABCD EFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ツ뇈 !"# !$%&'scO!x%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/editors/editpaste.png0000666000000000000000000000241700000000000017054 0ustar00PNG  IHDR(-SgAMA|Q cHRMz%u0`:o_FPLTEtLk8s[?=89:=hefgbmGjf\wPuHl;tIǒg2]+]/\`0\^-yQ[*^G,26?889:::Z(򚚛Y'X&W%~V$~U"}T!}T!vLxP|S |R |R uI|lPSNBz|z{{aaaٰ{aVخźŹŹ˿Ƚy]|Lѩe}LzGjCd@b;a=kHkCͣ{•g}{ysr͝tvxD]꛾șp_8Q@O[YlR{TE;UragQ}I`{h~\azFl[# !~j\vEm:b-s`ZtCsBf.xZvPh=ePDP2 #s25'3 33gs\M&\ pB)uA!^-v^;YqNɖ=Ac2JT*W ,f T jmث >|SŒs!-*JMi&QԨ'3fÀ @Pň^VxsgR'@+%ꎩJ)b&.ԨJ| Cāf)W UY' (Gb[ѱjlMHjoH黬&l ?uhAPDiCĄ& },~T-hD' }((ݝ0X.(k-1(Xzc Q `c'J+2h1JP% Zy5g6sy9F ã~tPgGX@D q|ANJFByGĜa){ BTr]xOB 1JBrPf̵SnOpO2/s-_ Z.EB21Ffp<Ҙq.ċٍmO )ѦPiH. gWs{/cuu"A*BQ.paF {"C)`J0 & Y~o3VRk1E}u#;~I[$EOє&OH|B" Ir$Ɖ=.dIJ >,H31N/%Zc5λf뾊BWvRM6pݣqq/@OS'ec[olY ɢ DmV~AmQ%`j a=lYw[bCW0A̿|7p%JVƽ$7a8 [WA>BVX,7|5qXbpp9iKW}-ޡ330`TǟJ!8sʯQJl9/Gi8VdopG^ `ؾ}"pjƻ.Z5k .ް>n_Ak5ysw2F-H[s9I=ByL/Q~g#V#ڃR̎CwLxA,%qx?%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/editors/filesave.png0000666000000000000000000000217000000000000016664 0ustar00PNG  IHDRw=gAMA a cHRMz&u0`:pQ<bKGDIDATHǝˋ\E{nOc8!N MpIH\H DA+ܻtޅ;7".ąBdKF#><\۝.֭s|_ٳoOǎ?ugϔWDVK)s;?|A_n@NFO;͛7˼.]z87_.# g2/]z_nnJIRp"a6@#0Sl !ܝNq+k $CfD?(j $@DO-ڮ`ڭY0Pe@1QKQm}.rDA@WS]2fK "U(?$徖h4_ޝۜ?’c~&\wbUF@o?CBF?wOwNzn``gᒐbCR!,1CE -j*(P'RT 2cz%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/editors/plot.png0000666000000000000000000000100300000000000016036 0ustar00PNG  IHDR(-SgAMA|Q cHRMz%u0`:o_FZPLTELDeLDeiSr>>̀*+ xdtRNS4&bKGDH pHYs  #utIMEURIDAT]0E*_&jvMN)m?SJy. 8%\wZ: rE;`uqƊ{$ 2Rahcwrhγ5^~'+߾^ SL%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/editors/rename.png0000666000000000000000000000126100000000000016335 0ustar00PNG  IHDR(-SgAMA|Q cHRMz%u0`:o_FPLTEfff+f8tRNS4hbKGDH pHYs  d_tIMEURIDATӅY@ᙲE 3" I";Yw:90 #%!$S錐͉ RYTk NhCojFlw,?0`LeK3/Bťlnee;zXi0 E,$KdqW%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/exit.png0000666000000000000000000000221400000000000014365 0ustar00PNG  IHDR(-SgAMA a cHRMz&u0`:pQ<CPLTEVP\p7gp7hQ ]VW,Mtryw0PW'R-2/3'QY#Xw4v1WVDFCDV :U 8Q 9K 9>K9Ig fq=m7*s(- %*s!0p48,1!/o9|B]tssrAZ:}KB]bqbqC^L_PfX0"IAibngNF/" ;32) 祡ᐌ70矛 "魪2, up@9 /'.' ?9rnxtA:$#@9vr{B<ꢞ&&衞>7쨥rlrl餡 "2),$.!91_W^W4,%H:xC5tf=tRNS'ab'##;]{=[zPj ?Rbv6Wv1Qq[k.9BU[c}6Tt-LkyV`jGWjh~YwPog}AUj%"! .'.)EVtLj.K *GfF]vH_x,Ii=]}NgOh;Z{4UuNgMe-KjE`{WpSkF`{XrdWxTtTllcE`|dSrQpZqJdaCdUoeZyYx\tUoJiUuYsngYtXwեԆЪВ֠twchwby`widwq_|[zHhKjca~JjXwXvNnB`?_?_~Cb8Wv;Zy7VuSq[x>]}=[z=\|=\{=]{\zEeGfLlb~gMmIhFe^{[xMlSrYxnmZyRr\yTnRrWwaxycWvSsUob}q}nb|tm]tRNS&+)NF52VO;LhbUnh3D,!0&,>BbOKbN\-R??@ABCDE FGHIJKLMNOPQRSTUVWXYZ[ \]^_`a  bcde!"#$%fghi&'(jklm)nopqrstuvwxyz{|}*~瀁+脅,-鈉.+/01-203…qmA̵%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/fileimport.png0000666000000000000000000000302400000000000015566 0ustar00PNG  IHDRw=gAMA a cHRMz&u0`:pQ<bKGD+IDATH}m\WϹwf3$m$5+AԾ6Z aVbAE BXP꧶5RKmie)*diMww2۹3sΝ{nf7?W3+B=yAVMm6#zzo_Pl!_ڶfgddnJ/kń Ujjo Օx d0 "X#<vr5arqZu cb0xA)HŅ,0kKm/һpMbE)R rm7 3)j. ƣm@-#%k=.UAsbFl-}?cb ĄQ@GEawG'>5Y?VGu*A3N1]W$CH o~zj 1^!eȏbal3@؁qȠ(Gz멍֮_+pW]J| xT#IJx1g-iF))hDj)WBaڂf++i&i@ RuuW5}BY3]d gkuKeC3["#9>dW_#$4M ]@a trٍmFr 뀶9^>:gpwNyR͜~,cPE7tG'Ґ'(;Ջ<|)mXN/9{r9TUm%&,9\\~ԛ|)c/"N jR?a߭M;}~H6*=q<О挙& #8};_.d6P=ۯ;gh7:,/h.0r=|k&i%Z4/5i$ ݜSOm=>xhvuIG:>`ط_y- / P(™{Ip@0KXs%`OH1շ&!xka , j^ES `]ՠ`@yk 1mY}** `7#Ft㳎Q?;&fc[CvsT)ll.%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/fileopen.png0000666000000000000000000000300300000000000015212 0ustar00PNG  IHDRשgAMA a cHRMz&u0`:pQ<PLTEެXXXNNNKKKJJJHHHFFFDDDCCCBBB???<<<|wwwzdbbx(%%(%%WTTu(%%(%%(%%(%%!WUUr(%%(%%(%%!VTTq(%%(%%(%%!USSd(%%!USSƚ]I/}(y(%%!WUU֪mgd:Xspp v(%%%"":77̸;Uyc%6I(%%%""uss >Y_&,5)+1(')洿ᦽ菾呿ૻǯ쌻㎻㍻সƪ鋸ኸॷƨ臵އ݈ߤŨ牴܀$ɹutRNS5[r0@Ru0SRRRRRRRRQ[9O\&)$g 73q@B{о.Nߖ^q+dn5ap @+ dl  ebKGDHtIMEURIDAT(c`@L,lN.n^>~AҲ aheT7%RM-mp J 뗂KHOKL4yiӦ%d!3&M9krp yP .ZD.$l+W*%AVYn*p M![n۾C .vٻo FġG;ОN0uRjէ_}|΁AUVа(ccbSR32ˇ  %C%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/filesave.png0000666000000000000000000000217000000000000015213 0ustar00PNG  IHDRw=gAMA a cHRMz&u0`:pQ<bKGDIDATHǝˋ\E{nOc8!N MpIH\H DA+ܻtޅ;7".ąBdKF#><\۝.֭s|_ٳoOǎ?ugϔWDVK)s;?|A_n@NFO;͛7˼.]z87_.# g2/]z_nnJIRp"a6@#0Sl !ܝNq+k $CfD?(j $@DO-ڮ`ڭY0Pe@1QKQm}.rDA@WS]2fK "U(?$徖h4_ޝۜ?’c~&\wbUF@o?CBF?wOwNzn̬l]CKeέm-tk׬}rKH:{?L"D -R(hT?hDpƟ2u9 D5T!ƈIoY5Td*JBiR.1.4lC$(Iꑐ' 9]ֶv?bV(hD} kuN|>͛{A$! a-5$207Coo/&&YXν$pE kEU%M\YhٳLjpeU˴ aCTQ[7 b1\0?y-sg8BV;ZŘvڹ0Ͷm̊) 184 1??GX\SիרvA>_̪Վi$k-"rD8i}5rVҠY晴s#[]"~ @IbsK a~qfg/R(@ H\ 4Mp< f)hoK/'ׯ1z=;w)d%tqI>rBX*mpQTJEɑNq9wnaK[+*^g]h>l`HT,1p^UN:ʼn03sbH2i~g xO!Ƹ^10NT*cEB{OzWusΑi!`~TT*72<[o291c줥}s /]B6ADɾbLa,(حDDL224Ҕ0(ahruyyNsIU::vR8$u, -ԥt, P(*(**(ɭNYmr 8|}*|e*؉W `B %tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/filetypes/gif.png0000666000000000000000000000275000000000000016172 0ustar00PNG  IHDR szzgAMA|Q cHRMz%u0`:o_FbKGD pHYs  IDATXŗ]hUw>vfWئ mkJ+*j}HJQQP&P 6BJ/RMmHlI6d7ݝ{}()fR/,ws+R(M!PJ!D4? !VB@={ږ@,(Kɖ@H)QJib ]m $+ ^)lI _/4&$7CPI%@zBh[ӵJ 2cyT6@ƾFh`쯘+Fy5LdٓX/ZUFKF \x?P4sAVnE6 Zf]C n:0JyhBN&uv&jBS=@>g/(VN^9?}N>_O$AA[{=в,a9ojw ~ 69^pIxi vc=zp]hl(‘Sr )i`"[`:Ќ꿄 úLNQ|8y6GyA)Bsݳį I !?;| ~8X=n>uy7ȷI)Eh<2fXdri;I` i7bxIjӌ% RIs s HJC \qjZb 3xe '0 4`U`61MIak=lsO*ۄu4B[h@z I\;rZ]I{;,g#d`9} c N\xqI15RJBhCa Oy\D&a ~><~ \*?Xɳ#|c}frȫ}gH M5P[ ]Թ/\nqTo<3dFTl5a)|^xr\o|-,ٵ!ЩKOLXgq%Ui3XmJ)<2’@_@YzKscjrcKֶcok&?}X_6cd}PSٌX܈!x,_},VR[+-޽]Wɯq/Ɩ[Q85M`ےzJ+ -zaT׹m00^s{i !Wy\<[ T)\T?zͰVOz{m޻#c Fxl4;n҈֚6|)hdž0LI)x ̈́J`9s,o~YF($z.6 )h c A6Cy6V@l4;y={5`)Ou;&5nVP r'Q1~O24P.eDcN{(+5yߦ6`t(p DZ6SZבdI/ZR4ĊO1C8H8`O0KscBЈd]4}8RTXocuR"Xĵ%R%A R`8E t\t tj;CD!xG%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/filetypes/jpg.png0000666000000000000000000000275000000000000016205 0ustar00PNG  IHDR szzgAMA|Q cHRMz%u0`:o_FbKGD pHYs  IDATXŗ]hUw>vfWئ mkJ+*j}HJQQP&P 6BJ/RMmHlI6d7ݝ{}()fR/,ws+R(M!PJ!D4? !VB@={ږ@,(Kɖ@H)QJib ]m $+ ^)lI _/4&$7CPI%@zBh[ӵJ 2cyT6@ƾFh`쯘+Fy5LdٓX/ZUFKF \x?P4sAVnE6 Zf]C n:0JyhBN&uv&jBS=@>g/(VN^9?}N>_O$AA[{=в,a9ojw ~ 69^pIxi vc=zp]hl(‘Sr )i`"[`:Ќ꿄 úLNQ|8y6GyA)Bsݳį I !?;| ~8X=n>uy7ȷI)Eh<2fXdri;I` i7bxIjӌ% RIs s HJC \qjZb 3xe '0 4`U`61MIak=lsO*ۄu13_]sz ԁDUD@$H)VY*͇uGk$MFU3#jREO=M}(LTP ("*DJ'ՍY1c>( sXmla+dHٍ:A"EŊ[J  C l?'м@DPSixW@ 'aRG`emH5 HABDTe``NI, o/jS phFY|128хȤ!I0 qU Z[) 2 skQ24;=/DA Rx,{p=ۼ҂okuu/|j4Np ܒx!ԩE(҄HYnqw7V_߸1RrOQ|WW @@HB*6 1|Ha) wt 7o\i F|\Od۶4k~&dxv/"V5Zl!k$ħO3}z* իIolrTBzNL1q\Y `1~f] ,W|qmmdj^lŊJ¨- =y(£ IL>%ݰgOay{O=i֬;A"ب\AV{C,;~ݻ?KH +Zq uK$%R(aPojbYg''N4’4rAE3_T3'$7!SoM c0M `2-ͺPU%iΓ`xlk2 :<Q!4|uu&kERmCDUyP? \;n1CY:<5ȦrG%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/filetypes/png.png0000666000000000000000000000275000000000000016211 0ustar00PNG  IHDR szzgAMA|Q cHRMz%u0`:o_FbKGD pHYs  IDATXŗ]hUw>vfWئ mkJ+*j}HJQQP&P 6BJ/RMmHlI6d7ݝ{}()fR/,ws+R(M!PJ!D4? !VB@={ږ@,(Kɖ@H)QJib ]m $+ ^)lI _/4&$7CPI%@zBh[ӵJ 2cyT6@ƾFh`쯘+Fy5LdٓX/ZUFKF \x?P4sAVnE6 Zf]C n:0JyhBN&uv&jBS=@>g/(VN^9?}N>_O$AA[{=в,a9ojw ~ 69^pIxi vc=zp]hl(‘Sr )i`"[`:Ќ꿄 úLNQ|8y6GyA)Bsݳį I !?;| ~8X=n>uy7ȷI)Eh<2fXdri;I` i7bxIjӌ% RIs s HJC \qjZb 3xe '0 4`U`61MIak=lsO*ۄuvą:J`>񤢢kvm`cga^pH򱯭Gle]Ql[b🟟b댾씽뎹xuoۓډ։֊نىӠ~ͭ퐐)tRNS帹b;bKGDH pHYs  tIMEURIDAT8ˍwPۚuv#}"lmwVl;  lh,nr C/w9'99لqGKy@A%Y,DQUp>h0"H{@|ES2,{jz { L{`((%)LPEV۴m׾CNJN=2:Kn{ջO__z 4xPT c LjeUU.qML8iӦϘY-Hx651o/Yl9Vd+VXv 7m޲dvT0sW={?p2@:#GM;~gΞ; T ~eW^~w$,h><|g_+QdPaں 0#ۡ#>/ Jip8W7D?uBw,+B`@@HFIHFH@?A}~䊊xxxkkkzzzwwwyyyabaݎЇDZrktRNS帹b;bKGDH pHYs  tIMEURIDAT8c` 0212323hF& +`@(`$IR@R@I(P$+B8+ %-##+@BB⒒ bbP$& 4 MBR\\RYQEE ԁcC !.!.! Z@w#LE("6$:( DQ +Y-v;)I{G'gGGW7wOO/Io>~A!!aaQH`OHLJNIIMǨFV_PPXT\RZZV^QYUU]c! YA76pQ=}&ILfGR6m3gAٳ̝'`/Y ˖Xr WYn``i-ّlYev60غJGB\@Ʉ-lٱS wHp")ز{18ؾGGBRa-wݷ8x : lY:;''&NUǮݻ0T4/s}ɱ]%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/filetypes/tar.png0000666000000000000000000000305100000000000016206 0ustar00PNG  IHDR szzgAMA|Q cHRMz%u0`:o_FbKGD pHYs  +IDATXŗ[{.;;{񒵝8 p.F+HƆG)tf49՟sZ5LF5) GH/1*\eVcZl,¿XGʵXL|C%;%RrP!],o/^W_fB8 ⌻ %TQ ߍBWqbĘBg ȡұ#Z]3!FŇb7y@dHWc 1%RCpˎ}`.D읔L Jg zCjUYw%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/filetypes/tgz.png0000666000000000000000000000305100000000000016224 0ustar00PNG  IHDR szzgAMA|Q cHRMz%u0`:o_FbKGD pHYs  +IDATXŗ[{.;;{񒵝8 p.F+HƆG)tf49՟sZ5LF5) GH/1*\eVcZl,¿XGʵXL|C%;%RrP!],o/^W_fB8 ⌻ %TQ ߍBWqbĘBg ȡұ#Z]3!FŇb7y@dHWc 1%RCpˎ}`.D읔L Jg zCjUYw%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/filetypes/tif.png0000666000000000000000000000275000000000000016207 0ustar00PNG  IHDR szzgAMA|Q cHRMz%u0`:o_FbKGD pHYs  IDATXŗ]hUw>vfWئ mkJ+*j}HJQQP&P 6BJ/RMmHlI6d7ݝ{}()fR/,ws+R(M!PJ!D4? !VB@={ږ@,(Kɖ@H)QJib ]m $+ ^)lI _/4&$7CPI%@zBh[ӵJ 2cyT6@ƾFh`쯘+Fy5LdٓX/ZUFKF \x?P4sAVnE6 Zf]C n:0JyhBN&uv&jBS=@>g/(VN^9?}N>_O$AA[{=в,a9ojw ~ 69^pIxi vc=zp]hl(‘Sr )i`"[`:Ќ꿄 úLNQ|8y6GyA)Bsݳį I !?;| ~8X=n>uy7ȷI)Eh<2fXdri;I` i7bxIjӌ% RIs s HJC \qjZb 3xe '0 4`U`61MIak=lsO*ۄumE9m~',ft ˢ9t:/xj3"/67)?kƀ)%ii )2dYA%G`XE\\ymb[e=._ pK\vq?awWM,`{sFc^Yz/}͛O Q0 G5"EX&I"$i4M`0Y,d2s)y,d2y֭Gw*Ah 0<H^'I(`nf\8ϋj'0,[-2O,,\>wp0{V05/ 0=}ƹi_t14盛#at6Ba;;LpP) Ţ<0pls (@JeY|ry |QkB0V#19Fc󋲠z0?7hM&!?Y+WXgl76ז8;B|AM ӦRW=H2t|/~"^FЏ{;ވ^ E-еnXhPTU4M3^ak*~a)o+2( c:2PPuZSM4MC:V V C1QUng 0 |LLO!=-q_OYfd63hGyyl`!:i6J3tG/a6!ٚfb8o/ކ;ڌ4M蚉i R$B p>N#f[<ω$NHØ0)AgADb'6gڦ\yajwg 2v{(0$Sm cvwwH[]G/b&bMӨULӤ\.*Z-Dqp UkNF^c>Y<ڦZ^_gբӣ\*f~nl0N#R BЏCJaJ< ANINGH2E!'1Op2st(0a(NH )2ˈˆ(' i Tʀm,--zc;Be0dqZ-4!FS~ף>t2:F?[{%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/filetypes/xls.png0000666000000000000000000000232500000000000016231 0ustar00PNG  IHDR DgAMA|Q cHRMz%u0`:o_FPLTE̷΋ל||##::QQee\[mm ''@@U\uHc7] ##;;LTA`3[']*pX$&G@e7N(L"HX*).C*E0=2'.\zS v1[0) TqS{22"+y v yHaG +3&̐`habmbxNNM 0;1%W < tQQ+j)AL܀6I]4%wufO?nRƆG)tf49՟sZ5LF5) GH/1*\eVcZl,¿XGʵXL|C%;%RrP!],o/^W_fB8 ⌻ %TQ ߍBWqbĘBg ȡұ#Z]3!FŇb7y@dHWc 1%RCpˎ}`.D읔L Jg zCjUYw%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/guidata.svg0000666000000000000000000004560500000000000015060 0ustar00 image/svg+xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/max.png0000666000000000000000000000054400000000000014205 0ustar00PNG  IHDRbgAMA a cHRMz&u0`:pQ< PLTEPYʆtRNS+NbKGD LtIMEUR#IDATc`P%  b:ci%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErtEXtSoftwarePaint.NET v3.30@GIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/min.png0000666000000000000000000000053700000000000014205 0ustar00PNG  IHDRbgAMA a cHRMz&u0`:pQ< PLTEPYʆtRNS+NbKGD LtIMEURIDATc`@| Y14B c9ԧ%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErtEXtSoftwarePaint.NET v3.30@GIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/none.png0000666000000000000000000000051300000000000014353 0ustar00PNG  IHDR7gAMA a cHRMz&u0`:pQ<tRNSv8bKGD݊ pHYs ;ttIMEUR IDATc` 0Ǫ%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErtEXtSoftwarePaint.NET v3.30@GIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/not_found.png0000666000000000000000000000102300000000000015404 0ustar00PNG  IHDR(-SgAMA a cHRMz&u0`:pQ<QPLTEji_jiiioigjjjjgiiijiiiiijAtRNSHh ߧ0Ѽ埇znbKGDug2 pHYs[ tIMEURaIDAT}I [EDܷGطPB:`t@nBiEd~Ge}a\G=u@oBjDeFd}[OP=v?qBktRG@FjEd}`VK@9aZODe\Q?]HIDA'tRNS% !)*#P; (#PbKGD-tIMEURIDATc`F&ufV8`a prqԍMLy`L]h#N'gW733wO/o9~@MA!| aQ11q < I)@~jZzFf0zLL6*.q II)nܼ|uiY9S K.U(-+R ()H"+ͭ%%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/quickview.png0000666000000000000000000000157000000000000015427 0ustar00PNG  IHDR(-SgAMA|Q cHRMz%u0`:o_FYPLTEffffffffffffffffffffffffffffffUUUUUUffffffUUUQQQMMMUUUUUUPPPUUUfffUUUJJJIIIzzzPPPUUUiiiNNNpppfffAAA555111;;;KKK&,3^ys]9RMMMYB$1tFFFSoE2躺OXk???333c6;dFOX_7CCC솺Yim?b漼===ĖE8ikkkꬬڷ&tRNSo/_O??/_Oϯo?oobKGDH pHYs  tIMEURIDATc````dbffa@V65 `g9ffv56 , R<<@x"| ZB `ut 5DD98%ML-,5$E9xml%EY8x\\=<}|"| RA^!a@CEcbSd@ȦgdfgɁ'._^Q *(T) l%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/save_all.png0000666000000000000000000000307700000000000015212 0ustar00PNG  IHDRw=gAMA a cHRMz&u0`:pQ<bKGDVIDATHǝˏ\Guϙx왉 c,LHV% $X0 +6(A@"F,0!q&I4'v^qOO?{T"9#]UΧ}{ᯜ87NիFf$ vxyUNaVVxϳMk,[}h޽{ߝ;ǿMɜLu+8*+8@UՈ8Dxɧ1} hل0x!`"۵րKƵ~` UJ"I@DpsFS_旿5G~}.ʢ"C)E- ]~p4ִt+W6jpwq4C񱟮G ~xωS1o,|"#D C@=d׻#Q(JPj0FmΫ,"V $ʢbZ *IX4js#MS# Y`1;]\$Qe NnJ{O EhZ[fLk1BŤҥW"9q""wE ( ]?q=GUU8H"ڔI1!xL&%шxh\0{H$O0dyy7 ^cj&BN CUUX[#a?ZTTBLPَ\J)r nuԕxa:%UUcdZcP5"bDI3V={nXrR3g9t| \pA)E7v\>zcǏ- [[uuSBls$>|~/e] Sn-1}};Sm`X+N<`C&* KS*%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/selection.png0000666000000000000000000000135000000000000015401 0ustar00PNG  IHDR(-SgAMA a cHRMz&u0`:pQ<PLTE/Ɯˑˍ˔˞egsY[hTVcLN\CETgitz|KM[02BsuˌlnyCET-Z\i24Eϴceqʔ𛝤홚򒔜񌍖ӎ|}Y[gÃČabnxzfhsqs~#$tRNS/;FD50yrJYk3QH"ɔɈabKGD.TtIMEURIDATc````Tab@,jl(Z(:\zz<(چF\H&F3M-,١v1Z6vP{G'g>~AO/aoQ14L@FVA^!0HŇJ! h(%tEXtdate:create2015-08-31T15:00:15+02:00qH%tEXtdate:modify2015-08-31T15:00:15+02:00ErtEXtSoftwarePaint.NET v3.36%IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1443344001.0 guidata-2.0.2/guidata/images/settings.png0000666000000000000000000000461500000000000015263 0ustar00PNG  IHDR(-SiCCPiccH{LSW TZ "+hW!ǐ(Hg_BmAMeAT|@6dsAppIb (:H@;]799==ݪ", &2!2LV&Ax&+T[7{?>}8$#8b?x0!NXB91LJG\x&~VUQIGc{Klgѵ9ZQGr A>l*w"v9%(ll*?%C8{?'?!߁Xj*^.XC%ݣ|Th`"X "8ê\WhmN2范(+N.>a7|Nc9EpKX4o˔k ^aގ|xGJT(:5a \5;ʐ䰦-UUuhgm3 O ?S8nՒ#),&ju #7w8Vn\}n}A?G ݽ~,ٮɧ/2-7 `o &"!Zc$20YScG';DrCD8& .\ҼPW~BLxyE> ɤJʀrBLI kpfҮ䈵/Su~<1l5`/SH&KNmw>VUcMH퍺/76E:9Ԗѳ[o6^zj5;3⍵A>|ٰ~K``IˤZ8be'svcFs'85 \6V^(tbys|D_t[\(<EpDjs*)ޚS?_Y ̬?RHƞMGڱ58vvk-{˪(Mכ8dqN,Bk^>ˀyG7{G#>Xl^9MthgfTLh,XmXq6+y9 =7*wU54x*8\W*1=h@ Bm5L 2^! H}zItkґqTO"JeT?۹K[Kg7nwPcnPLTEƙƥUUU~~||hhlhhi```^^^jlxdfnegoUUUaciabi000\]j]_mEEEacsEEE\^k555 NPYQR\ͼӘؾ֡ݽٝح¥ҥѣĬҢ̡̬ӫ¢ŋřƬƔuxǰȮǏtwx}uy}|txy}bduPtRNS O= AE堝NV建 a_ *WodcnE"  '+-,%K7bKGD k=tIMEURIDATc`Ya|V6v.v6V'0(8$4>~ȨXA!~>H\|BbRRrJ\0HhZzFxVvN+)_P( ST\R* ++W`P(iQSVohlRU՚[Z::T=}}&jih3LSZah    # 2>FJ \=h:   &< CQ` hs  IOTdt( II"xlJD0>u  F T]cs z ! * 8 D8Q4 "5, *6K![} D!0@qv } @ ]k%z ^ 5<ry   $!;]n;     T/ R ! ! !%!+!0!7!T!]!m! !!!!!! !!!!!!!! !#","4":"C" and between higher than lower than %s are currently not supported%s arrays%s editor%s filesUnable to assign data to item.

Error message:
%sUnable to plot data.

Error message:
%sUnable to proceed to next step

Please check your entries.

Error message:
%sUnable to save array

Error message:
%sUnable to show image.

Error message:
%sWarning: changes are applied separatelyAdditional optionsAll supported filesApplyArray editorArrays with more than 3 dimensions are not supportedAttributeAxis:Background colorBackground:Builtin:CancelClipboard contentsCloseColumn min/maxColumn separator:Comment:Comments:CopyCurrent cell:Current line:DataDefinition:DescriptionDictionaryDo you want to remove all selected items?Do you want to remove the selected item?DoneDuplicateEOLEditEdit array contentsEdit itemEmpty clipboardErrorFloat formattingFor performance reasons, changes applied to masked array won't be reflected in array's data (and vice-versa).FormatFormat (%s) is incorrectFormat ({}) is incorrectFormat ({}) should start with '%'HistogramImport asImport errorImport from clipboardImport wizardIndexIndex:InsertInstance:It is not possible to display this value because an error ocurred while trying to do itIt was not possible to copy this arrayIt was not possible to copy this dataframeIt was not possible to copy values for this arrayKeyKey:Keyword:Largest element in arrayLink:ListMaskMasked dataNameNew variable name:NextNo description availableNormal text:Nothing to be imported from clipboard.NumPy arrayNumPy arraysNumber of rows x Number of columnsNumber:Occurrence:Opening this variable can be slow Do you want to continue anyway?PastePlease check highlighted fields.Please install matplotlib or guiqwt.PlotPreviewPreviousQuitRaw textRecord array fields:RemoveRenameResizeResize rows to contentsRow separator:Run this scriptSave and CloseSave arrayShow arrays min/maxShow imageSide areas:SizeSkip rows:Smallest element in arraySome required entries are incorrectSource codeString:TabTests - %s moduleText editorThe 'xlabels' argument length do no match array column numberThe 'ylabels' argument length do no match array row numberTo boolTo complexTo floatTo intTo strTransposeTupleTypeValueValue is forced to %dValue:Variable NameVariable name:WarningWhitespaceall file typesarraycodedataelementsevenfloatintegerlistnon zerooddotherread onlysupported file types:tabletextunit:variable_nameProject-Id-Version: PACKAGE VERSION POT-Creation-Date: 2021-08-05 09:25+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: utf-8 Generated-By: pygettext.py 1.5 et compris entre supérieur à inférieur à Attention: %s ne sont pas pris en chargeles tableaux %sÉditeur de %sFichiers %sImpossible d'accéder aux données

Message d'erreur :
%sImpossible d'afficher les données

Message d'erreur :
%sImpossible de passer à l'étape suivante

Merci de vérifier votre saisie.

Message d'erreur :
%sImpossible d'enregistrer le tableau

Message d'erreur :
%sImpossible d'afficher l'image

Message d'erreur :
%sAttention: les changements sont appliqués séparémentOptions supplémentairesTous les fichiers pris en chargeAppliquerÉditeur de tableauxLes tableaux ayant plus de trois dimensions ne sont pas pris en chargeAttributAxe :Couleur de fondFond :Builtin :AnnulerContenu du presse-papiersFermerMin/max de colonneSéparateur de colonne :Commentaire :Commentaires :CopierCellule courante :Ligne courante :DonnéesDéfinition :DescriptionDictionnaireSouhaitez-vous supprimer les éléments sélectionnés ?Souhaitez-vous supprimer l'élément sélectionné ?TerminerDupliquerEOLModifierModifier le contenu du tableauModifierPresse-papiers videErreurFormat de flottantPour des raisons de performance, les changements appliqués au masque ne sont pas reflétés dans le tableau associé (et vice-versa).FormatLe format (%s) est incorrectLe format ({}) est incorrectLe format ({}) ne doit pas commencer par '%'HistogrammeImporter en tant queErreur d'importImporter depuis le presse-papiersAssistant d'importationIndiceIndice :InsérerInstance :Impossible d'afficher cette valeur en raison d'une erreur inattendueImpossible de copier ce tableauImpossible de copier ce DataFrameImpossible de copier les valeurs pour ce tableauCléClé :Mot-clé :Valeur maximale du tableauLien :ListeMasqueDonnées masquéesNomNouveau nom de variable :SuivantAucune description disponibleText normal :Aucune donnée ne peut être importée depuis le presse-papiers.Tableau NumPyTableaux NumPyNombre de lignes x Nombre de colonnesNombre :Occurence :Editer cette variable sera vraisemblablement très long. Souhaitez-vous néanmoins continuer ?CollerVeuillez vérifier votre saisie.Merci d'installer guiqwt ou matplotlib.TracerAperçuPrécédentQuitterText brutChamps :SupprimerRenommerAjusterAjuster les colonnes au contenuSéparateur de ligne :Exécuter ce scriptEnregistrer et FermerEnregistrer le tableauAfficher les min/max des tableauxAfficher l'imageZone latérale :TailleSauter des lignes :Valeur minimale du tableauLes champs surlignés n'ont pas été remplis correctement.Code sourceChaîne :TabTests - Module %sÉditeur de texteLa taille de l'argument 'xlabels' ne correspond pas au nombre de colonnes du tableauLa taille de l'argument 'ylabels' ne correspond pas au nombre de lignes du tableauVers booléenVers complexeVers flottantVers entierVers chaîneTransposerTupleTypeValeurLa valeur est imposée à %dValeur :Nom de variableNom de variable :AvertissementEspacetout type de fichiertableaucodedonnéesélémentspairflottantentierlistenon nulimpairautrelecture seuletypes de fichiers pris en charge : tableautexteunité :nom_de_variable././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628148308.0 guidata-2.0.2/guidata/locale/fr/LC_MESSAGES/guidata.po0000666000000000000000000004314700000000000017064 0ustar00# -*- coding: utf-8 -*- # guidata module translation file # Copyright (C) 2009 CEA # Pierre Raybaut , 2009. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "POT-Creation-Date: 2021-08-05 09:25+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: utf-8\n" "Generated-By: pygettext.py 1.5\n" #: guidata\dataset\dataitems.py:53 msgid "float" msgstr "flottant" #: guidata\dataset\dataitems.py:53 msgid "integer" msgstr "entier" #: guidata\dataset\dataitems.py:59 msgid " and " msgstr " et " #: guidata\dataset\dataitems.py:59 msgid " between " msgstr " compris entre " #: guidata\dataset\dataitems.py:61 msgid " higher than " msgstr " supérieur à " #: guidata\dataset\dataitems.py:63 msgid " lower than " msgstr " inférieur à " #: guidata\dataset\dataitems.py:65 msgid "non zero" msgstr "non nul" #: guidata\dataset\dataitems.py:67 msgid "unit:" msgstr "unité :" #: guidata\dataset\dataitems.py:211 msgid "even" msgstr "pair" #: guidata\dataset\dataitems.py:213 msgid "odd" msgstr "impair" #: guidata\dataset\dataitems.py:392 msgid "all file types" msgstr "tout type de fichier" #: guidata\dataset\dataitems.py:394 msgid "supported file types:" msgstr "types de fichiers pris en charge : " #: guidata\dataset\qtitemwidgets.py:272 guidata\dataset\qtitemwidgets.py:335 msgid "Value is forced to %d" msgstr "La valeur est imposée à %d" #: guidata\dataset\qtitemwidgets.py:644 msgid "%s files" msgstr "Fichiers %s" #: guidata\dataset\qtitemwidgets.py:646 msgid "All supported files" msgstr "Tous les fichiers pris en charge" #: guidata\dataset\qtitemwidgets.py:873 msgid "Number of rows x Number of columns" msgstr "Nombre de lignes x Nombre de colonnes" #: guidata\dataset\qtitemwidgets.py:876 msgid "Edit array contents" msgstr "Modifier le contenu du tableau" #: guidata\dataset\qtitemwidgets.py:882 msgid "Smallest element in array" msgstr "Valeur minimale du tableau" #: guidata\dataset\qtitemwidgets.py:886 msgid "Largest element in array" msgstr "Valeur maximale du tableau" #: guidata\dataset\qtwidgets.py:147 guidata\tests\translations.py:16 msgid "Some required entries are incorrect" msgstr "Les champs surlignés n'ont pas été remplis correctement." #: guidata\dataset\qtwidgets.py:149 msgid "Please check highlighted fields." msgstr "Veuillez vérifier votre saisie." #: guidata\dataset\qtwidgets.py:567 msgid "Apply" msgstr "Appliquer" #: guidata\guitest.py:73 msgid "No description available" msgstr "Aucune description disponible" #: guidata\guitest.py:105 msgid "Description" msgstr "Description" #: guidata\guitest.py:114 msgid "Source code" msgstr "Code source" #: guidata\guitest.py:119 msgid "Run this script" msgstr "Exécuter ce script" #: guidata\guitest.py:120 msgid "Quit" msgstr "Quitter" #: guidata\guitest.py:140 msgid "Tests - %s module" msgstr "Tests - Module %s" #: guidata\widgets\arrayeditor.py:535 guidata\widgets\collectionseditor.py:842 #: guidata\widgets\dataframeeditor.py:668 msgid "Copy" msgstr "Copier" #: guidata\widgets\arrayeditor.py:583 guidata\widgets\collectionseditor.py:527 #: guidata\widgets\collectionseditor.py:1315 #: guidata\widgets\collectionseditor.py:1328 msgid "Warning" msgstr "Avertissement" #: guidata\widgets\arrayeditor.py:584 msgid "It was not possible to copy values for this array" msgstr "Impossible de copier les valeurs pour ce tableau" #: guidata\widgets\arrayeditor.py:626 guidata\widgets\arrayeditor.py:661 #: guidata\widgets\dataframeeditor.py:780 #: guidata\widgets\dataframeeditor.py:842 msgid "Format" msgstr "Format" #: guidata\widgets\arrayeditor.py:631 guidata\widgets\dataframeeditor.py:784 msgid "Resize" msgstr "Ajuster" #: guidata\widgets\arrayeditor.py:634 guidata\widgets\dataframeeditor.py:788 msgid "Background color" msgstr "Couleur de fond" #: guidata\widgets\arrayeditor.py:662 guidata\widgets\dataframeeditor.py:843 msgid "Float formatting" msgstr "Format de flottant" #: guidata\widgets\arrayeditor.py:672 msgid "Format (%s) is incorrect" msgstr "Le format (%s) est incorrect" #: guidata\widgets\arrayeditor.py:672 guidata\widgets\collectionseditor.py:543 #: guidata\widgets\dataframeeditor.py:853 #: guidata\widgets\dataframeeditor.py:857 msgid "Error" msgstr "Erreur" #: guidata\widgets\arrayeditor.py:713 msgid "Arrays with more than 3 dimensions are not supported" msgstr "Les tableaux ayant plus de trois dimensions ne sont pas pris en charge" #: guidata\widgets\arrayeditor.py:717 msgid "The 'xlabels' argument length do no match array column number" msgstr "" "La taille de l'argument 'xlabels' ne correspond pas au nombre de colonnes du " "tableau" #: guidata\widgets\arrayeditor.py:722 msgid "The 'ylabels' argument length do no match array row number" msgstr "" "La taille de l'argument 'ylabels' ne correspond pas au nombre de lignes du " "tableau" #: guidata\widgets\arrayeditor.py:732 msgid "%s arrays" msgstr "les tableaux %s" #: guidata\widgets\arrayeditor.py:733 msgid "%s are currently not supported" msgstr "Attention: %s ne sont pas pris en charge" #: guidata\widgets\arrayeditor.py:740 msgid "NumPy array" msgstr "Tableau NumPy" #: guidata\widgets\arrayeditor.py:742 guidata\widgets\arrayeditor.py:934 msgid "Array editor" msgstr "Éditeur de tableaux" #: guidata\widgets\arrayeditor.py:744 msgid "read only" msgstr "lecture seule" #: guidata\widgets\arrayeditor.py:781 msgid "Record array fields:" msgstr "Champs :" #: guidata\widgets\arrayeditor.py:793 msgid "Data" msgstr "Données" #: guidata\widgets\arrayeditor.py:793 msgid "Mask" msgstr "Masque" #: guidata\widgets\arrayeditor.py:793 msgid "Masked data" msgstr "Données masquées" #: guidata\widgets\arrayeditor.py:804 msgid "Axis:" msgstr "Axe :" #: guidata\widgets\arrayeditor.py:809 msgid "Index:" msgstr "Indice :" #: guidata\widgets\arrayeditor.py:822 msgid "Warning: changes are applied separately" msgstr "Attention: les changements sont appliqués séparément" #: guidata\widgets\arrayeditor.py:824 msgid "" "For performance reasons, changes applied to masked array won't be reflected " "in array's data (and vice-versa)." msgstr "" "Pour des raisons de performance, les changements appliqués au masque ne sont " "pas reflétés dans le tableau associé (et vice-versa)." #: guidata\widgets\arrayeditor.py:835 guidata\widgets\collectionseditor.py:1584 #: guidata\widgets\dataframeeditor.py:804 guidata\widgets\texteditor.py:71 msgid "Save and Close" msgstr "Enregistrer et Fermer" #: guidata\widgets\arrayeditor.py:840 guidata\widgets\collectionseditor.py:1589 #: guidata\widgets\dataframeeditor.py:809 guidata\widgets\texteditor.py:76 msgid "Close" msgstr "Fermer" #: guidata\widgets\collectionseditor.py:205 msgid "Index" msgstr "Indice" #: guidata\widgets\collectionseditor.py:207 msgid "Name" msgstr "Nom" #: guidata\widgets\collectionseditor.py:210 msgid "Tuple" msgstr "Tuple" #: guidata\widgets\collectionseditor.py:213 msgid "List" msgstr "Liste" #: guidata\widgets\collectionseditor.py:216 msgid "Dictionary" msgstr "Dictionnaire" #: guidata\widgets\collectionseditor.py:218 msgid "Key" msgstr "Clé" #: guidata\widgets\collectionseditor.py:223 msgid "Attribute" msgstr "Attribut" #: guidata\widgets\collectionseditor.py:226 msgid "elements" msgstr "éléments" #: guidata\widgets\collectionseditor.py:418 msgid "Size" msgstr "Taille" #: guidata\widgets\collectionseditor.py:418 msgid "Type" msgstr "Type" #: guidata\widgets\collectionseditor.py:418 msgid "Value" msgstr "Valeur" #: guidata\widgets\collectionseditor.py:528 msgid "" "Opening this variable can be slow\n" "\n" "Do you want to continue anyway?" msgstr "" "Editer cette variable sera vraisemblablement très long.\n" "Souhaitez-vous néanmoins continuer ?" #: guidata\widgets\collectionseditor.py:544 msgid "" "Spyder was unable to retrieve the value of this variable from the console." "

The error mesage was:
%s" msgstr "" #: guidata\widgets\collectionseditor.py:764 msgid "Edit item" msgstr "Modifier" #: guidata\widgets\collectionseditor.py:765 msgid "Unable to assign data to item.

Error message:
%s" msgstr "" "Impossible d'accéder aux données

Message d'erreur :
%s" #: guidata\widgets\collectionseditor.py:836 msgid "Resize rows to contents" msgstr "Ajuster les colonnes au contenu" #: guidata\widgets\collectionseditor.py:839 msgid "Paste" msgstr "Coller" #: guidata\widgets\collectionseditor.py:845 msgid "Edit" msgstr "Modifier" #: guidata\widgets\collectionseditor.py:849 #: guidata\widgets\collectionseditor.py:1247 #: guidata\widgets\collectionseditor.py:1266 msgid "Plot" msgstr "Tracer" #: guidata\widgets\collectionseditor.py:856 msgid "Histogram" msgstr "Histogramme" #: guidata\widgets\collectionseditor.py:863 msgid "Show image" msgstr "Afficher l'image" #: guidata\widgets\collectionseditor.py:870 #: guidata\widgets\collectionseditor.py:1274 msgid "Save array" msgstr "Enregistrer le tableau" #: guidata\widgets\collectionseditor.py:876 #: guidata\widgets\collectionseditor.py:1204 #: guidata\widgets\collectionseditor.py:1213 msgid "Insert" msgstr "Insérer" #: guidata\widgets\collectionseditor.py:880 #: guidata\widgets\collectionseditor.py:1141 msgid "Remove" msgstr "Supprimer" #: guidata\widgets\collectionseditor.py:885 msgid "Show arrays min/max" msgstr "Afficher les min/max des tableaux" #: guidata\widgets\collectionseditor.py:890 #: guidata\widgets\collectionseditor.py:1160 msgid "Rename" msgstr "Renommer" #: guidata\widgets\collectionseditor.py:894 #: guidata\widgets\collectionseditor.py:1163 msgid "Duplicate" msgstr "Dupliquer" #: guidata\widgets\collectionseditor.py:1137 msgid "Do you want to remove the selected item?" msgstr "Souhaitez-vous supprimer l'élément sélectionné ?" #: guidata\widgets\collectionseditor.py:1138 msgid "Do you want to remove all selected items?" msgstr "Souhaitez-vous supprimer les éléments sélectionnés ?" #: guidata\widgets\collectionseditor.py:1161 msgid "New variable name:" msgstr "Nouveau nom de variable :" #: guidata\widgets\collectionseditor.py:1164 msgid "Variable name:" msgstr "Nom de variable :" #: guidata\widgets\collectionseditor.py:1204 msgid "Key:" msgstr "Clé :" #: guidata\widgets\collectionseditor.py:1213 msgid "Value:" msgstr "Valeur :" #: guidata\widgets\collectionseditor.py:1233 msgid "Import error" msgstr "Erreur d'import" #: guidata\widgets\collectionseditor.py:1234 msgid "Please install matplotlib or guiqwt." msgstr "Merci d'installer guiqwt ou matplotlib." #: guidata\widgets\collectionseditor.py:1248 msgid "Unable to plot data.

Error message:
%s" msgstr "" "Impossible d'afficher les données

Message d'erreur :
%s" #: guidata\widgets\collectionseditor.py:1267 msgid "Unable to show image.

Error message:
%s" msgstr "Impossible d'afficher l'image

Message d'erreur :
%s" #: guidata\widgets\collectionseditor.py:1279 msgid "NumPy arrays" msgstr "Tableaux NumPy" #: guidata\widgets\collectionseditor.py:1293 msgid "Unable to save array

Error message:
%s" msgstr "" "Impossible d'enregistrer le tableau

Message d'erreur :
%s" #: guidata\widgets\collectionseditor.py:1316 msgid "It was not possible to copy this array" msgstr "Impossible de copier ce tableau" #: guidata\widgets\collectionseditor.py:1329 msgid "It was not possible to copy this dataframe" msgstr "Impossible de copier ce DataFrame" #: guidata\widgets\collectionseditor.py:1351 msgid "Clipboard contents" msgstr "Contenu du presse-papiers" #: guidata\widgets\collectionseditor.py:1366 msgid "Import from clipboard" msgstr "Importer depuis le presse-papiers" #: guidata\widgets\collectionseditor.py:1369 msgid "Empty clipboard" msgstr "Presse-papiers vide" #: guidata\widgets\collectionseditor.py:1369 msgid "Nothing to be imported from clipboard." msgstr "Aucune donnée ne peut être importée depuis le presse-papiers." #: guidata\widgets\dataframeeditor.py:331 msgid "" "It is not possible to display this value because\n" "an error ocurred while trying to do it" msgstr "Impossible d'afficher cette valeur en raison d'une erreur inattendue" #: guidata\widgets\dataframeeditor.py:675 msgid "To bool" msgstr "Vers booléen" #: guidata\widgets\dataframeeditor.py:676 msgid "To complex" msgstr "Vers complexe" #: guidata\widgets\dataframeeditor.py:677 msgid "To int" msgstr "Vers entier" #: guidata\widgets\dataframeeditor.py:678 msgid "To float" msgstr "Vers flottant" #: guidata\widgets\dataframeeditor.py:679 msgid "To str" msgstr "Vers chaîne" #: guidata\widgets\dataframeeditor.py:759 msgid "%s editor" msgstr "Éditeur de %s" #: guidata\widgets\dataframeeditor.py:794 msgid "Column min/max" msgstr "Min/max de colonne" #: guidata\widgets\dataframeeditor.py:852 msgid "Format ({}) is incorrect" msgstr "Le format ({}) est incorrect" #: guidata\widgets\dataframeeditor.py:856 msgid "Format ({}) should start with '%'" msgstr "Le format ({}) ne doit pas commencer par '%'" #: guidata\widgets\importwizard.py:166 guidata\widgets\importwizard.py:516 msgid "Import as" msgstr "Importer en tant que" #: guidata\widgets\importwizard.py:168 msgid "data" msgstr "données" #: guidata\widgets\importwizard.py:172 msgid "code" msgstr "code" #: guidata\widgets\importwizard.py:175 guidata\widgets\importwizard.py:606 msgid "text" msgstr "texte" #: guidata\widgets\importwizard.py:187 msgid "Column separator:" msgstr "Séparateur de colonne :" #: guidata\widgets\importwizard.py:191 msgid "Tab" msgstr "Tab" #: guidata\widgets\importwizard.py:194 msgid "Whitespace" msgstr "Espace" #: guidata\widgets\importwizard.py:197 guidata\widgets\importwizard.py:215 msgid "other" msgstr "autre" #: guidata\widgets\importwizard.py:208 msgid "Row separator:" msgstr "Séparateur de ligne :" #: guidata\widgets\importwizard.py:212 msgid "EOL" msgstr "EOL" #: guidata\widgets\importwizard.py:227 msgid "Additional options" msgstr "Options supplémentaires" #: guidata\widgets\importwizard.py:231 msgid "Skip rows:" msgstr "Sauter des lignes :" #: guidata\widgets\importwizard.py:241 msgid "Comments:" msgstr "Commentaires :" #: guidata\widgets\importwizard.py:247 msgid "Transpose" msgstr "Transposer" #: guidata\widgets\importwizard.py:519 msgid "array" msgstr "tableau" #: guidata\widgets\importwizard.py:524 msgid "list" msgstr "liste" #: guidata\widgets\importwizard.py:529 msgid "DataFrame" msgstr "" #: guidata\widgets\importwizard.py:589 guidata\widgets\importwizard.py:676 msgid "Import wizard" msgstr "Assistant d'importation" #: guidata\widgets\importwizard.py:594 msgid "Raw text" msgstr "Text brut" #: guidata\widgets\importwizard.py:597 msgid "variable_name" msgstr "nom_de_variable" #: guidata\widgets\importwizard.py:608 msgid "table" msgstr "tableau" #: guidata\widgets\importwizard.py:609 msgid "Preview" msgstr "Aperçu" #: guidata\widgets\importwizard.py:613 msgid "Variable Name" msgstr "Nom de variable" #: guidata\widgets\importwizard.py:621 msgid "Cancel" msgstr "Annuler" #: guidata\widgets\importwizard.py:626 msgid "Previous" msgstr "Précédent" #: guidata\widgets\importwizard.py:630 msgid "Next" msgstr "Suivant" #: guidata\widgets\importwizard.py:635 msgid "Done" msgstr "Terminer" #: guidata\widgets\importwizard.py:677 msgid "" "Unable to proceed to next step

Please check your entries." "

Error message:
%s" msgstr "" "Impossible de passer à l'étape suivante

Merci de vérifier " "votre saisie.

Message d'erreur :
%s" #: guidata\widgets\syntaxhighlighters.py:37 msgid "Background:" msgstr "Fond :" #: guidata\widgets\syntaxhighlighters.py:38 msgid "Current line:" msgstr "Ligne courante :" #: guidata\widgets\syntaxhighlighters.py:39 msgid "Current cell:" msgstr "Cellule courante :" #: guidata\widgets\syntaxhighlighters.py:40 msgid "Occurrence:" msgstr "Occurence :" #: guidata\widgets\syntaxhighlighters.py:41 msgid "Link:" msgstr "Lien :" #: guidata\widgets\syntaxhighlighters.py:42 msgid "Side areas:" msgstr "Zone latérale :" #: guidata\widgets\syntaxhighlighters.py:43 msgid "Matched
parens:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:44 msgid "Unmatched
parens:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:45 msgid "Normal text:" msgstr "Text normal :" #: guidata\widgets\syntaxhighlighters.py:46 msgid "Keyword:" msgstr "Mot-clé :" #: guidata\widgets\syntaxhighlighters.py:47 msgid "Builtin:" msgstr "Builtin :" #: guidata\widgets\syntaxhighlighters.py:48 msgid "Definition:" msgstr "Définition :" #: guidata\widgets\syntaxhighlighters.py:49 msgid "Comment:" msgstr "Commentaire :" #: guidata\widgets\syntaxhighlighters.py:50 msgid "String:" msgstr "Chaîne :" #: guidata\widgets\syntaxhighlighters.py:51 msgid "Number:" msgstr "Nombre :" #: guidata\widgets\syntaxhighlighters.py:52 msgid "Instance:" msgstr "Instance :" #: guidata\widgets\texteditor.py:89 msgid "Text editor" msgstr "Éditeur de texte" #~ msgid "Run all tests" #~ msgstr "Exécuter tous les tests" #~ msgid "Open a file" #~ msgstr "Ouvrir un fichier" #~ msgid "All" #~ msgstr "Tout" #~ msgid "Opening " #~ msgstr "Ouverture de " #~ msgid "Array is empty" #~ msgstr "Le tableau est vide" #~ msgid "unknown" #~ msgstr "inconnue" #~ msgid "Unable to retrieve data.

Error message:
%s" #~ msgstr "" #~ "Impossible d'afficher les données

Message d'erreur :
%s" #~ msgid "Truncate values" #~ msgstr "Tronquer les valeurs" #~ msgid "Show collection contents" #~ msgstr "Afficher le contenu des séquences" #~ msgid "Always edit in-place" #~ msgstr "Édition en ligne pour tous les types" #~ msgid "Import as array" #~ msgstr "Importer en tant que tableau" #~ msgid "Progression" #~ msgstr "Progression" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628148308.0 guidata-2.0.2/guidata/locale/guidata.pot0000666000000000000000000003440700000000000015053 0ustar00# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "POT-Creation-Date: 2021-08-05 09:25+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=cp1252\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" #: guidata\dataset\dataitems.py:53 msgid "float" msgstr "" #: guidata\dataset\dataitems.py:53 msgid "integer" msgstr "" #: guidata\dataset\dataitems.py:59 msgid " and " msgstr "" #: guidata\dataset\dataitems.py:59 msgid " between " msgstr "" #: guidata\dataset\dataitems.py:61 msgid " higher than " msgstr "" #: guidata\dataset\dataitems.py:63 msgid " lower than " msgstr "" #: guidata\dataset\dataitems.py:65 msgid "non zero" msgstr "" #: guidata\dataset\dataitems.py:67 msgid "unit:" msgstr "" #: guidata\dataset\dataitems.py:211 msgid "even" msgstr "" #: guidata\dataset\dataitems.py:213 msgid "odd" msgstr "" #: guidata\dataset\dataitems.py:392 msgid "all file types" msgstr "" #: guidata\dataset\dataitems.py:394 msgid "supported file types:" msgstr "" #: guidata\dataset\qtitemwidgets.py:272 guidata\dataset\qtitemwidgets.py:335 msgid "Value is forced to %d" msgstr "" #: guidata\dataset\qtitemwidgets.py:644 msgid "%s files" msgstr "" #: guidata\dataset\qtitemwidgets.py:646 msgid "All supported files" msgstr "" #: guidata\dataset\qtitemwidgets.py:873 msgid "Number of rows x Number of columns" msgstr "" #: guidata\dataset\qtitemwidgets.py:876 msgid "Edit array contents" msgstr "" #: guidata\dataset\qtitemwidgets.py:882 msgid "Smallest element in array" msgstr "" #: guidata\dataset\qtitemwidgets.py:886 msgid "Largest element in array" msgstr "" #: guidata\dataset\qtwidgets.py:147 guidata\tests\translations.py:16 msgid "Some required entries are incorrect" msgstr "" #: guidata\dataset\qtwidgets.py:149 msgid "Please check highlighted fields." msgstr "" #: guidata\dataset\qtwidgets.py:567 msgid "Apply" msgstr "" #: guidata\guitest.py:73 msgid "No description available" msgstr "" #: guidata\guitest.py:105 msgid "Description" msgstr "" #: guidata\guitest.py:114 msgid "Source code" msgstr "" #: guidata\guitest.py:119 msgid "Run this script" msgstr "" #: guidata\guitest.py:120 msgid "Quit" msgstr "" #: guidata\guitest.py:140 msgid "Tests - %s module" msgstr "" #: guidata\widgets\arrayeditor.py:535 guidata\widgets\collectionseditor.py:842 #: guidata\widgets\dataframeeditor.py:668 msgid "Copy" msgstr "" #: guidata\widgets\arrayeditor.py:583 guidata\widgets\collectionseditor.py:527 #: guidata\widgets\collectionseditor.py:1315 #: guidata\widgets\collectionseditor.py:1328 msgid "Warning" msgstr "" #: guidata\widgets\arrayeditor.py:584 msgid "It was not possible to copy values for this array" msgstr "" #: guidata\widgets\arrayeditor.py:626 guidata\widgets\arrayeditor.py:661 #: guidata\widgets\dataframeeditor.py:780 #: guidata\widgets\dataframeeditor.py:842 msgid "Format" msgstr "" #: guidata\widgets\arrayeditor.py:631 guidata\widgets\dataframeeditor.py:784 msgid "Resize" msgstr "" #: guidata\widgets\arrayeditor.py:634 guidata\widgets\dataframeeditor.py:788 msgid "Background color" msgstr "" #: guidata\widgets\arrayeditor.py:662 guidata\widgets\dataframeeditor.py:843 msgid "Float formatting" msgstr "" #: guidata\widgets\arrayeditor.py:672 msgid "Format (%s) is incorrect" msgstr "" #: guidata\widgets\arrayeditor.py:672 guidata\widgets\collectionseditor.py:543 #: guidata\widgets\dataframeeditor.py:853 #: guidata\widgets\dataframeeditor.py:857 msgid "Error" msgstr "" #: guidata\widgets\arrayeditor.py:713 msgid "Arrays with more than 3 dimensions are not supported" msgstr "" #: guidata\widgets\arrayeditor.py:717 msgid "The 'xlabels' argument length do no match array column number" msgstr "" #: guidata\widgets\arrayeditor.py:722 msgid "The 'ylabels' argument length do no match array row number" msgstr "" #: guidata\widgets\arrayeditor.py:732 msgid "%s arrays" msgstr "" #: guidata\widgets\arrayeditor.py:733 msgid "%s are currently not supported" msgstr "" #: guidata\widgets\arrayeditor.py:740 msgid "NumPy array" msgstr "" #: guidata\widgets\arrayeditor.py:742 guidata\widgets\arrayeditor.py:934 msgid "Array editor" msgstr "" #: guidata\widgets\arrayeditor.py:744 msgid "read only" msgstr "" #: guidata\widgets\arrayeditor.py:781 msgid "Record array fields:" msgstr "" #: guidata\widgets\arrayeditor.py:793 msgid "Data" msgstr "" #: guidata\widgets\arrayeditor.py:793 msgid "Mask" msgstr "" #: guidata\widgets\arrayeditor.py:793 msgid "Masked data" msgstr "" #: guidata\widgets\arrayeditor.py:804 msgid "Axis:" msgstr "" #: guidata\widgets\arrayeditor.py:809 msgid "Index:" msgstr "" #: guidata\widgets\arrayeditor.py:822 msgid "Warning: changes are applied separately" msgstr "" #: guidata\widgets\arrayeditor.py:824 msgid "For performance reasons, changes applied to masked array won't be reflected in array's data (and vice-versa)." msgstr "" #: guidata\widgets\arrayeditor.py:835 #: guidata\widgets\collectionseditor.py:1584 #: guidata\widgets\dataframeeditor.py:804 guidata\widgets\texteditor.py:71 msgid "Save and Close" msgstr "" #: guidata\widgets\arrayeditor.py:840 #: guidata\widgets\collectionseditor.py:1589 #: guidata\widgets\dataframeeditor.py:809 guidata\widgets\texteditor.py:76 msgid "Close" msgstr "" #: guidata\widgets\collectionseditor.py:205 msgid "Index" msgstr "" #: guidata\widgets\collectionseditor.py:207 msgid "Name" msgstr "" #: guidata\widgets\collectionseditor.py:210 msgid "Tuple" msgstr "" #: guidata\widgets\collectionseditor.py:213 msgid "List" msgstr "" #: guidata\widgets\collectionseditor.py:216 msgid "Dictionary" msgstr "" #: guidata\widgets\collectionseditor.py:218 msgid "Key" msgstr "" #: guidata\widgets\collectionseditor.py:223 msgid "Attribute" msgstr "" #: guidata\widgets\collectionseditor.py:226 msgid "elements" msgstr "" #: guidata\widgets\collectionseditor.py:418 msgid "Size" msgstr "" #: guidata\widgets\collectionseditor.py:418 msgid "Type" msgstr "" #: guidata\widgets\collectionseditor.py:418 msgid "Value" msgstr "" #: guidata\widgets\collectionseditor.py:528 msgid "" "Opening this variable can be slow\n" "\n" "Do you want to continue anyway?" msgstr "" #: guidata\widgets\collectionseditor.py:544 msgid "Spyder was unable to retrieve the value of this variable from the console.

The error mesage was:
%s" msgstr "" #: guidata\widgets\collectionseditor.py:764 msgid "Edit item" msgstr "" #: guidata\widgets\collectionseditor.py:765 msgid "Unable to assign data to item.

Error message:
%s" msgstr "" #: guidata\widgets\collectionseditor.py:836 msgid "Resize rows to contents" msgstr "" #: guidata\widgets\collectionseditor.py:839 msgid "Paste" msgstr "" #: guidata\widgets\collectionseditor.py:845 msgid "Edit" msgstr "" #: guidata\widgets\collectionseditor.py:849 #: guidata\widgets\collectionseditor.py:1247 #: guidata\widgets\collectionseditor.py:1266 msgid "Plot" msgstr "" #: guidata\widgets\collectionseditor.py:856 msgid "Histogram" msgstr "" #: guidata\widgets\collectionseditor.py:863 msgid "Show image" msgstr "" #: guidata\widgets\collectionseditor.py:870 #: guidata\widgets\collectionseditor.py:1274 msgid "Save array" msgstr "" #: guidata\widgets\collectionseditor.py:876 #: guidata\widgets\collectionseditor.py:1204 #: guidata\widgets\collectionseditor.py:1213 msgid "Insert" msgstr "" #: guidata\widgets\collectionseditor.py:880 #: guidata\widgets\collectionseditor.py:1141 msgid "Remove" msgstr "" #: guidata\widgets\collectionseditor.py:885 msgid "Show arrays min/max" msgstr "" #: guidata\widgets\collectionseditor.py:890 #: guidata\widgets\collectionseditor.py:1160 msgid "Rename" msgstr "" #: guidata\widgets\collectionseditor.py:894 #: guidata\widgets\collectionseditor.py:1163 msgid "Duplicate" msgstr "" #: guidata\widgets\collectionseditor.py:1137 msgid "Do you want to remove the selected item?" msgstr "" #: guidata\widgets\collectionseditor.py:1138 msgid "Do you want to remove all selected items?" msgstr "" #: guidata\widgets\collectionseditor.py:1161 msgid "New variable name:" msgstr "" #: guidata\widgets\collectionseditor.py:1164 msgid "Variable name:" msgstr "" #: guidata\widgets\collectionseditor.py:1204 msgid "Key:" msgstr "" #: guidata\widgets\collectionseditor.py:1213 msgid "Value:" msgstr "" #: guidata\widgets\collectionseditor.py:1233 msgid "Import error" msgstr "" #: guidata\widgets\collectionseditor.py:1234 msgid "Please install matplotlib or guiqwt." msgstr "" #: guidata\widgets\collectionseditor.py:1248 msgid "Unable to plot data.

Error message:
%s" msgstr "" #: guidata\widgets\collectionseditor.py:1267 msgid "Unable to show image.

Error message:
%s" msgstr "" #: guidata\widgets\collectionseditor.py:1279 msgid "NumPy arrays" msgstr "" #: guidata\widgets\collectionseditor.py:1293 msgid "Unable to save array

Error message:
%s" msgstr "" #: guidata\widgets\collectionseditor.py:1316 msgid "It was not possible to copy this array" msgstr "" #: guidata\widgets\collectionseditor.py:1329 msgid "It was not possible to copy this dataframe" msgstr "" #: guidata\widgets\collectionseditor.py:1351 msgid "Clipboard contents" msgstr "" #: guidata\widgets\collectionseditor.py:1366 msgid "Import from clipboard" msgstr "" #: guidata\widgets\collectionseditor.py:1369 msgid "Empty clipboard" msgstr "" #: guidata\widgets\collectionseditor.py:1369 msgid "Nothing to be imported from clipboard." msgstr "" #: guidata\widgets\dataframeeditor.py:331 msgid "" "It is not possible to display this value because\n" "an error ocurred while trying to do it" msgstr "" #: guidata\widgets\dataframeeditor.py:675 msgid "To bool" msgstr "" #: guidata\widgets\dataframeeditor.py:676 msgid "To complex" msgstr "" #: guidata\widgets\dataframeeditor.py:677 msgid "To int" msgstr "" #: guidata\widgets\dataframeeditor.py:678 msgid "To float" msgstr "" #: guidata\widgets\dataframeeditor.py:679 msgid "To str" msgstr "" #: guidata\widgets\dataframeeditor.py:759 msgid "%s editor" msgstr "" #: guidata\widgets\dataframeeditor.py:794 msgid "Column min/max" msgstr "" #: guidata\widgets\dataframeeditor.py:852 msgid "Format ({}) is incorrect" msgstr "" #: guidata\widgets\dataframeeditor.py:856 msgid "Format ({}) should start with '%'" msgstr "" #: guidata\widgets\importwizard.py:166 guidata\widgets\importwizard.py:516 msgid "Import as" msgstr "" #: guidata\widgets\importwizard.py:168 msgid "data" msgstr "" #: guidata\widgets\importwizard.py:172 msgid "code" msgstr "" #: guidata\widgets\importwizard.py:175 guidata\widgets\importwizard.py:606 msgid "text" msgstr "" #: guidata\widgets\importwizard.py:187 msgid "Column separator:" msgstr "" #: guidata\widgets\importwizard.py:191 msgid "Tab" msgstr "" #: guidata\widgets\importwizard.py:194 msgid "Whitespace" msgstr "" #: guidata\widgets\importwizard.py:197 guidata\widgets\importwizard.py:215 msgid "other" msgstr "" #: guidata\widgets\importwizard.py:208 msgid "Row separator:" msgstr "" #: guidata\widgets\importwizard.py:212 msgid "EOL" msgstr "" #: guidata\widgets\importwizard.py:227 msgid "Additional options" msgstr "" #: guidata\widgets\importwizard.py:231 msgid "Skip rows:" msgstr "" #: guidata\widgets\importwizard.py:241 msgid "Comments:" msgstr "" #: guidata\widgets\importwizard.py:247 msgid "Transpose" msgstr "" #: guidata\widgets\importwizard.py:519 msgid "array" msgstr "" #: guidata\widgets\importwizard.py:524 msgid "list" msgstr "" #: guidata\widgets\importwizard.py:529 msgid "DataFrame" msgstr "" #: guidata\widgets\importwizard.py:589 guidata\widgets\importwizard.py:676 msgid "Import wizard" msgstr "" #: guidata\widgets\importwizard.py:594 msgid "Raw text" msgstr "" #: guidata\widgets\importwizard.py:597 msgid "variable_name" msgstr "" #: guidata\widgets\importwizard.py:608 msgid "table" msgstr "" #: guidata\widgets\importwizard.py:609 msgid "Preview" msgstr "" #: guidata\widgets\importwizard.py:613 msgid "Variable Name" msgstr "" #: guidata\widgets\importwizard.py:621 msgid "Cancel" msgstr "" #: guidata\widgets\importwizard.py:626 msgid "Previous" msgstr "" #: guidata\widgets\importwizard.py:630 msgid "Next" msgstr "" #: guidata\widgets\importwizard.py:635 msgid "Done" msgstr "" #: guidata\widgets\importwizard.py:677 msgid "Unable to proceed to next step

Please check your entries.

Error message:
%s" msgstr "" #: guidata\widgets\syntaxhighlighters.py:37 msgid "Background:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:38 msgid "Current line:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:39 msgid "Current cell:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:40 msgid "Occurrence:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:41 msgid "Link:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:42 msgid "Side areas:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:43 msgid "Matched
parens:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:44 msgid "Unmatched
parens:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:45 msgid "Normal text:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:46 msgid "Keyword:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:47 msgid "Builtin:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:48 msgid "Definition:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:49 msgid "Comment:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:50 msgid "String:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:51 msgid "Number:" msgstr "" #: guidata\widgets\syntaxhighlighters.py:52 msgid "Instance:" msgstr "" #: guidata\widgets\texteditor.py:89 msgid "Text editor" msgstr "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640603538.0 guidata-2.0.2/guidata/qthelpers.py0000666000000000000000000002136000000000000014025 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ qthelpers --------- The ``guidata.qthelpers`` module provides helper functions for developing easily Qt-based graphical user interfaces. """ import sys import os import os.path as osp from qtpy.QtWidgets import ( QAction, QApplication, QHBoxLayout, QLabel, QLineEdit, QMenu, QPushButton, QStyle, QToolButton, QVBoxLayout, QWidget, QGroupBox, ) from qtpy.QtGui import QColor, QIcon, QKeySequence from qtpy.QtCore import Qt # Local imports: from guidata.external import darkdetect from guidata.configtools import get_icon def win32_fix_title_bar_background(widget): """Fix window title bar background for Windows 10+ dark theme""" if os.name != "nt" or not darkdetect.isDark(): return import ctypes from ctypes import wintypes class ACCENTPOLICY(ctypes.Structure): _fields_ = [ ("AccentState", ctypes.c_uint), ("AccentFlags", ctypes.c_uint), ("GradientColor", ctypes.c_uint), ("AnimationId", ctypes.c_uint), ] class WINDOWCOMPOSITIONATTRIBDATA(ctypes.Structure): _fields_ = [ ("Attribute", ctypes.c_int), ("Data", ctypes.POINTER(ctypes.c_int)), ("SizeOfData", ctypes.c_size_t), ] accent = ACCENTPOLICY() accent.AccentState = 1 # Default window Blur #ACCENT_ENABLE_BLURBEHIND data = WINDOWCOMPOSITIONATTRIBDATA() data.Attribute = 26 # WCA_USEDARKMODECOLORS data.SizeOfData = ctypes.sizeof(accent) data.Data = ctypes.cast(ctypes.pointer(accent), ctypes.POINTER(ctypes.c_int)) set_win_cpa = ctypes.windll.user32.SetWindowCompositionAttribute set_win_cpa.argtypes = (wintypes.HWND, WINDOWCOMPOSITIONATTRIBDATA) set_win_cpa.restype = ctypes.c_int set_win_cpa(int(widget.winId()), data) def text_to_qcolor(text): """Create a QColor from specified string""" color = QColor() if text is not None and text.startswith("#") and len(text) == 7: correct = "#0123456789abcdef" for char in text: if char.lower() not in correct: return color elif text not in list(QColor.colorNames()): return color color.setNamedColor(text) return color def create_action( parent, title, triggered=None, toggled=None, shortcut=None, icon=None, tip=None, checkable=None, context=Qt.WindowShortcut, enabled=None, ): """ Create a new QAction """ action = QAction(title, parent) if triggered: if checkable: action.triggered.connect(triggered) else: action.triggered.connect(lambda checked=False: triggered()) if checkable is not None: # Action may be checkable even if the toggled signal is not connected action.setCheckable(checkable) if toggled: action.toggled.connect(toggled) action.setCheckable(True) if icon is not None: assert isinstance(icon, QIcon) action.setIcon(icon) if shortcut is not None: action.setShortcut(shortcut) if tip is not None: action.setToolTip(tip) action.setStatusTip(tip) if enabled is not None: action.setEnabled(enabled) action.setShortcutContext(context) return action def create_toolbutton( parent, icon=None, text=None, triggered=None, tip=None, toggled=None, shortcut=None, autoraise=True, enabled=None, ): """Create a QToolButton""" if autoraise: button = QToolButton(parent) else: button = QPushButton(parent) if text is not None: button.setText(text) if icon is not None: if isinstance(icon, str): icon = get_icon(icon) button.setIcon(icon) if text is not None or tip is not None: button.setToolTip(text if tip is None else tip) if autoraise: button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) button.setAutoRaise(True) if triggered is not None: button.clicked.connect(lambda checked=False: triggered()) if toggled is not None: button.toggled.connect(toggled) button.setCheckable(True) if shortcut is not None: button.setShortcut(shortcut) if enabled is not None: button.setEnabled(enabled) return button def create_groupbox( parent, title=None, toggled=None, checked=None, flat=False, layout=None ): """Create a QGroupBox""" if title is None: group = QGroupBox(parent) else: group = QGroupBox(title, parent) group.setFlat(flat) if toggled is not None: group.setCheckable(True) if checked is not None: group.setChecked(checked) group.toggled.connect(toggled) if layout is not None: group.setLayout(layout) return group def keybinding(attr): """Return keybinding""" ks = getattr(QKeySequence, attr) return QKeySequence.keyBindings(ks)[0].toString() def add_separator(target): """Add separator to target only if last action is not a separator""" target_actions = list(target.actions()) if target_actions: if not target_actions[-1].isSeparator(): target.addSeparator() def add_actions(target, actions): """ Add actions (list of QAction instances) to target (menu, toolbar) """ for action in actions: if isinstance(action, QAction): target.addAction(action) elif isinstance(action, QMenu): target.addMenu(action) elif action is None: add_separator(target) def _process_mime_path(path, extlist): if path.startswith(r"file://"): if os.name == "nt": # On Windows platforms, a local path reads: file:///c:/... # and a UNC based path reads like: file://server/share if path.startswith(r"file:///"): # this is a local path path = path[8:] else: # this is a unc path path = path[5:] else: path = path[7:] path = path.replace("%5C", os.sep) # Transforming backslashes if osp.exists(path): if extlist is None or osp.splitext(path)[1] in extlist: return path def mimedata2url(source, extlist=None): """ Extract url list from MIME data extlist: for example ('.py', '.pyw') """ pathlist = [] if source.hasUrls(): for url in source.urls(): path = _process_mime_path(str(url.toString()), extlist) if path is not None: pathlist.append(path) elif source.hasText(): for rawpath in str(source.text()).splitlines(): path = _process_mime_path(rawpath, extlist) if path is not None: pathlist.append(path) if pathlist: return pathlist def get_std_icon(name, size=None): """ Get standard platform icon Call 'show_std_icons()' for details """ if not name.startswith("SP_"): name = "SP_" + name icon = QWidget().style().standardIcon(getattr(QStyle, name)) if size is None: return icon else: return QIcon(icon.pixmap(size, size)) class ShowStdIcons(QWidget): """ Dialog showing standard icons """ def __init__(self, parent): QWidget.__init__(self, parent) layout = QHBoxLayout() row_nb = 14 cindex = 0 col_layout = QVBoxLayout() for child in dir(QStyle): if child.startswith("SP_"): if cindex == 0: col_layout = QVBoxLayout() icon_layout = QHBoxLayout() icon = get_std_icon(child) label = QLabel() label.setPixmap(icon.pixmap(32, 32)) icon_layout.addWidget(label) icon_layout.addWidget(QLineEdit(child.replace("SP_", ""))) col_layout.addLayout(icon_layout) cindex = (cindex + 1) % row_nb if cindex == 0: layout.addLayout(col_layout) self.setLayout(layout) self.setWindowTitle("Standard Platform Icons") self.setWindowIcon(get_std_icon("TitleBarMenuButton")) def show_std_icons(): """ Show all standard Icons """ app = QApplication(sys.argv) dialog = ShowStdIcons(None) dialog.show() sys.exit(app.exec_()) if __name__ == "__main__": show_std_icons() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640621847.0 guidata-2.0.2/guidata/qtwidgets.py0000666000000000000000000001047000000000000014031 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2011 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ qtwidgets --------- The ``guidata.qtwidgets`` module provides ready-to-use or generic widgets for developing easily Qt-based graphical user interfaces. """ from math import cos, sin, pi from qtpy.QtWidgets import QLabel, QWidget, QDockWidget from qtpy.QtGui import QPainter, QPen from qtpy.QtCore import QSize, Qt # Local imports: from guidata.configtools import get_family class RotatedLabel(QLabel): """ Rotated QLabel (rich text is not supported) Arguments: * parent: parent widget * angle=270 (int): rotation angle in degrees * family (string): font family * bold (bool): font weight * italic (bool): font italic style * color (QColor): font color """ def __init__( self, text, parent=None, angle=270, family=None, bold=False, italic=False, color=None, ): QLabel.__init__(self, text, parent) font = self.font() if family is not None: font.setFamily(get_family(family)) font.setBold(bold) font.setItalic(italic) self.setFont(font) self.color = color self.angle = angle self.setAlignment(Qt.AlignCenter) def paintEvent(self, evt): painter = QPainter(self) if self.color is not None: painter.setPen(QPen(self.color)) painter.rotate(self.angle) transform = painter.transform().inverted()[0] rct = transform.mapRect(self.rect()) painter.drawText(rct, self.alignment(), self.text()) def sizeHint(self): hint = QLabel.sizeHint(self) width, height = hint.width(), hint.height() angle = self.angle * pi / 180 rotated_width = int(abs(width * cos(angle)) + abs(height * sin(angle))) rotated_height = int(abs(width * sin(angle)) + abs(height * cos(angle))) return QSize(rotated_width, rotated_height) def minimumSizeHint(self): return self.sizeHint() class DockableWidgetMixin(object): ALLOWED_AREAS = Qt.AllDockWidgetAreas LOCATION = Qt.TopDockWidgetArea FEATURES = ( QDockWidget.DockWidgetClosable | QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable ) def __init__(self): self._isvisible = False self.dockwidget = None self._allowed_areas = self.ALLOWED_AREAS self._location = self.LOCATION self._features = self.FEATURES @property def parent_widget(self): """Return associated QWidget parent""" return self.parent() def setup_dockwidget(self, location=None, features=None, allowed_areas=None): assert ( self.dockwidget is None ), "Dockwidget must be setup before calling 'create_dockwidget'" if location is not None: self._location = location if features is not None: self._features = features if allowed_areas is not None: self._allowed_areas = allowed_areas def get_focus_widget(self): pass def create_dockwidget(self, title): """Add to parent QMainWindow as a dock widget""" dock = QDockWidget(title, self.parent_widget) dock.setObjectName(self.__class__.__name__ + "_dw") dock.setAllowedAreas(self._allowed_areas) dock.setFeatures(self._features) dock.setWidget(self) dock.visibilityChanged.connect(self.visibility_changed) self.dockwidget = dock return (dock, self._location) def is_visible(self): return self._isvisible def visibility_changed(self, enable): """DockWidget visibility has changed""" if enable: self.dockwidget.raise_() widget = self.get_focus_widget() if widget is not None: widget.setFocus() self._isvisible = enable and self.dockwidget.isVisible() class DockableWidget(QWidget, DockableWidgetMixin): def __init__(self, parent): QWidget.__init__(self, parent) DockableWidgetMixin.__init__(self) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640626112.1130245 guidata-2.0.2/guidata/tests/0000777000000000000000000000000000000000000012604 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628151429.0 guidata-2.0.2/guidata/tests/__init__.py0000666000000000000000000000064600000000000014723 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ guidata test package ==================== """ def run(): """Run guidata test launcher""" import guidata from guidata.guitest import run_testlauncher run_testlauncher(guidata) if __name__ == "__main__": run() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628151429.0 guidata-2.0.2/guidata/tests/activable_dataset.py0000666000000000000000000000305000000000000016613 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ ActivableDataSet example Warning: ActivableDataSet objects were made to be integrated inside GUI layouts. So this example with dialog boxes may be confusing. --> see tests/editgroupbox.py to understand the activable dataset usage """ # When editing, all items are shown. # When showing dataset in read-only mode (e.g. inside another layout), all items # are shown except the enable item. SHOW = True # Show test in GUI-based test launcher from guidata.dataset.datatypes import ActivableDataSet from guidata.dataset.dataitems import BoolItem, FloatItem, ChoiceItem, ColorItem class ExampleDataSet(ActivableDataSet): """ Example Activable dataset example """ enable = BoolItem( "Enable parameter set", help="If disabled, the following parameters will be ignored", default=False, ) param0 = ChoiceItem("Param 0", ["choice #1", "choice #2", "choice #3"]) param1 = FloatItem("Param 1", default=0, min=0) param2 = FloatItem("Param 2", default=0.93) color = ColorItem("Color", default="red") ExampleDataSet.active_setup() if __name__ == "__main__": # Create QApplication import guidata _app = guidata.qapplication() # Editing mode: prm = ExampleDataSet() prm.set_writeable() prm.edit() # Showing mode: prm.set_readonly() prm.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628151429.0 guidata-2.0.2/guidata/tests/activable_items.py0000666000000000000000000000202700000000000016312 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2012 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ Example with activable items: items which active state is changed depending on another item's value. """ SHOW = True # Show test in GUI-based test launcher from guidata.dataset.dataitems import ChoiceItem, FloatItem from guidata.dataset.datatypes import DataSet, GetAttrProp, FuncProp choices = (("A", "Choice #1: A"), ("B", "Choice #2: B"), ("C", "Choice #3: C")) class Test(DataSet): _prop = GetAttrProp("choice") choice = ChoiceItem("Choice", choices).set_prop("display", store=_prop) x1 = FloatItem("x1") x2 = FloatItem("x2").set_prop("display", active=FuncProp(_prop, lambda x: x == "B")) x3 = FloatItem("x3").set_prop("display", active=FuncProp(_prop, lambda x: x == "C")) if __name__ == "__main__": # Create QApplication import guidata _app = guidata.qapplication() test = Test() test.edit() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/tests/all_features.py0000666000000000000000000001057500000000000015634 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ All guidata item/group features demo """ SHOW = True # Show test in GUI-based test launcher import tempfile, atexit, shutil import numpy as np from guidata.dataset.datatypes import ( DataSet, BeginTabGroup, EndTabGroup, BeginGroup, EndGroup, ObjectItem, ) from guidata.dataset.dataitems import ( FloatItem, IntItem, BoolItem, ChoiceItem, MultipleChoiceItem, ImageChoiceItem, FilesOpenItem, StringItem, TextItem, ColorItem, FileSaveItem, FileOpenItem, DirectoryItem, FloatArrayItem, ) from guidata.dataset.qtwidgets import DataSetEditLayout, DataSetShowLayout from guidata.dataset.qtitemwidgets import DataSetWidget # Creating temporary files and registering cleanup functions TEMPDIR = tempfile.mkdtemp(prefix="test_") atexit.register(shutil.rmtree, TEMPDIR) FILE_ETA = tempfile.NamedTemporaryFile(suffix=".eta", dir=TEMPDIR) atexit.register(FILE_ETA.close) FILE_CSV = tempfile.NamedTemporaryFile(suffix=".csv", dir=TEMPDIR) atexit.register(FILE_CSV.close) class SubDataSet(DataSet): dir = DirectoryItem("Directory", TEMPDIR) fname = FileOpenItem("Single file (open)", ("csv", "eta"), FILE_CSV.name) fnames = FilesOpenItem("Multiple files", "csv", FILE_CSV.name) fname_s = FileSaveItem("Single file (save)", "eta", FILE_ETA.name) class SubDataSetWidget(DataSetWidget): klass = SubDataSet class SubDataSetItem(ObjectItem): klass = SubDataSet DataSetEditLayout.register(SubDataSetItem, SubDataSetWidget) DataSetShowLayout.register(SubDataSetItem, SubDataSetWidget) class TestParameters(DataSet): """ DataSet test The following text is the DataSet 'comment':
Plain text or rich text2 are both supported, as well as special characters (α, β, γ, δ, ...) """ files = SubDataSetItem("files") string = StringItem("String") text = TextItem("Text") _bg = BeginGroup("A sub group") float_slider = FloatItem( "Float (with slider)", default=0.5, min=0, max=1, step=0.01, slider=True ) fl1 = FloatItem( "Current", default=10.0, min=1, max=30, unit="mA", help="Threshold current" ) fl2 = FloatItem( "Float (col=1)", default=1.0, min=1, max=1, help="Help on float item" ).set_pos(col=1) fl3 = FloatItem("Not checked float").set_prop("data", check_value=False) bool1 = BoolItem("Boolean option without label") bool2 = BoolItem("Boolean option with label", "Label").set_pos(col=1, colspan=2) color = ColorItem("Color", default="red") choice1 = ChoiceItem( "Single choice (radio)", [(16, "first choice"), (32, "second choice"), (64, "third choice")], radio=True, ).set_pos(col=1, colspan=2) choice2 = ChoiceItem( "Single choice (combo)", [(16, "first choice"), (32, "second choice"), (64, "third choice")], ).set_pos(col=1, colspan=2) _eg = EndGroup("A sub group") floatarray = FloatArrayItem( "Float array", default=np.ones((50, 5), float), format=" %.2e " ).set_pos(col=1) g0 = BeginTabGroup("group") mchoice1 = MultipleChoiceItem( "MC type 1", ["first choice", "second choice", "third choice"] ).vertical(2) mchoice2 = ( ImageChoiceItem( "MC type 2", [ ("rect", "first choice", "gif.png"), ("ell", "second choice", "txt.png"), ("qcq", "third choice", "file.png"), ], ) .set_pos(col=1) .set_prop("display", icon="file.png") ) mchoice3 = MultipleChoiceItem("MC type 3", [str(i) for i in range(10)]).horizontal( 2 ) eg0 = EndTabGroup("group") integer_slider = IntItem( "Integer (with slider)", default=5, min=-50, max=100, slider=True ) integer = IntItem("Integer", default=5, min=3, max=6).set_pos(col=1) if __name__ == "__main__": # Create QApplication import guidata _app = guidata.qapplication() e = TestParameters() e.floatarray[:, 0] = np.linspace(-5, 5, 50) print(e) if e.edit(): e.edit() print(e) e.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/tests/all_items.py0000666000000000000000000000715400000000000015136 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ All guidata DataItem objects demo A DataSet object is a set of parameters of various types (integer, float, boolean, string, etc.) which may be edited in a dialog box thanks to the 'edit' method. Parameters are defined by assigning DataItem objects to a DataSet class definition: each parameter type has its own DataItem class (IntItem for integers, FloatItem for floats, StringItem for strings, etc.) """ SHOW = True # Show test in GUI-based test launcher import tempfile, atexit, shutil, datetime, numpy as np from guidata.dataset.datatypes import DataSet, BeginGroup, EndGroup from guidata.dataset.dataitems import ( FloatItem, IntItem, BoolItem, ChoiceItem, MultipleChoiceItem, ImageChoiceItem, FilesOpenItem, StringItem, TextItem, ColorItem, FileSaveItem, FileOpenItem, DirectoryItem, FloatArrayItem, DateItem, DateTimeItem, ) # Creating temporary files and registering cleanup functions TEMPDIR = tempfile.mkdtemp(prefix="test_") atexit.register(shutil.rmtree, TEMPDIR) FILE_ETA = tempfile.NamedTemporaryFile(suffix=".eta", dir=TEMPDIR) atexit.register(FILE_ETA.close) FILE_CSV = tempfile.NamedTemporaryFile(suffix=".csv", dir=TEMPDIR) atexit.register(FILE_CSV.close) class TestParameters(DataSet): """ DataSet test The following text is the DataSet 'comment':
Plain text or rich text2 are both supported, as well as special characters (α, β, γ, δ, ...) """ dir = DirectoryItem("Directory", TEMPDIR) fname = FileOpenItem("Open file", ("csv", "eta"), FILE_CSV.name) fnames = FilesOpenItem("Open files", "csv", FILE_CSV.name) fname_s = FileSaveItem("Save file", "eta", FILE_ETA.name) string = StringItem("String") text = TextItem("Text") float_slider = FloatItem( "Float (with slider)", default=0.5, min=0, max=1, step=0.01, slider=True ) integer = IntItem("Integer", default=5, min=3, max=16, slider=True).set_pos(col=1) dtime = DateTimeItem("Date/time", default=datetime.datetime(2010, 10, 10)) date = DateItem("Date", default=datetime.date(2010, 10, 10)).set_pos(col=1) bool1 = BoolItem("Boolean option without label") bool2 = BoolItem("Boolean option with label", "Label") _bg = BeginGroup("A sub group") color = ColorItem("Color", default="red") choice = ChoiceItem( "Single choice 1", [("16", "first choice"), ("32", "second choice"), ("64", "third choice")], ) mchoice2 = ImageChoiceItem( "Single choice 2", [ ("rect", "first choice", "gif.png"), ("ell", "second choice", "txt.png"), ("qcq", "third choice", "file.png"), ], ) _eg = EndGroup("A sub group") floatarray = FloatArrayItem( "Float array", default=np.ones((50, 5), float), format=" %.2e " ).set_pos(col=1) mchoice3 = MultipleChoiceItem("MC type 1", [str(i) for i in range(12)]).horizontal( 4 ) mchoice1 = ( MultipleChoiceItem( "MC type 2", ["first choice", "second choice", "third choice"] ) .vertical(1) .set_pos(col=1) ) if __name__ == "__main__": # Create QApplication import guidata _app = guidata.qapplication() e = TestParameters() e.floatarray[:, 0] = np.linspace(-5, 5, 50) print(e) if e.edit(): print(e) e.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/tests/bool_selector.py0000666000000000000000000000352300000000000016014 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ DataItem groups and group selection DataSet items may be included in groups (these items are then shown in group box widgets in the editing dialog box) and item groups may be enabled/disabled using one group parameter (a boolean item). """ SHOW = True # Show test in GUI-based test launcher from guidata.dataset.datatypes import DataSet, BeginGroup, EndGroup, ValueProp from guidata.dataset.dataitems import BoolItem, FloatItem prop1 = ValueProp(False) prop2 = ValueProp(False) class GroupSelection(DataSet): """ Group selection test Group selection example: """ g1 = BeginGroup("group 1") enable1 = BoolItem( "Enable parameter set #1", help="If disabled, the following parameters will be ignored", default=False, ).set_prop("display", store=prop1) param1_1 = FloatItem("Param 1.1", default=0, min=0).set_prop( "display", active=prop1 ) param1_2 = FloatItem("Param 1.2", default=0.93).set_prop("display", active=prop1) _g1 = EndGroup("group 1") g2 = BeginGroup("group 2") enable2 = BoolItem( "Enable parameter set #2", help="If disabled, the following parameters will be ignored", default=True, ).set_prop("display", store=prop2) param2_1 = FloatItem("Param 2.1", default=0, min=0).set_prop( "display", active=prop2 ) param2_2 = FloatItem("Param 2.2", default=0.93).set_prop("display", active=prop2) _g2 = EndGroup("group 2") if __name__ == "__main__": # Create QApplication import guidata _app = guidata.qapplication() prm = GroupSelection() prm.edit() print(prm) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/tests/callbacks.py0000666000000000000000000000362100000000000015077 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2011 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ Demonstrates how items may trigger callbacks when activated """ SHOW = True # Show test in GUI-based test launcher from guidata.dataset.datatypes import DataSet from guidata.dataset.dataitems import ( ChoiceItem, StringItem, TextItem, ColorItem, FloatItem, ) class TestParameters(DataSet): def cb_example(self, item, value): print("\nitem: ", item, "\nvalue:", value) if self.results is None: self.results = "" self.results += str(value) + "\n" print("results:", self.results) def update_x1plusx2(self, item, value): print("\nitem: ", item, "\nvalue:", value) if self.x1 is not None and self.x2 is not None: self.x1plusx2 = self.x1 + self.x2 else: self.x1plusx2 = None string = StringItem("String", default="foobar").set_prop( "display", callback=cb_example ) x1 = FloatItem("x1").set_prop("display", callback=update_x1plusx2) x2 = FloatItem("x2").set_prop("display", callback=update_x1plusx2) x1plusx2 = FloatItem("x1+x2").set_prop("display", active=False) color = ColorItem("Color", default="red").set_prop("display", callback=cb_example) choice = ( ChoiceItem( "Single choice", [(16, "first choice"), (32, "second choice"), (64, "third choice")], default=64, ) .set_pos(col=1, colspan=2) .set_prop("display", callback=cb_example) ) results = TextItem("Results") if __name__ == "__main__": # Create QApplication import guidata _app = guidata.qapplication() e = TestParameters() print(e) if e.edit(): print(e) e.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628151429.0 guidata-2.0.2/guidata/tests/config.py0000666000000000000000000000273000000000000014425 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """Config test""" SHOW = False # Do not show test in GUI-based test launcher import unittest from guidata.tests.all_features import TestParameters from guidata.config import UserConfig class TestBasic(unittest.TestCase): def setUp(self): self.test_save() def test_save(self): eta = TestParameters() eta.write_config(CONF, "TestParameters", "") # print "fin test_save" def test_load(self): eta = TestParameters() eta.read_config(CONF, "TestParameters", "") # print "fin test_load" def test_default(self): eta = TestParameters() eta.write_config(CONF, "etagere2", "") eta = TestParameters() eta.read_config(CONF, "etagere2", "") self.assertEqual(eta.fl2, 1.0) self.assertEqual(eta.integer, 5) # print "fin test_default" def test_restore(self): eta = TestParameters() eta.fl2 = 2 eta.integer = 6 eta.write_config(CONF, "etagere3", "") eta = TestParameters() eta.read_config(CONF, "etagere3", "") self.assertEqual(eta.fl2, 2.0) self.assertEqual(eta.integer, 6) # print "fin test_restore" if __name__ == "__main__": CONF = UserConfig({}) unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628151429.0 guidata-2.0.2/guidata/tests/data.py0000666000000000000000000000264200000000000014073 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """Unit tests""" SHOW = False # Do not show test in GUI-based test launcher import unittest from guidata.dataset.datatypes import DataSet from guidata.dataset.dataitems import FloatItem, IntItem from guidata.utils import update_dataset class Parameters(DataSet): float1 = FloatItem("float #1", min=1, max=250, help="height in cm") float2 = FloatItem("float #2", min=1, max=250, help="width in cm") number = IntItem("number", min=3, max=20) class TestCheck(unittest.TestCase): def test_range(self): """Test range checking of FloatItem""" e = Parameters() e.float1 = 150.0 e.float2 = 400.0 e.number = 4 errors = e.check() self.assertEquals(errors, ["float2"]) def test_typechecking(self): """Test type checking of FloatItem""" e = Parameters() e.float1 = 150 e.float2 = 400 e.number = 4.0 errors = e.check() self.assertEquals(errors, ["float1", "float2", "number"]) def test_update(self): e1 = Parameters() e2 = Parameters() e1.float1 = 23 update_dataset(e2, e1) self.assertEquals(e2.float1, 23) if __name__ == "__main__": unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/tests/datasetgroup.py0000666000000000000000000000143600000000000015664 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ DataSetGroup demo DataSet objects may be grouped into DataSetGroup, allowing them to be edited in a single dialog box (with one tab per DataSet object). """ SHOW = True # Show test in GUI-based test launcher from guidata.tests.all_features import TestParameters from guidata.dataset.datatypes import DataSetGroup if __name__ == "__main__": # Create QApplication import guidata _app = guidata.qapplication() e1 = TestParameters("DataSet #1") e2 = TestParameters("DataSet #2") g = DataSetGroup([e1, e2], title="Parameters group") g.edit() print(e1) g.edit() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628151429.0 guidata-2.0.2/guidata/tests/disthelpers.py0000666000000000000000000000145300000000000015507 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2011 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ guidata.disthelpers demo How to create an executable with py2exe or cx_Freeze with less efforts than writing a complete setup script. """ SHOW = True # Show test in GUI-based test launcher import os.path as osp from guidata.disthelpers import Distribution if __name__ == "__main__": dist = Distribution() dist.setup( name="Application demo", version="1.0.0", description="Application demo based on editgroupbox.py", script=osp.join(osp.dirname(__file__), "editgroupbox.py"), target_name="demo.exe", ) dist.add_modules("guidata") dist.build("cx_Freeze") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/tests/editgroupbox.py0000666000000000000000000001311700000000000015674 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ DataSetEditGroupBox and DataSetShowGroupBox demo These group box widgets are intended to be integrated in a GUI application layout, showing read-only parameter sets or allowing to edit parameter values. """ SHOW = True # Show test in GUI-based test launcher from qtpy.QtWidgets import QMainWindow, QSplitter from guidata.dataset import datatypes as gdt from guidata.dataset import dataitems as gdi from guidata.dataset.qtwidgets import DataSetShowGroupBox, DataSetEditGroupBox from guidata.configtools import get_icon from guidata.qthelpers import ( create_action, add_actions, get_std_icon, win32_fix_title_bar_background, ) # Local test import: from guidata.tests.activable_dataset import ExampleDataSet class AnotherDataSet(gdt.DataSet): """ Example 2 Simple dataset example """ param0 = gdi.ChoiceItem("Choice", ["deazdazk", "aeazee", "87575757"]) param1 = gdi.FloatItem("Foobar 1", default=0, min=0) a_group = gdt.BeginGroup("A group") param2 = gdi.FloatItem("Foobar 2", default=0.93) param3 = gdi.FloatItem("Foobar 3", default=123) _a_group = gdt.EndGroup("A group") class ExampleMultiGroupDataSet(gdt.DataSet): param0 = gdi.ChoiceItem("Choice", ["deazdazk", "aeazee", "87575757"]) param1 = gdi.FloatItem("Foobar 1", default=0, min=0) t_group = gdt.BeginTabGroup("T group") a_group = gdt.BeginGroup("A group") param2 = gdi.FloatItem("Foobar 2", default=0.93) dir1 = gdi.DirectoryItem("Directory 1") file1 = gdi.FileOpenItem("File 1") _a_group = gdt.EndGroup("A group") b_group = gdt.BeginGroup("B group") param3 = gdi.FloatItem("Foobar 3", default=123) param4 = gdi.BoolItem("Boolean") _b_group = gdt.EndGroup("B group") c_group = gdt.BeginGroup("C group") param5 = gdi.FloatItem("Foobar 4", default=250) param6 = gdi.DateItem("Date") param7 = gdi.ColorItem("Color") _c_group = gdt.EndGroup("C group") _t_group = gdt.EndTabGroup("T group") class OtherDataSet(gdt.DataSet): title = gdi.StringItem("Title", default="Title") icon = gdi.ChoiceItem( "Icon", ( ("python.png", "Python"), ("guidata.svg", "guidata"), ("settings.png", "Settings"), ), ) opacity = gdi.FloatItem("Opacity", default=1.0, min=0.1, max=1) class MainWindow(QMainWindow): def __init__(self): QMainWindow.__init__(self) win32_fix_title_bar_background(self) self.setWindowIcon(get_icon("python.png")) self.setWindowTitle("Application example") # Instantiate dataset-related widgets: self.groupbox1 = DataSetShowGroupBox( "Activable dataset", ExampleDataSet, comment="" ) self.groupbox2 = DataSetShowGroupBox( "Standard dataset", AnotherDataSet, comment="" ) self.groupbox3 = DataSetEditGroupBox( "Standard dataset", OtherDataSet, comment="" ) self.groupbox4 = DataSetEditGroupBox( "Standard dataset", ExampleMultiGroupDataSet, comment="" ) self.groupbox3.SIG_APPLY_BUTTON_CLICKED.connect(self.update_window) self.update_groupboxes() splitter = QSplitter(self) splitter.addWidget(self.groupbox1) splitter.addWidget(self.groupbox2) splitter.addWidget(self.groupbox3) splitter.addWidget(self.groupbox4) self.setCentralWidget(splitter) self.setContentsMargins(10, 5, 10, 5) # File menu file_menu = self.menuBar().addMenu("File") quit_action = create_action( self, "Quit", shortcut="Ctrl+Q", icon=get_std_icon("DialogCloseButton"), tip="Quit application", triggered=self.close, ) add_actions(file_menu, (quit_action,)) # Edit menu edit_menu = self.menuBar().addMenu("Edit") editparam1_action = create_action( self, "Edit dataset 1", triggered=self.edit_dataset1 ) editparam2_action = create_action( self, "Edit dataset 2", triggered=self.edit_dataset2 ) editparam4_action = create_action( self, "Edit dataset 4", triggered=self.edit_dataset4 ) add_actions( edit_menu, (editparam1_action, editparam2_action, editparam4_action) ) def update_window(self): dataset = self.groupbox3.dataset self.setWindowTitle(dataset.title) self.setWindowIcon(get_icon(dataset.icon)) self.setWindowOpacity(dataset.opacity) def update_groupboxes(self): self.groupbox1.dataset.set_readonly() # This is an activable dataset self.groupbox1.get() self.groupbox2.get() self.groupbox4.get() def edit_dataset1(self): self.groupbox1.dataset.set_writeable() # This is an activable dataset if self.groupbox1.dataset.edit(self): self.update_groupboxes() def edit_dataset2(self): if self.groupbox2.dataset.edit(self): self.update_groupboxes() def edit_dataset4(self): if self.groupbox4.dataset.edit(self): self.update_groupboxes() if __name__ == "__main__": from guidata import qapplication app = qapplication() window = MainWindow() window.show() app.exec_() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628151429.0 guidata-2.0.2/guidata/tests/hdf5.py0000666000000000000000000000257400000000000014014 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ HDF5 I/O demo DataSet objects may be saved in HDF5 files, a universal hierarchical dataset file type. This script shows how to save in and then reload data from a HDF5 file. """ try: import guidata.hdf5io # @UnusedImport hdf5_is_available = True except ImportError: hdf5_is_available = False SHOW = hdf5_is_available # Show test in GUI-based test launcher import os from guidata.hdf5io import HDF5Reader, HDF5Writer from guidata.tests.all_items import TestParameters from guidata.dataset.dataitems import StringItem class TestParameters_Light(TestParameters): date = StringItem("D1", default="Replacement for unsupported DateItem") dtime = StringItem("D2", default="Replacement for unsupported DateTimeItem") if __name__ == "__main__": # Create QApplication import guidata _app = guidata.qapplication() if os.path.exists("test.h5"): os.unlink("test.h5") e = TestParameters() if e.edit(): writer = HDF5Writer("test.h5") e.serialize(writer) writer.close() e = TestParameters() reader = HDF5Reader("test.h5") e.deserialize(reader) reader.close() e.edit() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/tests/inheritance.py0000666000000000000000000000262000000000000015447 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ DataSet objects inheritance test From time to time, it may be useful to derive a DataSet from another. The main application is to extend a parameter set with additionnal parameters. """ SHOW = True # Show test in GUI-based test launcher import guidata.dataset.datatypes as gdt import guidata.dataset.dataitems as gdi class OriginalDataset(gdt.DataSet): """Original dataset This is the original dataset""" bool = gdi.BoolItem("Boolean") string = gdi.StringItem("String") text = gdi.TextItem("Text") float = gdi.FloatItem("Float", default=0.5, min=0, max=1, step=0.01, slider=True) class DerivedDataset(OriginalDataset): """Derived dataset This is the derived dataset""" bool = gdi.BoolItem("Boolean (modified in derived dataset)") a = gdi.FloatItem("Level 1 (added in derived dataset)", default=0) b = gdi.FloatItem("Level 2 (added in derived dataset)", default=0) c = gdi.FloatItem("Level 3 (added in derived dataset)", default=0) if __name__ == "__main__": # Create QApplication import guidata _app = guidata.qapplication() e = OriginalDataset() e.edit() print(e) e = DerivedDataset() e.edit() print(e) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/tests/item_order.py0000666000000000000000000000232500000000000015311 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ DataSet item order test From time to time, it may be useful to change the item order, for example when deriving a dataset from another. """ SHOW = True # Show test in GUI-based test launcher import guidata.dataset.datatypes as gdt import guidata.dataset.dataitems as gdi class OriginalDataset(gdt.DataSet): """Original dataset This is the original dataset""" param1 = gdi.BoolItem("P1 | Boolean") param2 = gdi.StringItem("P2 | String") param3 = gdi.TextItem("P3 | Text") param4 = gdi.FloatItem("P4 | Float", default=0) class DerivedDataset(OriginalDataset): """Derived dataset This is the derived dataset, with modified item order""" param5 = gdi.IntItem("P5 | Int", default=0).set_pos(row=2) param6 = gdi.DateItem("P6 | Date", default=0).set_pos(row=4) if __name__ == "__main__": # Create QApplication import guidata _app = guidata.qapplication() e = OriginalDataset() e.edit() print(e) e = DerivedDataset() e.edit() print(e) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/tests/rotatedlabel.py0000666000000000000000000000240700000000000015623 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ RotatedLabel test RotatedLabel is derived from QLabel: it provides rotated text display. """ SHOW = True # Show test in GUI-based test launcher from qtpy.QtWidgets import QFrame, QGridLayout from qtpy.QtCore import Qt from guidata.qtwidgets import RotatedLabel from guidata.qthelpers import win32_fix_title_bar_background class Frame(QFrame): def __init__(self, parent=None): QFrame.__init__(self, parent) win32_fix_title_bar_background(self) layout = QGridLayout() self.setLayout(layout) angle = 0 for row in range(7): for column in range(7): layout.addWidget( RotatedLabel( "Label %03d°" % angle, angle=angle, color=Qt.blue, bold=True ), row, column, Qt.AlignCenter, ) angle += 10 if __name__ == "__main__": from guidata import qapplication app = qapplication() frame = Frame() frame.show() app.exec_() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629141954.0 guidata-2.0.2/guidata/tests/test_arrayeditor.py0000666000000000000000000000451100000000000016543 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License SHOW = True # Show test in GUI-based test launcher """ Tests for arrayeditor.py """ import numpy as np # Local imports from guidata import qapplication from guidata.widgets.arrayeditor import ArrayEditor def launch_arrayeditor(data, title="", xlabels=None, ylabels=None): """Helper routine to launch an arrayeditor and return its result""" dlg = ArrayEditor() dlg.setup_and_check(data, title, xlabels=xlabels, ylabels=ylabels) dlg.exec_() return dlg.get_value() def test_arrayeditor(): """Test array editor for all supported data types""" for title, data in ( ("string array", np.array(["kjrekrjkejr"])), ("unicode array", np.array([u"ñññéáíó"])), ( "masked array", np.ma.array([[1, 0], [1, 0]], mask=[[True, False], [False, False]]), ), ( "record array", np.zeros( (2, 2), { "names": ("red", "green", "blue"), "formats": (np.float32, np.float32, np.float32), }, ), ), ( "record array with titles", np.array( [(0, 0.0), (0, 0.0), (0, 0.0)], dtype=[(("title 1", "x"), "|i1"), (("title 2", "y"), ">f4")], ), ), ("bool array", np.array([True, False, True])), ("int array", np.array([1, 2, 3], dtype="int8")), ("float16 array", np.zeros((5, 5), dtype=np.float16)), ): launch_arrayeditor(data, title) for title, data, xlabels, ylabels in ( ("float array", np.random.rand(5, 5), ["a", "b", "c", "d", "e"], None), ( "complex array", np.round(np.random.rand(5, 5) * 10) + np.round(np.random.rand(5, 5) * 10) * 1j, np.linspace(-12, 12, 5), np.linspace(-12, 12, 5), ), ): launch_arrayeditor(data, title, xlabels, ylabels) arr = np.zeros((3, 3, 4)) arr[0, 0, 0] = 1 arr[0, 0, 1] = 2 arr[0, 0, 2] = 3 launch_arrayeditor(arr, "3D array") if __name__ == "__main__": app = qapplication() test_arrayeditor() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629142492.0 guidata-2.0.2/guidata/tests/test_codeeditor.py0000666000000000000000000000113700000000000016340 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License SHOW = True # Show test in GUI-based test launcher """ Tests for codeeditor.py """ # Local imports from guidata import qapplication from guidata.widgets import codeeditor def test_codeeditor(): """Test Code editor.""" app = qapplication() widget = codeeditor.PythonCodeEditor() widget.set_text_from_file(codeeditor.__file__) widget.resize(800, 600) widget.show() app.exec_() if __name__ == "__main__": test_codeeditor() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629142426.0 guidata-2.0.2/guidata/tests/test_collectionseditor.py0000666000000000000000000000731600000000000017751 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License SHOW = True # Show test in GUI-based test launcher """ Tests for collectionseditor.py """ import datetime import numpy as np # Local imports from guidata import qapplication from guidata.widgets.collectionseditor import CollectionsEditor def get_test_data(): """Create test data.""" import numpy as np from PIL import Image image = Image.fromarray(np.random.randint(256, size=(100, 100)), mode="P") testdict = {"d": 1, "a": np.random.rand(10, 10), "b": [1, 2]} testdate = datetime.date(1945, 5, 8) test_timedelta = datetime.timedelta(days=-1, minutes=42, seconds=13) try: import pandas as pd except (ModuleNotFoundError, ImportError): test_timestamp = None test_pd_td = None test_dtindex = None test_series = None test_df = None else: test_timestamp = pd.Timestamp("1945-05-08T23:01:00.12345") test_pd_td = pd.Timedelta(days=2193, hours=12) test_dtindex = pd.date_range(start="1939-09-01T", end="1939-10-06", freq="12H") test_series = pd.Series({"series_name": [0, 1, 2, 3, 4, 5]}) test_df = pd.DataFrame( { "string_col": ["a", "b", "c", "d"], "int_col": [0, 1, 2, 3], "float_col": [1.1, 2.2, 3.3, 4.4], "bool_col": [True, False, False, True], } ) class Foobar(object): """ """ def __init__(self): self.text = "toto" self.testdict = testdict self.testdate = testdate foobar = Foobar() return { "object": foobar, "module": np, "str": "kjkj kj k j j kj k jkj", "unicode": "éù", "list": [1, 3, [sorted, 5, 6], "kjkj", None], "tuple": ([1, testdate, testdict, test_timedelta], "kjkj", None), "dict": testdict, "float": 1.2233, "int": 223, "bool": True, "array": np.random.rand(10, 10).astype(np.int64), "masked_array": np.ma.array( [[1, 0], [1, 0]], mask=[[True, False], [False, False]] ), "1D-array": np.linspace(-10, 10).astype(np.float16), "3D-array": np.random.randint(2, size=(5, 5, 5)).astype(np.bool_), "empty_array": np.array([]), "image": image, "date": testdate, "datetime": datetime.datetime(1945, 5, 8, 23, 1, 0, int(1.5e5)), "timedelta": test_timedelta, "complex": 2 + 1j, "complex64": np.complex64(2 + 1j), "complex128": np.complex128(9j), "int8_scalar": np.int8(8), "int16_scalar": np.int16(16), "int32_scalar": np.int32(32), "int64_scalar": np.int64(64), "float16_scalar": np.float16(16), "float32_scalar": np.float32(32), "float64_scalar": np.float64(64), "bool_scalar": bool, "bool__scalar": np.bool_(8), "timestamp": test_timestamp, "timedelta_pd": test_pd_td, "datetimeindex": test_dtindex, "series": test_series, "ddataframe": test_df, "None": None, "unsupported1": np.arccos, "unsupported2": np.cast, # Test for Issue #3518 "big_struct_array": np.zeros( 1000, dtype=[("ID", "f8"), ("param1", "f8", 5000)] ), } def test_collectionseditor(): """Test Collections editor.""" app = qapplication() dialog = CollectionsEditor() dialog.setup(get_test_data()) dialog.show() app.exec_() if __name__ == "__main__": test_collectionseditor() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640619577.0 guidata-2.0.2/guidata/tests/test_console.py0000666000000000000000000000116600000000000015663 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License SHOW = True # Show test in GUI-based test launcher """ Tests for codeeditor.py """ # Local imports from guidata import qapplication from guidata.widgets.console import Console def test_console(): """Test Console widget.""" app = qapplication() widget = Console(debug=False, multithreaded=True) widget.resize(800, 600) widget.show() try: app.exec() except AttributeError: app.exec_() if __name__ == "__main__": test_console() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629143584.0 guidata-2.0.2/guidata/tests/test_dataframeeditor.py0000666000000000000000000000337000000000000017353 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License SHOW = True # Show test in GUI-based test launcher """ Tests for dataframeeditor.py """ import numpy as np from numpy import nan from pandas import DataFrame, Series from pandas.testing import assert_frame_equal, assert_series_equal # Local imports from guidata import qapplication from guidata.widgets.dataframeeditor import DataFrameEditor def test_edit(data, title="", parent=None): """Test subroutine""" app = qapplication() # analysis:ignore dlg = DataFrameEditor(parent=parent) if dlg.setup_and_check(data, title=title): dlg.exec_() return dlg.get_value() else: import sys sys.exit(1) def test_dataframeeditor(): """DataFrame editor test""" df1 = DataFrame( [ [True, "bool"], [1 + 1j, "complex"], ["test", "string"], [1.11, "float"], [1, "int"], [np.random.rand(3, 3), "Unkown type"], ["Large value", 100], ["áéí", "unicode"], ], index=["a", "b", nan, nan, nan, "c", "Test global max", "d"], columns=[nan, "Type"], ) out = test_edit(df1) assert_frame_equal(df1, out) result = Series([True, "bool"], index=[nan, "Type"], name="a") out = test_edit(df1.iloc[0]) assert_series_equal(result, out) df1 = DataFrame(np.random.rand(100100, 10)) out = test_edit(df1) assert_frame_equal(out, df1) series = Series(np.arange(10), name=0) out = test_edit(series) assert_series_equal(series, out) if __name__ == "__main__": test_dataframeeditor() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629143678.0 guidata-2.0.2/guidata/tests/test_importwizard.py0000666000000000000000000000113000000000000016743 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License SHOW = True # Show test in GUI-based test launcher """ Tests for importwizard.py """ # Local imports from guidata import qapplication from guidata.widgets.importwizard import ImportWizard def test(text): """Test""" app = qapplication() # analysis:ignore dialog = ImportWizard(None, text) if dialog.exec_(): print(dialog.get_data()) # spyder: test-skip if __name__ == "__main__": test(u"17/11/1976\t1.34\n14/05/09\t3.14") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629143844.0 guidata-2.0.2/guidata/tests/test_objecteditor.py0000666000000000000000000000315500000000000016676 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License SHOW = True # Show test in GUI-based test launcher """ Tests for objecteditor.py """ import sys import datetime, numpy as np import PIL.Image # Local imports from guidata import qapplication from guidata.widgets.objecteditor import oedit def test(): """Run object editor test""" data = np.random.random_integers(255, size=(100, 100)).astype("uint8") image = PIL.Image.fromarray(data) example = { "str": "kjkj kj k j j kj k jkj", "list": [1, 3, 4, "kjkj", None], "dict": {"d": 1, "a": np.random.rand(10, 10), "b": [1, 2]}, "float": 1.2233, "array": np.random.rand(10, 10), "image": image, "date": datetime.date(1945, 5, 8), "datetime": datetime.datetime(1945, 5, 8), } image = oedit(image) class Foobar(object): """ """ def __init__(self): self.text = "toto" foobar = Foobar() print(oedit(foobar)) # spyder: test-skip print(oedit(example)) # spyder: test-skip print(oedit(np.random.rand(10, 10))) # spyder: test-skip print(oedit(oedit.__doc__)) # spyder: test-skip print(example) # spyder: test-skip if __name__ == "__main__": def catch_exceptions(type, value, traceback): """Méthode custom pour récupérer les exceptions de la boucle Qt.""" system_hook(type, value, traceback) sys.exit(1) system_hook = sys.excepthook sys.excepthook = catch_exceptions test() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/tests/text.py0000666000000000000000000000123700000000000014145 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """Test in text mode""" SHOW = False # Do not show test in GUI-based test launcher from guidata.dataset.datatypes import DataSet from guidata.dataset.dataitems import FloatItem, IntItem class Parameters(DataSet): height = FloatItem("Height", min=1, max=250, help="height in cm") width = FloatItem("Width", min=1, max=250, help="width in cm") number = IntItem("Number", min=3, max=20) if __name__ == "__main__": p = Parameters() p.text_edit() print(p) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/tests/translations.py0000666000000000000000000000066400000000000015705 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2012 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """Little translation test""" SHOW = False # Do not show test in GUI-based test launcher from guidata.config import _ translations = (_("Some required entries are incorrect"),) if __name__ == "__main__": for text in translations: print(text) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/tests/userconfig_app.py0000666000000000000000000000113500000000000016162 0ustar00# -*- coding: utf-8 -*- """ userconfig Application settings example This should create a file named ".app.ini" in your HOME directory containing: [main] version = 1.0.0 [a] b/f = 1.0 """ from guidata.dataset import datatypes as gdt from guidata.dataset import dataitems as gdi from guidata import userconfig class DS(gdt.DataSet): f = gdi.FloatItem("F", 1.0) if __name__ == "__main__": ds = DS("") uc = userconfig.UserConfig({}) uc.set_application("app", "1.0.0") ds.write_config(uc, "a", "b") print("Settings saved in: ", uc.filename()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/userconfig.py0000666000000000000000000003160300000000000014163 0ustar00#!/usr/bin/env python # -*- coding: utf-8 -*- # userconfig License Agreement (MIT License) # ------------------------------------------ # # Copyright © 2009-2012 Pierre Raybaut # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. """ userconfig ---------- The ``guidata.userconfig`` module provides user configuration file (.ini file) management features based on ``ConfigParser`` (standard Python library). It is the exact copy of the open-source package `userconfig` (MIT license). 19/12/2021: Removed Python 2 compatibility """ __version__ = "1.1.0" import os import re import os.path as osp import sys import configparser as cp def _check_values(sections): # Checks if all key/value pairs are writable err = False for section, data in list(sections.items()): for key, value in list(data.items()): try: _s = str(value) except Exception as _e: print("Can't convert:") print(section, key, repr(value)) err = True if err: assert False else: import traceback print("-" * 30) traceback.print_stack() def get_home_dir(): """ Return user home directory """ try: path = osp.expanduser("~") except: path = "" for env_var in ("HOME", "USERPROFILE", "TMP"): if osp.isdir(path): break path = os.environ.get(env_var, "") if path: return path else: raise RuntimeError("Please define environment variable $HOME") def get_config_dir(): if sys.platform == "win32": # TODO: on windows config files usually go in return get_home_dir() return osp.join(get_home_dir(), ".config") class NoDefault: pass class UserConfig(cp.ConfigParser): """ UserConfig class, based on ConfigParser name: name of the config options: dictionnary containing options *or* list of tuples (section_name, options) Note that 'get' and 'set' arguments number and type differ from the overriden methods """ default_section_name = "main" def __init__(self, defaults): cp.ConfigParser.__init__(self) self.name = "none" self.raw = 0 # 0=substitutions are enabled / 1=raw config parser assert isinstance(defaults, dict) for _key, val in list(defaults.items()): assert isinstance(val, dict) if self.default_section_name not in defaults: defaults[self.default_section_name] = {} self.defaults = defaults self.reset_to_defaults(save=False) self.check_default_values() def update_defaults(self, defaults): for key, sectdict in list(defaults.items()): if key not in self.defaults: self.defaults[key] = sectdict else: self.defaults[key].update(sectdict) self.reset_to_defaults(save=False) def save(self): # In any case, the resulting config is saved in config file: self.__save() def set_application(self, name, version, load=True, raw_mode=False): self.name = name self.raw = 1 if raw_mode else 0 if (version is not None) and (re.match("^(\d+).(\d+).(\d+)$", version) is None): raise RuntimeError( "Version number %r is incorrect - must be in X.Y.Z format" % version ) if load: # If config file already exists, it overrides Default options: self.__load() if version != self.get_version(version): # Version has changed -> overwriting .ini file self.reset_to_defaults(save=False) self.__remove_deprecated_options() # Set new version number self.set_version(version, save=False) if self.defaults is None: # If no defaults are defined, set .ini file settings as default self.set_as_defaults() def check_default_values(self): """Check the static options for forbidden data types""" errors = [] def _check(key, value): if value is None: return if isinstance(value, dict): for k, v in list(value.items()): _check(key + "{}", k) _check(key + "/" + k, v) elif isinstance(value, (list, tuple)): for v in value: _check(key + "[]", v) else: if not isinstance(value, (bool, int, float, str)): errors.append("Invalid value for %s: %r" % (key, value)) for name, section in list(self.defaults.items()): assert isinstance(name, str) for key, value in list(section.items()): _check(key, value) if errors: for err in errors: print(err) raise ValueError("Invalid default values") def get_version(self, version="0.0.0"): """Return configuration (not application!) version""" return self.get(self.default_section_name, "version", version) def set_version(self, version="0.0.0", save=True): """Set configuration (not application!) version""" self.set(self.default_section_name, "version", version, save=save) def __load(self): """ Load config from the associated .ini file """ try: self.read(self.filename(), encoding="utf-8") except cp.MissingSectionHeaderError: print("Warning: File contains no section headers.") def __remove_deprecated_options(self): """ Remove options which are present in the .ini file but not in defaults """ for section in self.sections(): for option, _ in self.items(section, raw=self.raw): if self.get_default(section, option) is NoDefault: self.remove_option(section, option) if len(self.items(section, raw=self.raw)) == 0: self.remove_section(section) def __save(self): """ Save config into the associated .ini file """ fname = self.filename() if osp.isfile(fname): os.remove(fname) with open(fname, "w", encoding="utf-8") as configfile: self.write(configfile) def filename(self): """ Create a .ini filename located in user home directory """ return osp.join(get_config_dir(), ".%s.ini" % self.name) def cleanup(self): """ Remove .ini file associated to config """ os.remove(self.filename()) def set_as_defaults(self): """ Set defaults from the current config """ self.defaults = {} for section in self.sections(): secdict = {} for option, value in self.items(section, raw=self.raw): secdict[option] = value self.defaults[section] = secdict def reset_to_defaults(self, save=True, verbose=False): """ Reset config to Default values """ for section, options in list(self.defaults.items()): for option in options: value = options[option] self.__set(section, option, value, verbose) if save: self.__save() def __check_section_option(self, section, option): """ Private method to check section and option types """ if section is None: section = self.default_section_name elif not isinstance(section, str): raise RuntimeError("Argument 'section' must be a string") if not isinstance(option, str): raise RuntimeError("Argument 'option' must be a string") return section def get_default(self, section, option): """ Get Default value for a given (section, option) Useful for type checking in 'get' method """ section = self.__check_section_option(section, option) options = self.defaults.get(section, {}) return options.get(option, NoDefault) def get(self, section, option, default=NoDefault, raw=None, **kwargs): """ Get an option section=None: attribute a default section name default: default value (if not specified, an exception will be raised if option doesn't exist) """ if raw is None: raw = self.raw section = self.__check_section_option(section, option) if not self.has_section(section): if default is NoDefault: raise RuntimeError("Unknown section %r" % section) else: self.add_section(section) if not self.has_option(section, option): if default is NoDefault: raise RuntimeError("Unknown option %r/%r" % (section, option)) else: self.set(section, option, default) return default value = cp.ConfigParser.get(self, section, option, raw=raw) default_value = self.get_default(section, option) if isinstance(default_value, bool): value = eval(value) elif isinstance(default_value, float): value = float(value) elif isinstance(default_value, int): value = int(value) elif isinstance(default_value, str): pass else: try: # lists, tuples, ... value = eval(value) except: pass return value def get_section(self, section): sect = self.defaults.get(section, {}).copy() for opt in self.options(section): sect[opt] = self.get(section, opt) return sect def __set(self, section, option, value, verbose): """ Private set method """ if not self.has_section(section): self.add_section(section) if not isinstance(value, str): value = repr(value) if verbose: print("%s[ %s ] = %s" % (section, option, value)) cp.ConfigParser.set(self, section, option, value) def set_default(self, section, option, default_value): """ Set Default value for a given (section, option) -> called when a new (section, option) is set and no default exists """ section = self.__check_section_option(section, option) options = self.defaults.setdefault(section, {}) options[option] = default_value def set(self, section, option, value, verbose=False, save=True): """ Set an option section=None: attribute a default section name """ section = self.__check_section_option(section, option) default_value = self.get_default(section, option) if default_value is NoDefault: default_value = value self.set_default(section, option, default_value) if isinstance(default_value, bool): value = bool(value) elif isinstance(default_value, float): value = float(value) elif isinstance(default_value, int): value = int(value) elif not isinstance(default_value, str): value = repr(value) self.__set(section, option, value, verbose) if save: self.__save() def remove_section(self, section): cp.ConfigParser.remove_section(self, section) self.__save() def remove_option(self, section, option): cp.ConfigParser.remove_option(self, section, option) self.__save() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640617623.0 guidata-2.0.2/guidata/userconfigio.py0000666000000000000000000001070000000000000014506 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2012 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ Reader and Writer for the serialization of DataSets into .ini files, using the open-source `userconfig` Python package UserConfig reader/writer objects (see guidata.hdf5io for another example of reader/writer) """ import collections.abc import datetime class GroupContext(object): """Group context object""" def __init__(self, handler, group_name): self.handler = handler self.group_name = group_name def __enter__(self): self.handler.begin(self.group_name) def __exit__(self, exc_type, exc_value, traceback): if exc_type is None: self.handler.end(self.group_name) return False class BaseIOHandler(object): """Base I/O Handler with group context manager (see guidata.hdf5io for another example of this handler usage)""" def __init__(self): self.option = [] def group(self, group_name): """Enter a group. This returns a context manager, to be used with the `with` statement""" return GroupContext(self, group_name) def begin(self, section): self.option.append(section) def end(self, section): sect = self.option.pop(-1) assert sect == section, "Error: %s != %s" % (sect, section) class UserConfigIOHandler(BaseIOHandler): def __init__(self, conf, section, option): self.conf = conf self.section = section self.option = [option] def begin(self, section): self.option.append(section) def end(self, section): sect = self.option.pop(-1) assert sect == section def group(self, option): """Enter a HDF5 group. This returns a context manager, to be used with the `with` statement""" return GroupContext(self, option) class WriterMixin(object): def write(self, val, group_name=None): """Write value using the appropriate routine depending on value type group_name: if None, writing the value in current group""" import numpy as np if group_name: self.begin(group_name) if isinstance(val, bool): self.write_bool(val) elif isinstance(val, int): self.write_int(val) elif isinstance(val, float): self.write_float(val) elif isinstance(val, str): self.write_unicode(val) elif isinstance(val, str): self.write_any(val) elif isinstance(val, np.ndarray): self.write_array(val) elif np.isscalar(val): self.write_any(val) elif val is None: self.write_none() elif isinstance(val, (list, tuple)): self.write_sequence(val) elif isinstance(val, datetime.datetime): self.write_float(val.timestamp()) elif isinstance(val, datetime.date): self.write_int(val.toordinal()) elif hasattr(val, "serialize") and isinstance( val.serialize, collections.abc.Callable ): # The object has a DataSet-like `serialize` method val.serialize(self) else: raise NotImplementedError( "cannot serialize %r of type %r" % (val, type(val)) ) if group_name: self.end(group_name) class UserConfigWriter(UserConfigIOHandler, WriterMixin): def write_any(self, val): option = "/".join(self.option) self.conf.set(self.section, option, val) write_bool = write_int = write_float = write_any write_array = write_sequence = write_str = write_any def write_unicode(self, val): self.write_any(val.encode("utf-8")) write_unicode = write_str def write_none(self): self.write_any(None) class UserConfigReader(UserConfigIOHandler): def read_any(self): option = "/".join(self.option) val = self.conf.get(self.section, option) return val read_bool = read_int = read_float = read_any read_array = read_sequence = read_none = read_str = read_any def read_unicode(self): val = self.read_any() if isinstance(val, str) or val is None: return val else: return self.read_str() read_unicode = read_str ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640601896.0 guidata-2.0.2/guidata/utils.py0000666000000000000000000003560200000000000013162 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) # pylint: disable=C0103 """ utils ----- The ``guidata.utils`` module provides various utility helper functions (pure python). """ import sys import time import subprocess import os import os.path as osp import locale # Warning: 2to3 false alarm ('import' fixer) import collections.abc # Local imports from guidata.userconfig import get_home_dir # ============================================================================== # Misc. # ============================================================================== def min_equals_max(min, max): """ Return True if minimium value equals maximum value Return False if not, or if maximum or minimum value is not defined """ return min is not None and max is not None and min == max def pairs(iterable): """A simple generator that takes a list and generates pairs [ (l[0],l[1]), ..., (l[n-2], l[n-1])] """ iterator = iter(iterable) first = next(iterator) while True: second = next(iterator) yield (first, second) first = second def add_extension(item, value): """Add extension to filename `item`: data item representing a file path `value`: possible value for data item""" value = str(value) formats = item.get_prop("data", "formats") if len(formats) == 1 and formats[0] != "*": if not value.endswith("." + formats[0]) and len(value) > 0: return value + "." + formats[0] return value def bind(fct, value): """ Returns a callable representing the function 'fct' with it's first argument bound to the value if g = bind(f,1) and f is a function of x,y,z then g(y,z) will return f(1,y,z) """ def callback(*args, **kwargs): return fct(value, *args, **kwargs) return callback def trace(fct): """A decorator that traces function entry/exit used for debugging only """ from functools import wraps @wraps(fct) def wrapper(*args, **kwargs): """Tracing function entry/exit (debugging only)""" print("enter:", fct.__name__) res = fct(*args, **kwargs) print("leave:", fct.__name__) return res return wrapper # ============================================================================== # Strings # ============================================================================== def decode_fs_string(string): """Convert string from file system charset to unicode""" charset = sys.getfilesystemencoding() if charset is None: charset = locale.getpreferredencoding() return string.decode(charset) def utf8_to_unicode(string): """Convert UTF-8 string to Unicode str""" if not isinstance(string, str): string = str(string) return string # Findout the encoding used for stdout or use ascii as default STDOUT_ENCODING = "ascii" if hasattr(sys.stdout, "encoding"): if sys.stdout.encoding: STDOUT_ENCODING = sys.stdout.encoding def unicode_to_stdout(ustr): """convert a unicode string to a byte string encoded for stdout output""" return ustr.encode(STDOUT_ENCODING, "replace") # ============================================================================== # Updating, restoring datasets # ============================================================================== def update_dataset(dest, source, visible_only=False): """ Update `dest` dataset items from `source` dataset dest should inherit from DataSet, whereas source can be: * any Python object containing matching attribute names * or a dictionary with matching key names For each DataSet item, the function will try to get the attribute of the same name from the source. visible_only: if True, update only visible items """ for item in dest._items: key = item._name if hasattr(source, key): try: hide = item.get_prop_value("display", source, "hide", False) except AttributeError: # FIXME: Remove this try...except hide = False if visible_only and hide: continue setattr(dest, key, getattr(source, key)) elif isinstance(source, dict) and key in source: setattr(dest, key, source[key]) def restore_dataset(source, dest): """ Restore `dest` dataset items from `source` dataset This function is almost the same as update_dataset but requires the source to be a DataSet instead of the destination. Symetrically from update_dataset, `dest` may also be a dictionary. """ for item in source._items: key = item._name value = getattr(source, key) if hasattr(dest, key): try: setattr(dest, key, value) except AttributeError: # This attribute is a property, skipping this iteration continue elif isinstance(dest, dict): dest[key] = value # ============================================================================== # Interface checking # ============================================================================== def assert_interface_supported(klass, iface): """Makes sure a class supports an interface""" for name, func in list(iface.__dict__.items()): if name == "__inherits__": continue if isinstance(func, collections.abc.Callable): assert hasattr(klass, name), "Attribute %s missing from %r" % (name, klass) imp_func = getattr(klass, name) imp_code = imp_func.__code__ code = func.__code__ imp_nargs = imp_code.co_argcount nargs = code.co_argcount if imp_code.co_varnames[:imp_nargs] != code.co_varnames[:nargs]: assert False, "Arguments of %s.%s differ from interface: " "%r!=%r" % ( klass.__name__, imp_func.__name__, imp_code.co_varnames[:imp_nargs], code.co_varnames[:nargs], ) else: pass # should check class attributes for consistency def assert_interfaces_valid(klass): """Makes sure a class supports the interfaces it declares""" assert hasattr(klass, "__implements__"), "Class doesn't implements anything" for iface in klass.__implements__: assert_interface_supported(klass, iface) if hasattr(iface, "__inherits__"): base = iface.__inherits__() assert issubclass(klass, base), "%s should be a subclass of %s" % ( klass, base, ) # ============================================================================== # Date, time, timer # ============================================================================== def localtime_to_isodate(time_struct): """Convert local time to ISO date""" s = time.strftime("%Y-%m-%d %H:%M:%S ", time_struct) s += "%+05d" % time.timezone return s def isodate_to_localtime(datestr): """Convert ISO date to local time""" return time.strptime(datestr[:16], "%Y-%m-%d %H:%M:%S") class FormatTime(object): """Helper object that substitute as a string to format seconds into (nn H mm min ss s)""" def __init__(self, hours_fmt="%d H ", min_fmt="%d min ", sec_fmt="%d s"): self.sec_fmt = sec_fmt self.hours_fmt = hours_fmt self.min_fmt = min_fmt def __mod__(self, val): val = val[0] hours = val // 3600.0 minutes = (val % 3600.0) // 60 seconds = val % 60.0 if hours: return ( (self.hours_fmt % hours) + (self.min_fmt % minutes) + (self.sec_fmt % seconds) ) elif minutes: return (self.min_fmt % minutes) + (self.sec_fmt % seconds) else: return self.sec_fmt % seconds format_hms = FormatTime() class Timer(object): """MATLAB-like timer: tic, toc""" def __init__(self): self.t0_dict = {} def tic(self, cat): """Starting timer""" print(">", cat) self.t0_dict[cat] = time.perf_counter() def toc(self, cat, msg="delta:"): """Stopping timer""" print("<", cat, ":", msg, time.perf_counter() - self.t0_dict[cat]) _TIMER = Timer() tic = _TIMER.tic toc = _TIMER.toc # ============================================================================== # Module, scripts, programs # ============================================================================== def get_module_path(modname): """Return module *modname* base path""" module = sys.modules.get(modname, __import__(modname)) return osp.abspath(osp.dirname(module.__file__)) def is_program_installed(basename): """Return program absolute path if installed in PATH Otherwise, return None""" for path in os.environ["PATH"].split(os.pathsep): abspath = osp.join(path, basename) if osp.isfile(abspath): return abspath def run_program(name, args="", cwd=None, shell=True, wait=False): """Run program in a separate process""" path = is_program_installed(name) if not path: raise RuntimeError("Program %s was not found" % name) command = [path] if args: command.append(args) if wait: subprocess.call(" ".join(command), cwd=cwd, shell=shell) else: subprocess.Popen(" ".join(command), cwd=cwd, shell=shell) class ProgramError(Exception): """Exception raised when a shell command failed to execute.""" pass def alter_subprocess_kwargs_by_platform(**kwargs): """ Given a dict, populate kwargs to create a generally useful default setup for running subprocess processes on different platforms. For example, `close_fds` is set on posix and creation of a new console window is disabled on Windows. This function will alter the given kwargs and return the modified dict. """ kwargs.setdefault("close_fds", os.name == "posix") if os.name == "nt": CONSOLE_CREATION_FLAGS = 0 # Default value # See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863%28v=vs.85%29.aspx CREATE_NO_WINDOW = 0x08000000 # We "or" them together CONSOLE_CREATION_FLAGS |= CREATE_NO_WINDOW kwargs.setdefault("creationflags", CONSOLE_CREATION_FLAGS) return kwargs def run_shell_command(cmdstr, **subprocess_kwargs): """ Execute the given shell command. Note that *args and **kwargs will be passed to the subprocess call. If 'shell' is given in subprocess_kwargs it must be True, otherwise ProgramError will be raised. If 'executable' is not given in subprocess_kwargs, it will be set to the value of the SHELL environment variable. Note that stdin, stdout and stderr will be set by default to PIPE unless specified in subprocess_kwargs. :str cmdstr: The string run as a shell command. :subprocess_kwargs: These will be passed to subprocess.Popen. """ if "shell" in subprocess_kwargs and not subprocess_kwargs["shell"]: raise ProgramError( 'The "shell" kwarg may be omitted, but if ' "provided it must be True." ) else: subprocess_kwargs["shell"] = True if "executable" not in subprocess_kwargs: subprocess_kwargs["executable"] = os.getenv("SHELL") for stream in ["stdin", "stdout", "stderr"]: subprocess_kwargs.setdefault(stream, subprocess.PIPE) subprocess_kwargs = alter_subprocess_kwargs_by_platform(**subprocess_kwargs) return subprocess.Popen(cmdstr, **subprocess_kwargs) def is_module_available(module_name): """Return True if Python module is available""" try: __import__(module_name) return True except ImportError: return False # ============================================================================== # Path utils # ============================================================================== def getcwd_or_home(): """Safe version of getcwd that will fallback to home user dir. This will catch the error raised when the current working directory was removed for an external program. """ try: return os.getcwd() except OSError: print( "WARNING: Current working directory was deleted, " "falling back to home directory" ) return get_home_dir() def remove_backslashes(path): """Remove backslashes in *path* For Windows platforms only. Returns the path unchanged on other platforms. This is especially useful when formatting path strings on Windows platforms for which folder paths may contain backslashes and provoke unicode decoding errors in Python 3 (or in Python 2 when future 'unicode_literals' symbol has been imported).""" if os.name == "nt": # Removing trailing single backslash if path.endswith("\\") and not path.endswith("\\\\"): path = path[:-1] # Replacing backslashes by slashes path = path.replace("\\", "/") path = path.replace("/'", "\\'") return path # ============================================================================== # Utilities for setup.py scripts # ============================================================================== def get_package_data(name, extlist, exclude_dirs=[]): """ Return data files for package *name* with extensions in *extlist* (search recursively in package directories) """ assert isinstance(extlist, (list, tuple)) flist = [] # Workaround to replace os.path.relpath (not available until Python 2.6): offset = len(name) + len(os.pathsep) for dirpath, _dirnames, filenames in os.walk(name): if dirpath not in exclude_dirs: for fname in filenames: if osp.splitext(fname)[1].lower() in extlist: flist.append(osp.join(dirpath, fname)[offset:]) return flist def get_subpackages(name): """Return subpackages of package *name*""" splist = [] for dirpath, _dirnames, _filenames in os.walk(name): if osp.isfile(osp.join(dirpath, "__init__.py")): splist.append(".".join(dirpath.split(os.sep))) return splist def cythonize_all(relpath): """Cythonize all Cython modules in relative path""" from Cython.Compiler import Main for fname in os.listdir(relpath): if osp.splitext(fname)[1] == ".pyx": Main.compile(osp.join(relpath, fname)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640626112.1442733 guidata-2.0.2/guidata/widgets/0000777000000000000000000000000000000000000013110 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/__init__.py0000666000000000000000000000127200000000000015223 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2021 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ widgets ======= The ``guidata.widgets`` package provides useful Qt-based widgets. .. automodule:: guidata.widgets.console :members: .. automodule:: guidata.widgets.codeeditor :members: .. automodule:: guidata.widgets.arrayeditor :members: .. automodule:: guidata.widgets.collectionseditor :members: .. automodule:: guidata.widgets.dataframeeditor :members: .. automodule:: guidata.widgets.texteditor :members: .. automodule:: guidata.widgets.objecteditor :members: """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/arrayeditor.py0000666000000000000000000010100300000000000016002 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """ guidata.widgets.arrayeditor =========================== This package provides a NumPy Array Editor Dialog based on Qt. .. autoclass:: ArrayEditor """ # pylint: disable=C0103 # pylint: disable=R0903 # pylint: disable=R0911 # pylint: disable=R0201 import io import numpy as np from guidata.configtools import get_font, get_icon from guidata.qthelpers import ( add_actions, create_action, keybinding, win32_fix_title_bar_background, ) from guidata.config import CONF, _ from qtpy.QtWidgets import ( QAbstractItemDelegate, QApplication, QCheckBox, QComboBox, QDialog, QGridLayout, QHBoxLayout, QInputDialog, QItemDelegate, QLabel, QLineEdit, QMenu, QMessageBox, QPushButton, QSpinBox, QStackedWidget, QTableView, QVBoxLayout, QWidget, QShortcut, ) from qtpy.QtCore import ( QAbstractTableModel, QLocale, QModelIndex, QItemSelection, QItemSelectionRange, Qt, Slot, ) from qtpy.QtGui import ( QColor, QCursor, QDoubleValidator, QKeySequence, ) # Note: string and unicode data types will be formatted with '%s' (see below) SUPPORTED_FORMATS = { "single": "%.6g", "double": "%.6g", "float_": "%.6g", "longfloat": "%.6g", "float16": "%.6g", "float32": "%.6g", "float64": "%.6g", "float96": "%.6g", "float128": "%.6g", "csingle": "%r", "complex_": "%r", "clongfloat": "%r", "complex64": "%r", "complex128": "%r", "complex192": "%r", "complex256": "%r", "byte": "%d", "bytes8": "%s", "short": "%d", "intc": "%d", "int_": "%d", "longlong": "%d", "intp": "%d", "int8": "%d", "int16": "%d", "int32": "%d", "int64": "%d", "ubyte": "%d", "ushort": "%d", "uintc": "%d", "uint": "%d", "ulonglong": "%d", "uintp": "%d", "uint8": "%d", "uint16": "%d", "uint32": "%d", "uint64": "%d", "bool_": "%r", "bool8": "%r", "bool": "%r", } LARGE_SIZE = 5e5 LARGE_NROWS = 1e5 LARGE_COLS = 60 # ============================================================================== # Utility functions # ============================================================================== def is_float(dtype): """Return True if datatype dtype is a float kind""" return ("float" in dtype.name) or dtype.name in ["single", "double"] def is_number(dtype): """Return True is datatype dtype is a number kind""" return ( is_float(dtype) or ("int" in dtype.name) or ("long" in dtype.name) or ("short" in dtype.name) ) def get_idx_rect(index_list): """Extract the boundaries from a list of indexes""" rows, cols = list(zip(*[(i.row(), i.column()) for i in index_list])) return (min(rows), max(rows), min(cols), max(cols)) # ============================================================================== # Main classes # ============================================================================== class ArrayModel(QAbstractTableModel): """Array Editor Table Model""" ROWS_TO_LOAD = 500 COLS_TO_LOAD = 40 def __init__( self, data, format="%.6g", xlabels=None, ylabels=None, readonly=False, parent=None, ): QAbstractTableModel.__init__(self) self.dialog = parent self.changes = {} self.xlabels = xlabels self.ylabels = ylabels self.readonly = readonly self.test_array = np.array([0], dtype=data.dtype) # for complex numbers, shading will be based on absolute value # but for all other types it will be the real part if data.dtype in (np.complex64, np.complex128): self.color_func = np.abs else: self.color_func = np.real # Backgroundcolor settings huerange = [0.66, 0.99] # Hue self.sat = 0.7 # Saturation self.val = 1.0 # Value self.alp = 0.6 # Alpha-channel self._data = data self._format = format self.total_rows = self._data.shape[0] self.total_cols = self._data.shape[1] size = self.total_rows * self.total_cols try: self.vmin = np.nanmin(self.color_func(data)) self.vmax = np.nanmax(self.color_func(data)) if self.vmax == self.vmin: self.vmin -= 1 self.hue0 = huerange[0] self.dhue = huerange[1] - huerange[0] self.bgcolor_enabled = True except (TypeError, ValueError): self.vmin = None self.vmax = None self.hue0 = None self.dhue = None self.bgcolor_enabled = False # Use paging when the total size, number of rows or number of # columns is too large if size > LARGE_SIZE: self.rows_loaded = self.ROWS_TO_LOAD self.cols_loaded = self.COLS_TO_LOAD else: if self.total_rows > LARGE_NROWS: self.rows_loaded = self.ROWS_TO_LOAD else: self.rows_loaded = self.total_rows if self.total_cols > LARGE_COLS: self.cols_loaded = self.COLS_TO_LOAD else: self.cols_loaded = self.total_cols def get_format(self): """Return current format""" # Avoid accessing the private attribute _format from outside return self._format def get_data(self): """Return data""" return self._data def set_format(self, format): """Change display format""" self._format = format self.reset() def columnCount(self, qindex=QModelIndex()): """Array column number""" if self.total_cols <= self.cols_loaded: return self.total_cols else: return self.cols_loaded def rowCount(self, qindex=QModelIndex()): """Array row number""" if self.total_rows <= self.rows_loaded: return self.total_rows else: return self.rows_loaded def can_fetch_more(self, rows=False, columns=False): """ :param rows: :param columns: :return: """ if rows: if self.total_rows > self.rows_loaded: return True else: return False if columns: if self.total_cols > self.cols_loaded: return True else: return False def fetch_more(self, rows=False, columns=False): """ :param rows: :param columns: """ if self.can_fetch_more(rows=rows): reminder = self.total_rows - self.rows_loaded items_to_fetch = min(reminder, self.ROWS_TO_LOAD) self.beginInsertRows( QModelIndex(), self.rows_loaded, self.rows_loaded + items_to_fetch - 1 ) self.rows_loaded += items_to_fetch self.endInsertRows() if self.can_fetch_more(columns=columns): reminder = self.total_cols - self.cols_loaded items_to_fetch = min(reminder, self.COLS_TO_LOAD) self.beginInsertColumns( QModelIndex(), self.cols_loaded, self.cols_loaded + items_to_fetch - 1 ) self.cols_loaded += items_to_fetch self.endInsertColumns() def bgcolor(self, state): """Toggle backgroundcolor""" self.bgcolor_enabled = state > 0 self.reset() def get_value(self, index): """ :param index: :return: """ i = index.row() j = index.column() if len(self._data.shape) == 1: value = self._data[j] else: value = self._data[i, j] return self.changes.get((i, j), value) def data(self, index, role=Qt.DisplayRole): """Cell content""" if not index.isValid(): return None value = self.get_value(index) if isinstance(value, bytes): try: value = str(value, "utf8") except: pass if role == Qt.DisplayRole: if value is np.ma.masked: return "" else: try: return self._format % value except TypeError: self.readonly = True return repr(value) elif role == Qt.TextAlignmentRole: return int(Qt.AlignCenter | Qt.AlignVCenter) elif ( role == Qt.BackgroundColorRole and self.bgcolor_enabled and value is not np.ma.masked ): try: hue = self.hue0 + self.dhue * ( float(self.vmax) - self.color_func(value) ) / (float(self.vmax) - self.vmin) hue = float(np.abs(hue)) color = QColor.fromHsvF(hue, self.sat, self.val, self.alp) return color except TypeError: return None elif role == Qt.FontRole: return get_font(CONF, "arrayeditor", "font") return None def setData(self, index, value, role=Qt.EditRole): """Cell content change""" if not index.isValid() or self.readonly: return False i = index.row() j = index.column() dtype = self._data.dtype.name if dtype == "bool": try: val = bool(float(value)) except ValueError: val = value.lower() == "true" elif dtype.startswith("string") or dtype.startswith("bytes"): val = bytes(value, "utf8") elif dtype.startswith("unicode") or dtype.startswith("str"): val = str(value) else: if value.lower().startswith("e") or value.lower().endswith("e"): return False try: val = complex(value) if not val.imag: val = val.real except ValueError as e: QMessageBox.critical(self.dialog, "Error", "Value error: %s" % str(e)) return False try: self.test_array[0] = val # will raise an Exception eventually except OverflowError as e: print("OverflowError: " + str(e)) # spyder: test-skip QMessageBox.critical(self.dialog, "Error", "Overflow error: %s" % str(e)) return False # Add change to self.changes self.changes[(i, j)] = val self.dataChanged.emit(index, index) if not isinstance(val, (str, bytes)): if val > self.vmax: self.vmax = val if val < self.vmin: self.vmin = val return True def flags(self, index): """Set editable flag""" if not index.isValid(): return Qt.ItemIsEnabled return Qt.ItemFlags(QAbstractTableModel.flags(self, index) | Qt.ItemIsEditable) def headerData(self, section, orientation, role=Qt.DisplayRole): """Set header data""" if role != Qt.DisplayRole: return None labels = self.xlabels if orientation == Qt.Horizontal else self.ylabels if labels is None: return int(section) else: return labels[section] def reset(self): """ """ self.beginResetModel() self.endResetModel() class ArrayDelegate(QItemDelegate): """Array Editor Item Delegate""" def __init__(self, dtype, parent=None): QItemDelegate.__init__(self, parent) self.dtype = dtype def createEditor(self, parent, option, index): """Create editor widget""" model = index.model() value = model.get_value(index) if model._data.dtype.name == "bool": value = not value model.setData(index, value) return elif value is not np.ma.masked: editor = QLineEdit(parent) editor.setFont(get_font(CONF, "arrayeditor", "font")) editor.setAlignment(Qt.AlignCenter) if is_number(self.dtype): validator = QDoubleValidator(editor) validator.setLocale(QLocale("C")) editor.setValidator(validator) editor.returnPressed.connect(self.commitAndCloseEditor) return editor def commitAndCloseEditor(self): """Commit and close editor""" editor = self.sender() # Avoid a segfault with PyQt5. Variable value won't be changed # but at least Spyder won't crash. It seems generated by a bug in sip. try: self.commitData.emit(editor) except AttributeError: pass self.closeEditor.emit(editor, QAbstractItemDelegate.NoHint) def setEditorData(self, editor, index): """Set editor widget's data""" text = index.model().data(index, Qt.DisplayRole) editor.setText(text) # TODO: Implement "Paste" (from clipboard) feature class ArrayView(QTableView): """Array view class""" def __init__(self, parent, model, dtype, shape): QTableView.__init__(self, parent) self.setModel(model) self.setItemDelegate(ArrayDelegate(dtype, self)) total_width = 0 for k in range(shape[1]): total_width += self.columnWidth(k) self.viewport().resize(min(total_width, 1024), self.height()) self.shape = shape self.menu = self.setup_menu() QShortcut(QKeySequence(QKeySequence.Copy), self, self.copy) self.horizontalScrollBar().valueChanged.connect( lambda val: self.load_more_data(val, columns=True) ) self.verticalScrollBar().valueChanged.connect( lambda val: self.load_more_data(val, rows=True) ) def load_more_data(self, value, rows=False, columns=False): """ :param value: :param rows: :param columns: """ old_selection = self.selectionModel().selection() old_rows_loaded = old_cols_loaded = None if rows and value == self.verticalScrollBar().maximum(): old_rows_loaded = self.model().rows_loaded self.model().fetch_more(rows=rows) if columns and value == self.horizontalScrollBar().maximum(): old_cols_loaded = self.model().cols_loaded self.model().fetch_more(columns=columns) if old_rows_loaded is not None or old_cols_loaded is not None: # if we've changed anything, update selection new_selection = QItemSelection() for part in old_selection: top = part.top() bottom = part.bottom() if ( old_rows_loaded is not None and top == 0 and bottom == (old_rows_loaded - 1) ): # complete column selected (so expand it to match updated range) bottom = self.model().rows_loaded - 1 left = part.left() right = part.right() if ( old_cols_loaded is not None and left == 0 and right == (old_cols_loaded - 1) ): # compete row selected (so expand it to match updated range) right = self.model().cols_loaded - 1 top_left = self.model().index(top, left) bottom_right = self.model().index(bottom, right) part = QItemSelectionRange(top_left, bottom_right) new_selection.append(part) self.selectionModel().select( new_selection, self.selectionModel().ClearAndSelect ) def resize_to_contents(self): """Resize cells to contents""" QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) self.resizeColumnsToContents() self.model().fetch_more(columns=True) self.resizeColumnsToContents() QApplication.restoreOverrideCursor() def setup_menu(self): """Setup context menu""" self.copy_action = create_action( self, _("Copy"), shortcut=keybinding("Copy"), icon=get_icon("editcopy.png"), triggered=self.copy, context=Qt.WidgetShortcut, ) menu = QMenu(self) add_actions(menu, [self.copy_action]) return menu def contextMenuEvent(self, event): """Reimplement Qt method""" self.menu.popup(event.globalPos()) event.accept() def keyPressEvent(self, event): """Reimplement Qt method""" if event == QKeySequence.Copy: self.copy() else: QTableView.keyPressEvent(self, event) def _sel_to_text(self, cell_range): """Copy an array portion to a unicode string""" if not cell_range: return row_min, row_max, col_min, col_max = get_idx_rect(cell_range) if col_min == 0 and col_max == (self.model().cols_loaded - 1): # we've selected a whole column. It isn't possible to # select only the first part of a column without loading more, # so we can treat it as intentional and copy the whole thing col_max = self.model().total_cols - 1 if row_min == 0 and row_max == (self.model().rows_loaded - 1): row_max = self.model().total_rows - 1 _data = self.model().get_data() output = io.BytesIO() try: np.savetxt( output, _data[row_min : row_max + 1, col_min : col_max + 1], delimiter="\t", fmt=self.model().get_format(), ) except: QMessageBox.warning( self, _("Warning"), _("It was not possible to copy values for " "this array"), ) return contents = output.getvalue().decode("utf-8") output.close() return contents @Slot() def copy(self): """Copy text to clipboard""" cliptxt = self._sel_to_text(self.selectedIndexes()) clipboard = QApplication.clipboard() clipboard.setText(cliptxt) class ArrayEditorWidget(QWidget): """ """ def __init__(self, parent, data, readonly=False, xlabels=None, ylabels=None): QWidget.__init__(self, parent) self.data = data self.old_data_shape = None if len(self.data.shape) == 1: self.old_data_shape = self.data.shape self.data.shape = (self.data.shape[0], 1) elif len(self.data.shape) == 0: self.old_data_shape = self.data.shape self.data.shape = (1, 1) format = SUPPORTED_FORMATS.get(data.dtype.name, "%s") self.model = ArrayModel( self.data, format=format, xlabels=xlabels, ylabels=ylabels, readonly=readonly, parent=self, ) self.view = ArrayView(self, self.model, data.dtype, data.shape) btn_layout = QHBoxLayout() btn_layout.setAlignment(Qt.AlignLeft) btn = QPushButton(_("Format")) # disable format button for int type btn.setEnabled(is_float(data.dtype)) btn_layout.addWidget(btn) btn.clicked.connect(self.change_format) btn = QPushButton(_("Resize")) btn_layout.addWidget(btn) btn.clicked.connect(self.view.resize_to_contents) bgcolor = QCheckBox(_("Background color")) bgcolor.setChecked(self.model.bgcolor_enabled) bgcolor.setEnabled(self.model.bgcolor_enabled) bgcolor.stateChanged.connect(self.model.bgcolor) btn_layout.addWidget(bgcolor) layout = QVBoxLayout() layout.addWidget(self.view) layout.addLayout(btn_layout) self.setLayout(layout) def accept_changes(self): """Accept changes""" for (i, j), value in list(self.model.changes.items()): self.data[i, j] = value if self.old_data_shape is not None: self.data.shape = self.old_data_shape def reject_changes(self): """Reject changes""" if self.old_data_shape is not None: self.data.shape = self.old_data_shape def change_format(self): """Change display format""" format, valid = QInputDialog.getText( self, _("Format"), _("Float formatting"), QLineEdit.Normal, self.model.get_format(), ) if valid: format = str(format) try: format % 1.1 except: QMessageBox.critical( self, _("Error"), _("Format (%s) is incorrect") % format ) return self.model.set_format(format) class ArrayEditor(QDialog): """Array Editor Dialog""" def __init__(self, parent=None): QDialog.__init__(self, parent) win32_fix_title_bar_background(self) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.data = None self.arraywidget = None self.stack = None self.layout = None self.btn_save_and_close = None self.btn_close = None # Values for 3d array editor self.dim_indexes = [{}, {}, {}] self.last_dim = 0 # Adjust this for changing the startup dimension def setup_and_check( self, data, title="", readonly=False, xlabels=None, ylabels=None ): """ Setup ArrayEditor: return False if data is not supported, True otherwise """ self.data = data self.data.flags.writeable = True is_record_array = data.dtype.names is not None is_masked_array = isinstance(data, np.ma.MaskedArray) if data.ndim > 3: self.error(_("Arrays with more than 3 dimensions are not " "supported")) return False if xlabels is not None and len(xlabels) != self.data.shape[1]: self.error( _("The 'xlabels' argument length do no match array " "column number") ) return False if ylabels is not None and len(ylabels) != self.data.shape[0]: self.error( _("The 'ylabels' argument length do no match array row " "number") ) return False if not is_record_array: dtn = data.dtype.name if ( dtn not in SUPPORTED_FORMATS and not dtn.startswith("str") and not dtn.startswith("unicode") ): arr = _("%s arrays") % data.dtype.name self.error(_("%s are currently not supported") % arr) return False self.layout = QGridLayout() self.setLayout(self.layout) self.setWindowIcon(get_icon("arredit.png")) if title: title = str(title) + " - " + _("NumPy array") else: title = _("Array editor") if readonly: title += " (" + _("read only") + ")" self.setWindowTitle(title) self.resize(600, 500) # Stack widget self.stack = QStackedWidget(self) if is_record_array: for name in data.dtype.names: self.stack.addWidget( ArrayEditorWidget(self, data[name], readonly, xlabels, ylabels) ) elif is_masked_array: self.stack.addWidget( ArrayEditorWidget(self, data, readonly, xlabels, ylabels) ) self.stack.addWidget( ArrayEditorWidget(self, data.data, readonly, xlabels, ylabels) ) self.stack.addWidget( ArrayEditorWidget(self, data.mask, readonly, xlabels, ylabels) ) elif data.ndim == 3: pass else: self.stack.addWidget( ArrayEditorWidget(self, data, readonly, xlabels, ylabels) ) self.arraywidget = self.stack.currentWidget() if self.arraywidget: self.arraywidget.model.dataChanged.connect(self.save_and_close_enable) self.stack.currentChanged.connect(self.current_widget_changed) self.layout.addWidget(self.stack, 1, 0) # Buttons configuration btn_layout = QHBoxLayout() if is_record_array or is_masked_array or data.ndim == 3: if is_record_array: btn_layout.addWidget(QLabel(_("Record array fields:"))) names = [] for name in data.dtype.names: field = data.dtype.fields[name] text = name if len(field) >= 3: title = field[2] if not isinstance(title, str): title = repr(title) text += " - " + title names.append(text) else: names = [_("Masked data"), _("Data"), _("Mask")] if data.ndim == 3: # QSpinBox self.index_spin = QSpinBox(self, keyboardTracking=False) self.index_spin.valueChanged.connect(self.change_active_widget) # QComboBox names = [str(i) for i in range(3)] ra_combo = QComboBox(self) ra_combo.addItems(names) ra_combo.currentIndexChanged.connect(self.current_dim_changed) # Adding the widgets to layout label = QLabel(_("Axis:")) btn_layout.addWidget(label) btn_layout.addWidget(ra_combo) self.shape_label = QLabel() btn_layout.addWidget(self.shape_label) label = QLabel(_("Index:")) btn_layout.addWidget(label) btn_layout.addWidget(self.index_spin) self.slicing_label = QLabel() btn_layout.addWidget(self.slicing_label) # set the widget to display when launched self.current_dim_changed(self.last_dim) else: ra_combo = QComboBox(self) ra_combo.currentIndexChanged.connect(self.stack.setCurrentIndex) ra_combo.addItems(names) btn_layout.addWidget(ra_combo) if is_masked_array: label = QLabel(_("Warning: changes are applied separately")) label.setToolTip( _( "For performance reasons, changes applied " "to masked array won't be reflected in " "array's data (and vice-versa)." ) ) btn_layout.addWidget(label) btn_layout.addStretch() if not readonly: self.btn_save_and_close = QPushButton(_("Save and Close")) self.btn_save_and_close.setDisabled(True) self.btn_save_and_close.clicked.connect(self.accept) btn_layout.addWidget(self.btn_save_and_close) self.btn_close = QPushButton(_("Close")) self.btn_close.setAutoDefault(True) self.btn_close.setDefault(True) self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) self.layout.addLayout(btn_layout, 2, 0) self.setMinimumSize(400, 300) # Make the dialog act as a window self.setWindowFlags(Qt.Window) return True @Slot(QModelIndex, QModelIndex) def save_and_close_enable(self, left_top, bottom_right): """Handle the data change event to enable the save and close button.""" if self.btn_save_and_close: self.btn_save_and_close.setEnabled(True) self.btn_save_and_close.setAutoDefault(True) self.btn_save_and_close.setDefault(True) def current_widget_changed(self, index): """ :param index: """ self.arraywidget = self.stack.widget(index) self.arraywidget.model.dataChanged.connect(self.save_and_close_enable) def change_active_widget(self, index): """ This is implemented for handling negative values in index for 3d arrays, to give the same behavior as slicing """ string_index = [":"] * 3 string_index[self.last_dim] = "%i" self.slicing_label.setText( (r"Slicing: [" + ", ".join(string_index) + "]") % index ) if index < 0: data_index = self.data.shape[self.last_dim] + index else: data_index = index slice_index = [slice(None)] * 3 slice_index[self.last_dim] = data_index stack_index = self.dim_indexes[self.last_dim].get(data_index) if stack_index is None: stack_index = self.stack.count() try: self.stack.addWidget( ArrayEditorWidget(self, self.data[tuple(slice_index)]) ) except IndexError: # Handle arrays of size 0 in one axis self.stack.addWidget(ArrayEditorWidget(self, self.data)) self.dim_indexes[self.last_dim][data_index] = stack_index self.stack.update() self.stack.setCurrentIndex(stack_index) def current_dim_changed(self, index): """ This change the active axis the array editor is plotting over in 3D """ self.last_dim = index string_size = ["%i"] * 3 string_size[index] = "%i" self.shape_label.setText( ("Shape: (" + ", ".join(string_size) + ") ") % self.data.shape ) if self.index_spin.value() != 0: self.index_spin.setValue(0) else: # this is done since if the value is currently 0 it does not emit # currentIndexChanged(int) self.change_active_widget(0) self.index_spin.setRange(-self.data.shape[index], self.data.shape[index] - 1) @Slot() def accept(self): """Reimplement Qt method""" for index in range(self.stack.count()): self.stack.widget(index).accept_changes() QDialog.accept(self) def get_value(self): """Return modified array -- this is *not* a copy""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.data def error(self, message): """An error occured, closing the dialog box""" QMessageBox.critical(self, _("Array editor"), message) self.setAttribute(Qt.WA_DeleteOnClose) self.reject() @Slot() def reject(self): """Reimplement Qt method""" if self.arraywidget is not None: for index in range(self.stack.count()): self.stack.widget(index).reject_changes() QDialog.reject(self) def launch_arrayeditor(data, title="", xlabels=None, ylabels=None): """Helper routine to launch an arrayeditor and return its result""" dlg = ArrayEditor() assert dlg.setup_and_check(data, title, xlabels=xlabels, ylabels=ylabels) dlg.exec_() # dlg.accept() # trigger slot connected to OK button return dlg.get_value() if __name__ == "__main__": from guidata import qapplication app = qapplication() from numpy.testing import assert_array_equal arr = np.zeros((5, 5), dtype=np.float16) assert_array_equal(arr, launch_arrayeditor(arr, "float16 array")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/codeeditor.py0000666000000000000000000003140500000000000015606 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """ guidata.widgets.codeeditor ========================== This package provides an Editor widget based on QtGui.QPlainTextEdit. .. autoclass:: PythonCodeEditor """ # %% This line is for cell execution testing # pylint: disable=C0103 # pylint: disable=R0903 # pylint: disable=R0911 # pylint: disable=R0201 from qtpy.QtWidgets import QWidget, QPlainTextEdit from qtpy.QtGui import QColor, QPainter from qtpy.QtCore import QRect, QSize, Qt from guidata.external import darkdetect import guidata.widgets.syntaxhighlighters as sh from guidata.configtools import get_font from guidata import encoding from guidata.qthelpers import win32_fix_title_bar_background from guidata.config import CONF, _ class LineNumberArea(QWidget): """Line number area (on the left side of the text editor widget)""" def __init__(self, editor): QWidget.__init__(self, editor) self.code_editor = editor self.setMouseTracking(True) def sizeHint(self): """Override Qt method""" return QSize(self.code_editor.compute_linenumberarea_width(), 0) def paintEvent(self, event): """Override Qt method""" self.code_editor.linenumberarea_paint_event(event) def mouseMoveEvent(self, event): """Override Qt method""" self.code_editor.linenumberarea_mousemove_event(event) def mouseDoubleClickEvent(self, event): """Override Qt method""" self.code_editor.linenumberarea_mousedoubleclick_event(event) def mousePressEvent(self, event): """Override Qt method""" self.code_editor.linenumberarea_mousepress_event(event) def mouseReleaseEvent(self, event): """Override Qt method""" self.code_editor.linenumberarea_mouserelease_event(event) def wheelEvent(self, event): """Override Qt method""" self.code_editor.wheelEvent(event) class PythonCodeEditor(QPlainTextEdit): # To have these attrs when early viewportEvent's are triggered linenumberarea = None def __init__(self, parent=None, font=None, columns=None, rows=None): QPlainTextEdit.__init__(self, parent) win32_fix_title_bar_background(self) self.visible_blocks = [] self.highlighter = None self.normal_color = None self.sideareas_color = None self.linenumbers_color = QColor( Qt.lightGray if darkdetect.isDark() else Qt.darkGray ) # Line number area management self.linenumbers_margin = True self.linenumberarea_enabled = None self.linenumberarea_pressed = None self.linenumberarea_released = None self.setFocusPolicy(Qt.StrongFocus) self.setup(font=font, columns=columns, rows=rows) def setup(self, font=None, columns=None, rows=None): """Setup widget""" if font is None: font = get_font(CONF, "codeeditor") self.setFont(font) self.setup_linenumberarea() self.highlighter = sh.PythonSH(self.document(), self.font()) self.highlighter.rehighlight() self.normal_color = self.highlighter.get_foreground_color() self.sideareas_color = self.highlighter.get_sideareas_color() if columns is not None: self.set_minimum_width(columns) if rows is not None: self.set_minimum_height(rows) def set_minimum_width(self, columns): """Set widget minimum width to show the specified number of columns""" width = self.fontMetrics().width("9" * (columns + 8)) self.setMinimumWidth(width) def set_minimum_height(self, rows): """Set widget minimum height to show the specified number of rows""" height = self.fontMetrics().height() * (rows + 1) self.setMinimumHeight(height) def setup_linenumberarea(self): """Setup widget""" self.linenumberarea = LineNumberArea(self) self.blockCountChanged.connect(self.update_linenumberarea_width) self.updateRequest.connect(self.update_linenumberarea) self.linenumberarea_pressed = -1 self.linenumberarea_released = -1 self.set_linenumberarea_enabled(True) self.update_linenumberarea_width() def set_text_from_file(self, filename): """Set the text of the editor from file *fname*""" text, _enc = encoding.read(filename) self.setPlainText(text) # -----linenumberarea def set_linenumberarea_enabled(self, state): """ :param state: """ self.linenumberarea_enabled = state self.linenumberarea.setVisible(state) self.update_linenumberarea_width() def get_linenumberarea_width(self): """Return current line number area width""" return self.linenumberarea.contentsRect().width() def compute_linenumberarea_width(self): """Compute and return line number area width""" if not self.linenumberarea_enabled: return 0 digits = 1 maxb = max(1, self.blockCount()) while maxb >= 10: maxb /= 10 digits += 1 if self.linenumbers_margin: linenumbers_margin = 3 + self.fontMetrics().width("9" * digits) else: linenumbers_margin = 0 return linenumbers_margin def update_linenumberarea_width(self, new_block_count=None): """ Update line number area width. new_block_count is needed to handle blockCountChanged(int) signal """ self.setViewportMargins(self.compute_linenumberarea_width(), 0, 0, 0) def update_linenumberarea(self, qrect, dy): """Update line number area""" if dy: self.linenumberarea.scroll(0, dy) else: self.linenumberarea.update( 0, qrect.y(), self.linenumberarea.width(), qrect.height() ) if qrect.contains(self.viewport().rect()): self.update_linenumberarea_width() def linenumberarea_paint_event(self, event): """Painting line number area""" painter = QPainter(self.linenumberarea) painter.fillRect(event.rect(), self.sideareas_color) # This is needed to make that the font size of line numbers # be the same as the text one when zooming # See Issues 2296 and 4811 font = self.font() font_height = self.fontMetrics().height() active_block = self.textCursor().block() active_line_number = active_block.blockNumber() + 1 def draw_pixmap(ytop, pixmap): """ :param ytop: :param pixmap: """ pixmap_height = pixmap.height() / pixmap.devicePixelRatio() painter.drawPixmap(0, ytop + (font_height - pixmap_height) / 2, pixmap) for top, line_number, block in self.visible_blocks: if self.linenumbers_margin: if line_number == active_line_number: font.setWeight(font.Bold) painter.setFont(font) painter.setPen(self.normal_color) else: font.setWeight(font.Normal) painter.setFont(font) painter.setPen(self.linenumbers_color) painter.drawText( 0, top, self.linenumberarea.width(), font_height, Qt.AlignRight | Qt.AlignBottom, str(line_number), ) def __get_linenumber_from_mouse_event(self, event): """Return line number from mouse event""" block = self.firstVisibleBlock() line_number = block.blockNumber() top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top() bottom = top + self.blockBoundingRect(block).height() while block.isValid() and top < event.pos().y(): block = block.next() top = bottom bottom = top + self.blockBoundingRect(block).height() line_number += 1 return line_number def linenumberarea_mousemove_event(self, event): """Handling line number area mouse move event""" line_number = self.__get_linenumber_from_mouse_event(event) block = self.document().findBlockByNumber(line_number - 1) data = block.userData() # this disables pyflakes messages if there is an active drag/selection # operation check = self.linenumberarea_released == -1 if data and data.code_analysis and check: self.__show_code_analysis_results(line_number, data.code_analysis) if event.buttons() == Qt.LeftButton: self.linenumberarea_released = line_number self.linenumberarea_select_lines( self.linenumberarea_pressed, self.linenumberarea_released ) def linenumberarea_mousedoubleclick_event(self, event): """Handling line number area mouse double-click event""" line_number = self.__get_linenumber_from_mouse_event(event) shift = event.modifiers() & Qt.ShiftModifier def linenumberarea_mousepress_event(self, event): """Handling line number area mouse double press event""" line_number = self.__get_linenumber_from_mouse_event(event) self.linenumberarea_pressed = line_number self.linenumberarea_released = line_number self.linenumberarea_select_lines( self.linenumberarea_pressed, self.linenumberarea_released ) def linenumberarea_mouserelease_event(self, event): """Handling line number area mouse release event""" self.linenumberarea_released = -1 self.linenumberarea_pressed = -1 def linenumberarea_select_lines(self, linenumber_pressed, linenumber_released): """Select line(s) after a mouse press/mouse press drag event""" find_block_by_line_number = self.document().findBlockByLineNumber move_n_blocks = linenumber_released - linenumber_pressed start_line = linenumber_pressed start_block = find_block_by_line_number(start_line - 1) cursor = self.textCursor() cursor.setPosition(start_block.position()) # Select/drag downwards if move_n_blocks > 0: for n in range(abs(move_n_blocks) + 1): cursor.movePosition(cursor.NextBlock, cursor.KeepAnchor) # Select/drag upwards or select single line else: cursor.movePosition(cursor.NextBlock) for n in range(abs(move_n_blocks) + 1): cursor.movePosition(cursor.PreviousBlock, cursor.KeepAnchor) # Account for last line case if linenumber_released == self.blockCount(): cursor.movePosition(cursor.EndOfBlock, cursor.KeepAnchor) else: cursor.movePosition(cursor.StartOfBlock, cursor.KeepAnchor) self.setTextCursor(cursor) def resizeEvent(self, event): """Reimplemented Qt method to handle line number area resizing""" QPlainTextEdit.resizeEvent(self, event) cr = self.contentsRect() self.linenumberarea.setGeometry( QRect(cr.left(), cr.top(), self.compute_linenumberarea_width(), cr.height()) ) def paintEvent(self, event): """Overrides paint event to update the list of visible blocks""" self.update_visible_blocks(event) QPlainTextEdit.paintEvent(self, event) def update_visible_blocks(self, event): """Update the list of visible blocks/lines position""" self.visible_blocks[:] = [] block = self.firstVisibleBlock() blockNumber = block.blockNumber() top = int( self.blockBoundingGeometry(block).translated(self.contentOffset()).top() ) bottom = top + int(self.blockBoundingRect(block).height()) ebottom_bottom = self.height() while block.isValid(): visible = bottom <= ebottom_bottom if not visible: break if block.isVisible(): self.visible_blocks.append((top, blockNumber + 1, block)) block = block.next() top = bottom bottom = top + int(self.blockBoundingRect(block).height()) blockNumber = block.blockNumber() if __name__ == "__main__": from guidata import qapplication app = qapplication() widget = PythonCodeEditor(columns=80, rows=40) widget.set_text_from_file(__file__) widget.show() app.exec_() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/collectionseditor.py0000666000000000000000000015443500000000000017223 0ustar00# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright © Spyder Project Contributors # # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) # ---------------------------------------------------------------------------- """ guidata.widgets.collectionseditor ================================= This package provides a Collections (i.e. dictionary, list and tuple) editor widget and dialog. .. autoclass:: CollectionsEditor """ # TODO: Multiple selection: open as many editors (array/dict/...) as necessary, # at the same time # pylint: disable=C0103 # pylint: disable=R0903 # pylint: disable=R0911 # pylint: disable=R0201 import datetime import io import re import sys import warnings import PIL.Image from guidata.utils import getcwd_or_home from guidata.configtools import get_font, get_icon from guidata.qthelpers import ( add_actions, create_action, mimedata2url, win32_fix_title_bar_background, ) from guidata.config import CONF, _ from qtpy.QtWidgets import ( QAbstractItemDelegate, QApplication, QDateEdit, QDateTimeEdit, QDialog, QHBoxLayout, QInputDialog, QItemDelegate, QLineEdit, QMenu, QMessageBox, QPushButton, QTableView, QVBoxLayout, QWidget, ) from qtpy.QtCore import ( QAbstractTableModel, QModelIndex, Qt, Signal, Slot, QDateTime, ) from qtpy.QtGui import ( QColor, QKeySequence, ) from guidata.widgets.importwizard import ImportWizard from guidata.widgets.nsview import ( DataFrame, DatetimeIndex, FakeObject, Image, MaskedArray, Series, array, display_to_value, get_color_name, get_human_readable_type, get_object_attrs, get_size, get_type_string, is_editable_type, is_known_type, ndarray, np_savetxt, sort_against, try_to_eval, unsorted_unique, value_to_display, ) from guidata.widgets.texteditor import TextEditor if ndarray is not FakeObject: from guidata.widgets.arrayeditor import ArrayEditor if DataFrame is not FakeObject: from guidata.widgets.dataframeeditor import DataFrameEditor LARGE_NROWS = 100 ROWS_TO_LOAD = 50 def fix_reference_name(name, blacklist=None): """Return a syntax-valid Python reference name from an arbitrary name""" name = "".join(re.split(r"[^0-9a-zA-Z_]", name)) while name and not re.match(r"([a-zA-Z]+[0-9a-zA-Z_]*)$", name): if not re.match(r"[a-zA-Z]", name[0]): name = name[1:] continue name = str(name) if not name: name = "data" if blacklist is not None and name in blacklist: get_new_name = lambda index: name + ("%03d" % index) index = 0 while get_new_name(index) in blacklist: index += 1 name = get_new_name(index) return name class ProxyObject(object): """Dictionary proxy to an unknown object.""" def __init__(self, obj): """Constructor.""" self.__obj__ = obj def __len__(self): """Get len according to detected attributes.""" return len(get_object_attrs(self.__obj__)) def __getitem__(self, key): """Get the attribute corresponding to the given key.""" # Catch NotImplementedError to fix #6284 in pandas MultiIndex # due to NA checking not being supported on a multiindex. # Catch AttributeError to fix #5642 in certain special classes like xml # when this method is called on certain attributes. # Catch TypeError to prevent fatal Python crash to desktop after # modifying certain pandas objects. Fix issue #6727 . # Catch ValueError to allow viewing and editing of pandas offsets. # Fix issue #6728 . try: attribute_toreturn = getattr(self.__obj__, key) except (NotImplementedError, AttributeError, TypeError, ValueError): attribute_toreturn = None return attribute_toreturn def __setitem__(self, key, value): """Set attribute corresponding to key with value.""" # Catch AttributeError to gracefully handle inability to set an # attribute due to it not being writeable or set-table. # Fix issue #6728 . Also, catch NotImplementedError for safety. try: setattr(self.__obj__, key, value) except (TypeError, AttributeError, NotImplementedError): pass except Exception as e: if "cannot set values for" not in str(e): raise class ReadOnlyCollectionsModel(QAbstractTableModel): """CollectionsEditor Read-Only Table Model""" sig_setting_data = Signal() def __init__( self, parent, data, title="", names=False, minmax=False, dataframe_format=None ): QAbstractTableModel.__init__(self, parent) if data is None: data = {} self.names = names self.minmax = minmax self.dataframe_format = dataframe_format self.header0 = None self._data = None self.total_rows = None self.showndata = None self.keys = None self.title = str(title) # in case title is not a string if self.title: self.title = self.title + " - " self.sizes = [] self.types = [] self.set_data(data) def get_data(self): """Return model data""" return self._data def set_data(self, data, coll_filter=None): """Set model data""" self._data = data data_type = get_type_string(data) if coll_filter is not None and isinstance(data, (tuple, list, dict)): data = coll_filter(data) self.showndata = data self.header0 = _("Index") if self.names: self.header0 = _("Name") if isinstance(data, tuple): self.keys = list(range(len(data))) self.title += _("Tuple") elif isinstance(data, list): self.keys = list(range(len(data))) self.title += _("List") elif isinstance(data, dict): self.keys = list(data.keys()) self.title += _("Dictionary") if not self.names: self.header0 = _("Key") else: self.keys = get_object_attrs(data) self._data = data = self.showndata = ProxyObject(data) if not self.names: self.header0 = _("Attribute") if not isinstance(self._data, ProxyObject): self.title += " (" + str(len(self.keys)) + " " + _("elements") + ")" else: self.title += data_type self.total_rows = len(self.keys) if self.total_rows > LARGE_NROWS: self.rows_loaded = ROWS_TO_LOAD else: self.rows_loaded = self.total_rows self.sig_setting_data.emit() self.set_size_and_type() self.reset() def set_size_and_type(self, start=None, stop=None): """ :param start: :param stop: """ data = self._data if start is None and stop is None: start = 0 stop = self.rows_loaded fetch_more = False else: fetch_more = True # Ignore pandas warnings that certain attributes are deprecated # and will be removed, since they will only be accessed if they exist. with warnings.catch_warnings(): warnings.filterwarnings( "ignore", message=( r"^\w+\.\w+ is deprecated and " "will be removed in a future version" ), ) sizes = [get_size(data[self.keys[index]]) for index in range(start, stop)] types = [ get_human_readable_type(data[self.keys[index]]) for index in range(start, stop) ] if fetch_more: self.sizes = self.sizes + sizes self.types = self.types + types else: self.sizes = sizes self.types = types def sort(self, column, order=Qt.AscendingOrder): """Overriding sort method""" reverse = order == Qt.DescendingOrder if column == 0: self.sizes = sort_against(self.sizes, self.keys, reverse) self.types = sort_against(self.types, self.keys, reverse) try: self.keys.sort(reverse=reverse) except: pass elif column == 1: self.keys[: self.rows_loaded] = sort_against(self.keys, self.types, reverse) self.sizes = sort_against(self.sizes, self.types, reverse) try: self.types.sort(reverse=reverse) except: pass elif column == 2: self.keys[: self.rows_loaded] = sort_against(self.keys, self.sizes, reverse) self.types = sort_against(self.types, self.sizes, reverse) try: self.sizes.sort(reverse=reverse) except: pass elif column == 3: values = [self._data[key] for key in self.keys] self.keys = sort_against(self.keys, values, reverse) self.sizes = sort_against(self.sizes, values, reverse) self.types = sort_against(self.types, values, reverse) self.beginResetModel() self.endResetModel() def columnCount(self, qindex=QModelIndex()): """Array column number""" return 4 def rowCount(self, index=QModelIndex()): """Array row number""" if self.total_rows <= self.rows_loaded: return self.total_rows else: return self.rows_loaded def canFetchMore(self, index=QModelIndex()): """ :param index: :return: """ if self.total_rows > self.rows_loaded: return True else: return False def fetchMore(self, index=QModelIndex()): """ :param index: """ reminder = self.total_rows - self.rows_loaded items_to_fetch = min(reminder, ROWS_TO_LOAD) self.set_size_and_type(self.rows_loaded, self.rows_loaded + items_to_fetch) self.beginInsertRows( QModelIndex(), self.rows_loaded, self.rows_loaded + items_to_fetch - 1 ) self.rows_loaded += items_to_fetch self.endInsertRows() def get_index_from_key(self, key): """ :param key: :return: """ try: return self.createIndex(self.keys.index(key), 0) except (RuntimeError, ValueError): return QModelIndex() def get_key(self, index): """Return current key""" return self.keys[index.row()] def get_value(self, index): """Return current value""" if index.column() == 0: return self.keys[index.row()] elif index.column() == 1: return self.types[index.row()] elif index.column() == 2: return self.sizes[index.row()] else: return self._data[self.keys[index.row()]] def get_bgcolor(self, index): """Background color depending on value""" if index.column() == 0: color = QColor(Qt.lightGray) color.setAlphaF(0.05) elif index.column() < 3: color = QColor(Qt.lightGray) color.setAlphaF(0.2) else: color = QColor(Qt.lightGray) color.setAlphaF(0.3) return color def data(self, index, role=Qt.DisplayRole): """Cell content""" if not index.isValid(): return None value = self.get_value(index) if index.column() == 3: display = value_to_display(value, minmax=self.minmax) else: display = str(value) if role == Qt.DisplayRole: return display elif role == Qt.EditRole: return value_to_display(value) elif role == Qt.TextAlignmentRole: if index.column() == 3: if len(display.splitlines()) < 3: return int(Qt.AlignLeft | Qt.AlignVCenter) else: return int(Qt.AlignLeft | Qt.AlignTop) else: return int(Qt.AlignLeft | Qt.AlignVCenter) elif role == Qt.BackgroundColorRole: return self.get_bgcolor(index) elif role == Qt.FontRole: return get_font(CONF, "dicteditor", "font") return None def headerData(self, section, orientation, role=Qt.DisplayRole): """Overriding method headerData""" if role != Qt.DisplayRole: return None i_column = int(section) if orientation == Qt.Horizontal: headers = (self.header0, _("Type"), _("Size"), _("Value")) return headers[i_column] else: return None def flags(self, index): """Overriding method flags""" # This method was implemented in CollectionsModel only, but to enable # tuple exploration (even without editing), this method was moved here if not index.isValid(): return Qt.ItemIsEnabled return Qt.ItemFlags(QAbstractTableModel.flags(self, index) | Qt.ItemIsEditable) def reset(self): """ """ self.beginResetModel() self.endResetModel() class CollectionsModel(ReadOnlyCollectionsModel): """Collections Table Model""" def set_value(self, index, value): """Set value""" self._data[self.keys[index.row()]] = value self.showndata[self.keys[index.row()]] = value self.sizes[index.row()] = get_size(value) self.types[index.row()] = get_human_readable_type(value) self.sig_setting_data.emit() def get_bgcolor(self, index): """Background color depending on value""" value = self.get_value(index) if index.column() < 3: color = ReadOnlyCollectionsModel.get_bgcolor(self, index) else: color_name = get_color_name(value) color = QColor(color_name) color.setAlphaF(0.2) return color def setData(self, index, value, role=Qt.EditRole): """Cell content change""" if not index.isValid(): return False if index.column() < 3: return False value = display_to_value(value, self.get_value(index), ignore_errors=True) self.set_value(index, value) self.dataChanged.emit(index, index) return True class CollectionsDelegate(QItemDelegate): """CollectionsEditor Item Delegate""" sig_free_memory = Signal() def __init__(self, parent=None): QItemDelegate.__init__(self, parent) self._editors = {} # keep references on opened editors def get_value(self, index): """ :param index: :return: """ if index.isValid(): return index.model().get_value(index) def set_value(self, index, value): """ :param index: :param value: """ if index.isValid(): index.model().set_value(index, value) def show_warning(self, index): """ Decide if showing a warning when the user is trying to view a big variable associated to a Tablemodel index This avoids getting the variables' value to know its size and type, using instead those already computed by the TableModel. The problem is when a variable is too big, it can take a lot of time just to get its value """ try: val_size = index.model().sizes[index.row()] val_type = index.model().types[index.row()] except: return False if val_type in ["list", "tuple", "dict"] and int(val_size) > 1e5: return True else: return False def createEditor(self, parent, option, index): """Overriding method createEditor""" if index.column() < 3: return None if self.show_warning(index): answer = QMessageBox.warning( self.parent(), _("Warning"), _( "Opening this variable can be slow\n\n" "Do you want to continue anyway?" ), QMessageBox.Yes | QMessageBox.No, ) if answer == QMessageBox.No: return None try: value = self.get_value(index) if value is None: return None except Exception as msg: QMessageBox.critical( self.parent(), _("Error"), _( "Spyder was unable to retrieve the value of " "this variable from the console.

" "The error mesage was:
" "%s" ) % str(msg), ) return key = index.model().get_key(index) readonly = ( isinstance(value, tuple) or self.parent().readonly or not is_known_type(value) ) # CollectionsEditor for a list, tuple, dict, etc. if isinstance(value, (list, tuple, dict)): editor = CollectionsEditor(parent=parent) editor.setup(value, key, icon=self.parent().windowIcon(), readonly=readonly) self.create_dialog( editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly), ) return None # ArrayEditor for a Numpy array elif isinstance(value, (ndarray, MaskedArray)) and ndarray is not FakeObject: editor = ArrayEditor(parent=parent) if not editor.setup_and_check(value, title=key, readonly=readonly): return self.create_dialog( editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly), ) return None # ArrayEditor for an images elif ( isinstance(value, Image) and ndarray is not FakeObject and Image is not FakeObject ): arr = array(value) editor = ArrayEditor(parent=parent) if not editor.setup_and_check(arr, title=key, readonly=readonly): return conv_func = lambda arr: PIL.Image.fromarray(arr, mode=value.mode) self.create_dialog( editor, dict( model=index.model(), editor=editor, key=key, readonly=readonly, conv=conv_func, ), ) return None # DataFrameEditor for a pandas dataframe, series or index elif ( isinstance(value, (DataFrame, DatetimeIndex, Series)) and DataFrame is not FakeObject ): editor = DataFrameEditor(parent=parent) if not editor.setup_and_check(value, title=key): return editor.dataModel.set_format(index.model().dataframe_format) editor.sig_option_changed.connect(self.change_option) self.create_dialog( editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly), ) return None # QDateEdit and QDateTimeEdit for a dates or datetime respectively elif isinstance(value, datetime.date): if readonly: return None else: if isinstance(value, datetime.datetime): editor = QDateTimeEdit(value, parent=parent) else: editor = QDateEdit(value, parent=parent) editor.setCalendarPopup(True) editor.setFont(get_font(CONF, "dicteditor", "font")) return editor # TextEditor for a long string elif isinstance(value, str) and len(value) > 40: te = TextEditor(None, parent=parent) if te.setup_and_check(value): editor = TextEditor(value, key, readonly=readonly, parent=parent) self.create_dialog( editor, dict( model=index.model(), editor=editor, key=key, readonly=readonly ), ) return None # QLineEdit for an individual value (int, float, short string, etc) elif is_editable_type(value): if readonly: return None else: editor = QLineEdit(parent=parent) editor.setFont(get_font(CONF, "dicteditor", "font")) editor.setAlignment(Qt.AlignLeft) # This is making Spyder crash because the QLineEdit that it's # been modified is removed and a new one is created after # evaluation. So the object on which this method is trying to # act doesn't exist anymore. # editor.returnPressed.connect(self.commitAndCloseEditor) return editor # CollectionsEditor for an arbitrary Python object else: editor = CollectionsEditor(parent=parent) editor.setup(value, key, icon=self.parent().windowIcon(), readonly=readonly) self.create_dialog( editor, dict(model=index.model(), editor=editor, key=key, readonly=readonly), ) return None def create_dialog(self, editor, data): """ :param editor: :param data: """ self._editors[id(editor)] = data editor.accepted.connect(lambda eid=id(editor): self.editor_accepted(eid)) editor.rejected.connect(lambda eid=id(editor): self.editor_rejected(eid)) editor.show() @Slot(str, object) def change_option(self, option_name, new_value): """ Change configuration option. This function is called when a `sig_option_changed` signal is received. At the moment, this signal can only come from a DataFrameEditor. """ if option_name == "dataframe_format": self.parent().set_dataframe_format(new_value) def editor_accepted(self, editor_id): """ :param editor_id: """ data = self._editors[editor_id] if not data["readonly"]: index = data["model"].get_index_from_key(data["key"]) value = data["editor"].get_value() conv_func = data.get("conv", lambda v: v) self.set_value(index, conv_func(value)) self._editors.pop(editor_id) self.free_memory() def editor_rejected(self, editor_id): """ :param editor_id: """ self._editors.pop(editor_id) self.free_memory() def free_memory(self): """Free memory after closing an editor.""" try: self.sig_free_memory.emit() except RuntimeError: pass def commitAndCloseEditor(self): """Overriding method commitAndCloseEditor""" editor = self.sender() # Avoid a segfault with PyQt5. Variable value won't be changed # but at least Spyder won't crash. It seems generated by a bug in sip. try: self.commitData.emit(editor) except AttributeError: pass self.closeEditor.emit(editor, QAbstractItemDelegate.NoHint) def setEditorData(self, editor, index): """ Overriding method setEditorData Model --> Editor """ value = self.get_value(index) if isinstance(editor, QLineEdit): if isinstance(value, bytes): try: value = str(value, "utf8") except: pass if not isinstance(value, str): value = repr(value) editor.setText(value) elif isinstance(editor, QDateEdit): editor.setDate(value) elif isinstance(editor, QDateTimeEdit): editor.setDateTime(QDateTime(value.date(), value.time())) def setModelData(self, editor, model, index): """ Overriding method setModelData Editor --> Model """ if not hasattr(model, "set_value"): # Read-only mode return if isinstance(editor, QLineEdit): value = editor.text() try: value = display_to_value( value, self.get_value(index), ignore_errors=False ) except Exception as msg: raise QMessageBox.critical( editor, _("Edit item"), _( "Unable to assign data to item." "

Error message:
%s" ) % str(msg), ) return elif isinstance(editor, QDateEdit): qdate = editor.date() value = datetime.date(qdate.year(), qdate.month(), qdate.day()) elif isinstance(editor, QDateTimeEdit): qdatetime = editor.dateTime() qdate = qdatetime.date() qtime = qdatetime.time() value = datetime.datetime( qdate.year(), qdate.month(), qdate.day(), qtime.hour(), qtime.minute(), qtime.second(), ) else: # Should not happen... raise RuntimeError("Unsupported editor widget") self.set_value(index, value) class BaseTableView(QTableView): """Base collection editor table view""" sig_option_changed = Signal(str, object) sig_files_dropped = Signal(list) redirect_stdio = Signal(bool) sig_free_memory = Signal() def __init__(self, parent): QTableView.__init__(self, parent) self.array_filename = None self.menu = None self.empty_ws_menu = None self.paste_action = None self.copy_action = None self.edit_action = None self.plot_action = None self.hist_action = None self.imshow_action = None self.save_array_action = None self.insert_action = None self.remove_action = None self.minmax_action = None self.rename_action = None self.duplicate_action = None self.delegate = None self.setAcceptDrops(True) def setup_table(self): """Setup table""" self.horizontalHeader().setStretchLastSection(True) self.adjust_columns() # Sorting columns self.setSortingEnabled(True) self.sortByColumn(0, Qt.AscendingOrder) def setup_menu(self, minmax): """Setup context menu""" if self.minmax_action is not None: self.minmax_action.setChecked(minmax) return resize_action = create_action( self, _("Resize rows to contents"), triggered=self.resizeRowsToContents ) self.paste_action = create_action( self, _("Paste"), icon=get_icon("editpaste.png"), triggered=self.paste ) self.copy_action = create_action( self, _("Copy"), icon=get_icon("editcopy.png"), triggered=self.copy ) self.edit_action = create_action( self, _("Edit"), icon=get_icon("edit.png"), triggered=self.edit_item ) self.plot_action = create_action( self, _("Plot"), icon=get_icon("plot.png"), triggered=lambda: self.plot_item("plot"), ) self.plot_action.setVisible(False) self.hist_action = create_action( self, _("Histogram"), icon=get_icon("hist.png"), triggered=lambda: self.plot_item("hist"), ) self.hist_action.setVisible(False) self.imshow_action = create_action( self, _("Show image"), icon=get_icon("imshow.png"), triggered=self.imshow_item, ) self.imshow_action.setVisible(False) self.save_array_action = create_action( self, _("Save array"), icon=get_icon("filesave.png"), triggered=self.save_array, ) self.save_array_action.setVisible(False) self.insert_action = create_action( self, _("Insert"), icon=get_icon("insert.png"), triggered=self.insert_item ) self.remove_action = create_action( self, _("Remove"), icon=get_icon("editdelete.png"), triggered=self.remove_item, ) self.minmax_action = create_action( self, _("Show arrays min/max"), toggled=self.toggle_minmax ) self.minmax_action.setChecked(minmax) self.toggle_minmax(minmax) self.rename_action = create_action( self, _("Rename"), icon=get_icon("rename.png"), triggered=self.rename_item ) self.duplicate_action = create_action( self, _("Duplicate"), icon=get_icon("edit_add.png"), triggered=self.duplicate_item, ) menu = QMenu(self) menu_actions = [ self.edit_action, self.plot_action, self.hist_action, self.imshow_action, self.save_array_action, self.insert_action, self.remove_action, self.copy_action, self.paste_action, None, self.rename_action, self.duplicate_action, None, resize_action, ] if ndarray is not FakeObject: menu_actions.append(self.minmax_action) add_actions(menu, menu_actions) self.empty_ws_menu = QMenu(self) add_actions( self.empty_ws_menu, [self.insert_action, self.paste_action, None, resize_action], ) return menu # ------ Remote/local API --------------------------------------------------- def remove_values(self, keys): """Remove values from data""" raise NotImplementedError def copy_value(self, orig_key, new_key): """Copy value""" raise NotImplementedError def new_value(self, key, value): """Create new value in data""" raise NotImplementedError def is_list(self, key): """Return True if variable is a list or a tuple""" raise NotImplementedError def get_len(self, key): """Return sequence length""" raise NotImplementedError def is_array(self, key): """Return True if variable is a numpy array""" raise NotImplementedError def is_image(self, key): """Return True if variable is a PIL.Image image""" raise NotImplementedError def is_dict(self, key): """Return True if variable is a dictionary""" raise NotImplementedError def get_array_shape(self, key): """Return array's shape""" raise NotImplementedError def get_array_ndim(self, key): """Return array's ndim""" raise NotImplementedError def oedit(self, key): """Edit item""" raise NotImplementedError def plot(self, key, funcname): """Plot item""" raise NotImplementedError def imshow(self, key): """Show item's image""" raise NotImplementedError def show_image(self, key): """Show image (item is a PIL image)""" raise NotImplementedError # --------------------------------------------------------------------------- def refresh_menu(self): """Refresh context menu""" index = self.currentIndex() condition = index.isValid() self.edit_action.setEnabled(condition) self.remove_action.setEnabled(condition) self.refresh_plot_entries(index) def refresh_plot_entries(self, index): """ :param index: """ if index.isValid(): key = self.model.get_key(index) is_list = self.is_list(key) is_array = self.is_array(key) and self.get_len(key) != 0 condition_plot = is_array and len(self.get_array_shape(key)) <= 2 condition_hist = is_array and self.get_array_ndim(key) == 1 condition_imshow = condition_plot and self.get_array_ndim(key) == 2 condition_imshow = condition_imshow or self.is_image(key) else: is_array = ( condition_plot ) = condition_imshow = is_list = condition_hist = False self.plot_action.setVisible(condition_plot or is_list) self.hist_action.setVisible(condition_hist or is_list) self.imshow_action.setVisible(condition_imshow) self.save_array_action.setVisible(is_array) def adjust_columns(self): """Resize two first columns to contents""" for col in range(3): self.resizeColumnToContents(col) def set_data(self, data): """Set table data""" if data is not None: self.model.set_data(data, self.dictfilter) self.sortByColumn(0, Qt.AscendingOrder) def mousePressEvent(self, event): """Reimplement Qt method""" if event.button() != Qt.LeftButton: QTableView.mousePressEvent(self, event) return index_clicked = self.indexAt(event.pos()) if index_clicked.isValid(): if ( index_clicked == self.currentIndex() and index_clicked in self.selectedIndexes() ): self.clearSelection() else: QTableView.mousePressEvent(self, event) else: self.clearSelection() event.accept() def mouseDoubleClickEvent(self, event): """Reimplement Qt method""" index_clicked = self.indexAt(event.pos()) if index_clicked.isValid(): row = index_clicked.row() # TODO: Remove hard coded "Value" column number (3 here) index_clicked = index_clicked.child(row, 3) self.edit(index_clicked) else: event.accept() def keyPressEvent(self, event): """Reimplement Qt methods""" if event.key() == Qt.Key_Delete: self.remove_item() elif event.key() == Qt.Key_F2: self.rename_item() elif event == QKeySequence.Copy: self.copy() elif event == QKeySequence.Paste: self.paste() else: QTableView.keyPressEvent(self, event) def contextMenuEvent(self, event): """Reimplement Qt method""" if self.model.showndata: self.refresh_menu() self.menu.popup(event.globalPos()) event.accept() else: self.empty_ws_menu.popup(event.globalPos()) event.accept() def dragEnterEvent(self, event): """Allow user to drag files""" if mimedata2url(event.mimeData()): event.accept() else: event.ignore() def dragMoveEvent(self, event): """Allow user to move files""" if mimedata2url(event.mimeData()): event.setDropAction(Qt.CopyAction) event.accept() else: event.ignore() def dropEvent(self, event): """Allow user to drop supported files""" urls = mimedata2url(event.mimeData()) if urls: event.setDropAction(Qt.CopyAction) event.accept() self.sig_files_dropped.emit(urls) else: event.ignore() @Slot(bool) def toggle_minmax(self, state): """Toggle min/max display for numpy arrays""" self.sig_option_changed.emit("minmax", state) self.model.minmax = state @Slot(str) def set_dataframe_format(self, new_format): """ Set format to use in DataframeEditor. Args: new_format (string): e.g. "%.3f" """ self.sig_option_changed.emit("dataframe_format", new_format) self.model.dataframe_format = new_format @Slot() def edit_item(self): """Edit item""" index = self.currentIndex() if not index.isValid(): return # TODO: Remove hard coded "Value" column number (3 here) self.edit(index.child(index.row(), 3)) @Slot() def remove_item(self): """Remove item""" indexes = self.selectedIndexes() if not indexes: return for index in indexes: if not index.isValid(): return one = _("Do you want to remove the selected item?") more = _("Do you want to remove all selected items?") answer = QMessageBox.question( self, _("Remove"), one if len(indexes) == 1 else more, QMessageBox.Yes | QMessageBox.No, ) if answer == QMessageBox.Yes: idx_rows = unsorted_unique([idx.row() for idx in indexes]) keys = [self.model.keys[idx_row] for idx_row in idx_rows] self.remove_values(keys) def copy_item(self, erase_original=False): """Copy item""" indexes = self.selectedIndexes() if not indexes: return idx_rows = unsorted_unique([idx.row() for idx in indexes]) if len(idx_rows) > 1 or not indexes[0].isValid(): return orig_key = self.model.keys[idx_rows[0]] if erase_original: title = _("Rename") field_text = _("New variable name:") else: title = _("Duplicate") field_text = _("Variable name:") data = self.model.get_data() if isinstance(data, (list, set)): new_key, valid = len(data), True else: new_key, valid = QInputDialog.getText( self, title, field_text, QLineEdit.Normal, orig_key ) if valid and str(new_key): new_key = try_to_eval(str(new_key)) if new_key == orig_key: return self.copy_value(orig_key, new_key) if erase_original: self.remove_values([orig_key]) @Slot() def duplicate_item(self): """Duplicate item""" self.copy_item() @Slot() def rename_item(self): """Rename item""" self.copy_item(True) @Slot() def insert_item(self): """Insert item""" index = self.currentIndex() if not index.isValid(): row = self.model.rowCount() else: row = index.row() data = self.model.get_data() if isinstance(data, list): key = row data.insert(row, "") elif isinstance(data, dict): key, valid = QInputDialog.getText( self, _("Insert"), _("Key:"), QLineEdit.Normal ) if valid and str(key): key = try_to_eval(str(key)) else: return else: return value, valid = QInputDialog.getText( self, _("Insert"), _("Value:"), QLineEdit.Normal ) if valid and str(value): self.new_value(key, try_to_eval(str(value))) def __prepare_plot(self): try: import guiqwt.pyplot # analysis:ignore return True except: try: if "matplotlib" not in sys.modules: import matplotlib matplotlib.use("Qt5Agg") return True except: QMessageBox.warning( self, _("Import error"), _("Please install matplotlib" " or guiqwt."), ) def plot_item(self, funcname): """Plot item""" index = self.currentIndex() if self.__prepare_plot(): key = self.model.get_key(index) try: self.plot(key, funcname) except (ValueError, TypeError) as error: QMessageBox.critical( self, _("Plot"), _("Unable to plot data." "

Error message:
%s") % str(error), ) @Slot() def imshow_item(self): """Imshow item""" index = self.currentIndex() if self.__prepare_plot(): key = self.model.get_key(index) try: if self.is_image(key): self.show_image(key) else: self.imshow(key) except (ValueError, TypeError) as error: QMessageBox.critical( self, _("Plot"), _("Unable to show image." "

Error message:
%s") % str(error), ) @Slot() def save_array(self): """Save array""" title = _("Save array") if self.array_filename is None: self.array_filename = getcwd_or_home() self.redirect_stdio.emit(False) filename, _selfilter = get_save_filename( self, title, self.array_filename, _("NumPy arrays") + " (*.npy)" ) self.redirect_stdio.emit(True) if filename: self.array_filename = filename data = self.delegate.get_value(self.currentIndex()) try: import numpy as np np.save(self.array_filename, data) except Exception as error: QMessageBox.critical( self, title, _("Unable to save array" "

Error message:
%s") % str(error), ) @Slot() def copy(self): """Copy text to clipboard""" clipboard = QApplication.clipboard() clipl = [] for idx in self.selectedIndexes(): if not idx.isValid(): continue obj = self.delegate.get_value(idx) # Check if we are trying to copy a numpy array, and if so make sure # to copy the whole thing in a tab separated format if isinstance(obj, (ndarray, MaskedArray)) and ndarray is not FakeObject: output = io.BytesIO() try: np_savetxt(output, obj, delimiter="\t") except: QMessageBox.warning( self, _("Warning"), _("It was not possible to copy " "this array"), ) return obj = output.getvalue().decode("utf-8") output.close() elif isinstance(obj, (DataFrame, Series)) and DataFrame is not FakeObject: output = io.StringIO() try: obj.to_csv(output, sep="\t", index=True, header=True) except Exception: QMessageBox.warning( self, _("Warning"), _("It was not possible to copy " "this dataframe"), ) return obj = output.getvalue() output.close() elif isinstance(obj, bytes): obj = str(obj, "utf8") else: obj = str(obj) clipl.append(obj) clipboard.setText("\n".join(clipl)) def import_from_string(self, text, title=None): """Import data from string""" data = self.model.get_data() # Check if data is a dict if not hasattr(data, "keys"): return editor = ImportWizard( self, text, title=title, contents_title=_("Clipboard contents"), varname=fix_reference_name("data", blacklist=list(data.keys())), ) if editor.exec_(): var_name, clip_data = editor.get_data() self.new_value(var_name, clip_data) @Slot() def paste(self): """Import text/data/code from clipboard""" clipboard = QApplication.clipboard() cliptext = "" if clipboard.mimeData().hasText(): cliptext = str(clipboard.text()) if cliptext.strip(): self.import_from_string(cliptext, title=_("Import from clipboard")) else: QMessageBox.warning( self, _("Empty clipboard"), _("Nothing to be imported from clipboard.") ) class CollectionsEditorTableView(BaseTableView): """CollectionsEditor table view""" def __init__( self, parent, data, readonly=False, title="", names=False, minmax=False ): BaseTableView.__init__(self, parent) self.dictfilter = None self.readonly = readonly or isinstance(data, tuple) CollectionsModelClass = ( ReadOnlyCollectionsModel if self.readonly else CollectionsModel ) self.model = CollectionsModelClass( self, data, title, names=names, minmax=minmax ) self.setModel(self.model) self.delegate = CollectionsDelegate(self) self.setItemDelegate(self.delegate) self.setup_table() self.menu = self.setup_menu(minmax) # ------ Remote/local API --------------------------------------------------- def remove_values(self, keys): """Remove values from data""" data = self.model.get_data() for key in sorted(keys, reverse=True): data.pop(key) self.set_data(data) def copy_value(self, orig_key, new_key): """Copy value""" data = self.model.get_data() if isinstance(data, list): data.append(data[orig_key]) if isinstance(data, set): data.add(data[orig_key]) else: data[new_key] = data[orig_key] self.set_data(data) def new_value(self, key, value): """Create new value in data""" data = self.model.get_data() data[key] = value self.set_data(data) def is_list(self, key): """Return True if variable is a list or a tuple""" data = self.model.get_data() return isinstance(data[key], (tuple, list)) def get_len(self, key): """Return sequence length""" data = self.model.get_data() return len(data[key]) def is_array(self, key): """Return True if variable is a numpy array""" data = self.model.get_data() return isinstance(data[key], (ndarray, MaskedArray)) def is_image(self, key): """Return True if variable is a PIL.Image image""" data = self.model.get_data() return isinstance(data[key], Image) def is_dict(self, key): """Return True if variable is a dictionary""" data = self.model.get_data() return isinstance(data[key], dict) def get_array_shape(self, key): """Return array's shape""" data = self.model.get_data() return data[key].shape def get_array_ndim(self, key): """Return array's ndim""" data = self.model.get_data() return data[key].ndim def oedit(self, key): """Edit item""" data = self.model.get_data() from guidata.widgets.objecteditor import oedit oedit(data[key]) def plot(self, key, funcname): """Plot item""" data = self.model.get_data() import guiqwt.pyplot as plt plt.figure() getattr(plt, funcname)(data[key]) plt.show() def imshow(self, key): """Show item's image""" data = self.model.get_data() import guiqwt.pyplot as plt plt.figure() plt.imshow(data[key]) plt.show() def show_image(self, key): """Show image (item is a PIL image)""" data = self.model.get_data() data[key].show() # --------------------------------------------------------------------------- def refresh_menu(self): """Refresh context menu""" data = self.model.get_data() index = self.currentIndex() condition = ( (not isinstance(data, tuple)) and index.isValid() and not self.readonly ) self.edit_action.setEnabled(condition) self.remove_action.setEnabled(condition) self.insert_action.setEnabled(not self.readonly) self.duplicate_action.setEnabled(condition) condition_rename = not isinstance(data, (tuple, list, set)) self.rename_action.setEnabled(condition_rename) self.refresh_plot_entries(index) def set_filter(self, dictfilter=None): """Set table dict filter""" self.dictfilter = dictfilter class CollectionsEditorWidget(QWidget): """Dictionary Editor Widget""" def __init__(self, parent, data, readonly=False, title=""): QWidget.__init__(self, parent) self.editor = CollectionsEditorTableView(self, data, readonly, title) layout = QVBoxLayout() layout.addWidget(self.editor) self.setLayout(layout) def set_data(self, data): """Set DictEditor data""" self.editor.set_data(data) def get_title(self): """Get model title""" return self.editor.model.title class CollectionsEditor(QDialog): """Collections Editor Dialog""" def __init__(self, parent=None): QDialog.__init__(self, parent) win32_fix_title_bar_background(self) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.data_copy = None self.widget = None self.btn_save_and_close = None self.btn_close = None def setup(self, data, title="", readonly=False, width=650, icon=None, parent=None): """Setup editor.""" if isinstance(data, dict): # dictionnary self.data_copy = data.copy() datalen = len(data) elif isinstance(data, (tuple, list)): # list, tuple self.data_copy = data[:] datalen = len(data) else: # unknown object import copy try: self.data_copy = copy.deepcopy(data) except NotImplementedError: self.data_copy = copy.copy(data) except (TypeError, AttributeError): readonly = True self.data_copy = data datalen = len(get_object_attrs(data)) # If the copy has a different type, then do not allow editing, because # this would change the type after saving; cf. issue #6936 if type(self.data_copy) != type(data): readonly = True self.widget = CollectionsEditorWidget( self, self.data_copy, title=title, readonly=readonly ) self.widget.editor.model.sig_setting_data.connect(self.save_and_close_enable) layout = QVBoxLayout() layout.addWidget(self.widget) self.setLayout(layout) # Buttons configuration btn_layout = QHBoxLayout() btn_layout.addStretch() if not readonly: self.btn_save_and_close = QPushButton(_("Save and Close")) self.btn_save_and_close.setDisabled(True) self.btn_save_and_close.clicked.connect(self.accept) btn_layout.addWidget(self.btn_save_and_close) self.btn_close = QPushButton(_("Close")) self.btn_close.setAutoDefault(True) self.btn_close.setDefault(True) self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) layout.addLayout(btn_layout) constant = 121 row_height = 30 error_margin = 10 height = constant + row_height * min([10, datalen]) + error_margin self.resize(width, height) self.setWindowTitle(self.widget.get_title()) if icon is None: self.setWindowIcon(get_icon("dictedit.png")) # Make the dialog act as a window self.setWindowFlags(Qt.Window) @Slot() def save_and_close_enable(self): """Handle the data change event to enable the save and close button.""" if self.btn_save_and_close: self.btn_save_and_close.setEnabled(True) self.btn_save_and_close.setAutoDefault(True) self.btn_save_and_close.setDefault(True) def get_value(self): """Return modified copy of dictionary or list""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.data_copy class DictEditor(CollectionsEditor): """ """ def __init__(self, parent=None): warnings.warn( "`DictEditor` has been renamed to `CollectionsEditor` in " "Spyder 3. Please use `CollectionsEditor` instead", RuntimeWarning, ) CollectionsEditor.__init__(self, parent) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640626112.1598969 guidata-2.0.2/guidata/widgets/console/0000777000000000000000000000000000000000000014552 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640622969.0 guidata-2.0.2/guidata/widgets/console/__init__.py0000666000000000000000000000626700000000000016676 0ustar00# -*- coding: utf-8 -*- """ guidata.widgets.console ======================= This package provides a Python console widget. .. autoclass:: Console .. autoclass:: DockableConsole """ from qtpy.QtCore import Qt from guidata.widgets.console.internalshell import InternalShell from guidata.qtwidgets import DockableWidgetMixin from guidata.qthelpers import win32_fix_title_bar_background from guidata.configtools import get_font from guidata.config import CONF, _ class Console(InternalShell): """ Python console that run an interactive shell linked to the running process. :param parent: parent Qt widget :param namespace: available python namespace when the console start :type namespace: dict :param message: banner displayed before the first prompt :param commands: commands run when the interpreter starts :param type commands: list of string :param multithreaded: multithreaded support """ def __init__( self, parent=None, namespace=None, message=None, commands=None, multithreaded=True, debug=False, ): InternalShell.__init__( self, parent=parent, namespace=namespace, message=message, commands=commands or [], multithreaded=multithreaded, debug=debug, ) win32_fix_title_bar_background(self) self.setup() def setup(self): """Setup the calltip widget and show the console once all internal handler are ready.""" font = get_font(CONF, "codeeditor") font.setPointSize(10) self.set_font(font) self.set_codecompletion_auto(True) self.set_calltips(True) self.setup_completion(size=(300, 180), font=font) try: self.exception_occurred.connect(self.show_console) except AttributeError: pass def closeEvent(self, event): """Reimplement Qt base method""" InternalShell.closeEvent(self, event) self.exit_interpreter() event.accept() class DockableConsole(Console, DockableWidgetMixin): """ Dockable Python console that run an interactive shell linked to the running process. :param parent: parent Qt widget :param namespace: available python namespace when the console start :type namespace: dict :param message: banner displayed before the first prompt :param commands: commands run when the interpreter starts :param type commands: list of string """ LOCATION = Qt.BottomDockWidgetArea def __init__( self, parent, namespace, message, commands=None, multithreaded=True, debug=False ): DockableWidgetMixin.__init__(self) Console.__init__( self, parent=parent, namespace=namespace, message=message, commands=commands or [], multithreaded=multithreaded, debug=debug, ) def show_console(self): """Show the console widget.""" self.dockwidget.raise_() self.dockwidget.show() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/console/base.py0000666000000000000000000016346000000000000016050 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """QPlainTextEdit base class""" # pylint: disable=C0103 # pylint: disable=R0903 # pylint: disable=R0911 # pylint: disable=R0201 import os import re import sys from collections import OrderedDict from qtpy.QtWidgets import ( QApplication, QListWidget, QListWidgetItem, QMainWindow, QPlainTextEdit, QTextEdit, QToolTip, QAbstractItemView, ) from qtpy.QtGui import ( QPalette, QColor, QFont, QTextCharFormat, QTextCursor, QTextFormat, QTextOption, QClipboard, QMouseEvent, ) from qtpy.QtCore import ( QEvent, QEventLoop, QPoint, Signal, Slot, Qt, ) from guidata.widgets.console.calltip import CallTipWidget from guidata.widgets.console.mixins import BaseEditMixin from guidata.widgets.console.terminal import ANSIEscapeCodeHandler from guidata.configtools import get_font, get_icon from guidata.config import CONF def insert_text_to(cursor, text, fmt): """Helper to print text, taking into account backspaces""" while True: index = text.find(chr(8)) # backspace if index == -1: break cursor.insertText(text[:index], fmt) if cursor.positionInBlock() > 0: cursor.deletePreviousChar() text = text[index + 1 :] cursor.insertText(text, fmt) class CompletionWidget(QListWidget): """Completion list widget""" sig_show_completions = Signal(object) def __init__(self, parent, ancestor): QListWidget.__init__(self, ancestor) self.setWindowFlags(Qt.SubWindow | Qt.FramelessWindowHint) self.textedit = parent self.completion_list = None self.case_sensitive = False self.enter_select = None self.hide() self.itemActivated.connect(self.item_selected) def setup_appearance(self, size, font): """ :param size: :param font: """ self.resize(*size) self.setFont(font) def show_list(self, completion_list, automatic=True): """ :param completion_list: :param automatic: :return: """ types = [c[1] for c in completion_list] completion_list = [c[0] for c in completion_list] if len(completion_list) == 1 and not automatic: self.textedit.insert_completion(completion_list[0]) return self.completion_list = completion_list self.clear() icons_map = { "instance": "attribute", "statement": "attribute", "method": "method", "function": "function", "class": "class", "module": "module", } self.type_list = types if any(types): for (c, t) in zip(completion_list, types): icon = icons_map.get(t, "no_match") self.addItem(QListWidgetItem(get_icon(icon + ".png"), c)) else: self.addItems(completion_list) self.setCurrentRow(0) QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) self.show() self.setFocus() self.raise_() # Retrieving current screen height desktop = QApplication.desktop() srect = desktop.availableGeometry(desktop.screenNumber(self)) screen_right = srect.right() screen_bottom = srect.bottom() point = self.textedit.cursorRect().bottomRight() point.setX(point.x() + self.textedit.get_linenumberarea_width()) point = self.textedit.mapToGlobal(point) # Computing completion widget and its parent right positions comp_right = point.x() + self.width() ancestor = self.parent() if ancestor is None: anc_right = screen_right else: anc_right = min([ancestor.x() + ancestor.width(), screen_right]) # Moving completion widget to the left # if there is not enough space to the right if comp_right > anc_right: point.setX(point.x() - self.width()) # Computing completion widget and its parent bottom positions comp_bottom = point.y() + self.height() ancestor = self.parent() if ancestor is None: anc_bottom = screen_bottom else: anc_bottom = min([ancestor.y() + ancestor.height(), screen_bottom]) # Moving completion widget above if there is not enough space below x_position = point.x() if comp_bottom > anc_bottom: point = self.textedit.cursorRect().topRight() point = self.textedit.mapToGlobal(point) point.setX(x_position) point.setY(point.y() - self.height()) if ancestor is not None: # Useful only if we set parent to 'ancestor' in __init__ point = ancestor.mapFromGlobal(point) self.move(point) if str(self.textedit.completion_text): # When initialized, if completion text is not empty, we need # to update the displayed list: self.update_current() # signal used for testing self.sig_show_completions.emit(completion_list) def hide(self): """ """ QListWidget.hide(self) self.textedit.setFocus() def keyPressEvent(self, event): """ :param event: """ text, key = event.text(), event.key() alt = event.modifiers() & Qt.AltModifier shift = event.modifiers() & Qt.ShiftModifier ctrl = event.modifiers() & Qt.ControlModifier modifier = shift or ctrl or alt if ( key in (Qt.Key_Return, Qt.Key_Enter) and self.enter_select ) or key == Qt.Key_Tab: self.item_selected() elif ( key in ( Qt.Key_Return, Qt.Key_Enter, Qt.Key_Left, Qt.Key_Right, ) or text in (".", ":") ): self.hide() self.textedit.keyPressEvent(event) elif ( key in ( Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown, Qt.Key_Home, Qt.Key_End, Qt.Key_CapsLock, ) and not modifier ): QListWidget.keyPressEvent(self, event) elif len(text) or key == Qt.Key_Backspace: self.textedit.keyPressEvent(event) self.update_current() elif modifier: self.textedit.keyPressEvent(event) else: self.hide() QListWidget.keyPressEvent(self, event) def update_current(self): """ """ completion_text = str(self.textedit.completion_text) if completion_text: for row, completion in enumerate(self.completion_list): if not self.case_sensitive: print(completion_text) # spyder: test-skip completion = completion.lower() completion_text = completion_text.lower() if completion.startswith(completion_text): self.setCurrentRow(row) self.scrollTo(self.currentIndex(), QAbstractItemView.PositionAtTop) break else: self.hide() else: self.hide() def focusOutEvent(self, event): """ :param event: """ event.ignore() # Don't hide it on Mac when main window loses focus because # keyboard input is lost # Fixes Issue 1318 if sys.platform == "darwin": if event.reason() != Qt.ActiveWindowFocusReason: self.hide() else: self.hide() def item_selected(self, item=None): """ :param item: """ if item is None: item = self.currentItem() self.textedit.insert_completion(str(item.text())) self.hide() class TextEditBaseWidget(QPlainTextEdit, BaseEditMixin): """Text edit base widget""" BRACE_MATCHING_SCOPE = ("sof", "eof") cell_separators = None focus_in = Signal() zoom_in = Signal() zoom_out = Signal() zoom_reset = Signal() focus_changed = Signal() sig_eol_chars_changed = Signal(str) def __init__(self, parent=None): QPlainTextEdit.__init__(self, parent) BaseEditMixin.__init__(self) self.setAttribute(Qt.WA_DeleteOnClose) self.extra_selections_dict = OrderedDict() self.textChanged.connect(self.changed) self.cursorPositionChanged.connect(self.cursor_position_changed) self.indent_chars = " " * 4 self.tab_stop_width_spaces = 4 # Code completion / calltips if parent is not None: mainwin = parent while not isinstance(mainwin, QMainWindow): mainwin = mainwin.parent() if mainwin is None: break if mainwin is not None: parent = mainwin self.completion_widget = CompletionWidget(self, parent) self.codecompletion_auto = False self.codecompletion_case = True self.codecompletion_enter = False self.completion_text = "" self.setup_completion() self.calltip_widget = CallTipWidget(self, hide_timer_on=False) self.calltips = True self.calltip_position = None self.has_cell_separators = False self.highlight_current_cell_enabled = False # The color values may be overridden by the syntax highlighter # Highlight current line color self.currentline_color = QColor(Qt.red).lighter(190) self.currentcell_color = QColor(Qt.red).lighter(194) # Brace matching self.bracepos = None self.matched_p_color = QColor(Qt.green) self.unmatched_p_color = QColor(Qt.red) self.last_cursor_cell = None def setup_completion(self, size=None, font=None): """ :param size: :param font: """ size = size or CONF.get("internal_console", "codecompletion/size") font = font or get_font(CONF, "texteditor", "font") self.completion_widget.setup_appearance(size, font) def set_indent_chars(self, indent_chars): """ :param indent_chars: """ self.indent_chars = indent_chars def set_tab_stop_width_spaces(self, tab_stop_width_spaces): """ :param tab_stop_width_spaces: """ self.tab_stop_width_spaces = tab_stop_width_spaces self.update_tab_stop_width_spaces() def update_tab_stop_width_spaces(self): """ """ self.setTabStopWidth(self.fontMetrics().width(" " * self.tab_stop_width_spaces)) def set_palette(self, background, foreground): """ Set text editor palette colors: background color and caret (text cursor) color """ palette = QPalette() palette.setColor(QPalette.Base, background) palette.setColor(QPalette.Text, foreground) self.setPalette(palette) # Set the right background color when changing color schemes # or creating new Editor windows. This seems to be a Qt bug. # Fixes Issue 2028 if sys.platform == "darwin": if self.objectName(): style = "QPlainTextEdit#%s {background: %s; color: %s;}" % ( self.objectName(), background.name(), foreground.name(), ) self.setStyleSheet(style) # ------Extra selections def extra_selection_length(self, key): """ :param key: :return: """ selection = self.get_extra_selections(key) if selection: cursor = self.extra_selections_dict[key][0].cursor selection_length = cursor.selectionEnd() - cursor.selectionStart() return selection_length else: return 0 def get_extra_selections(self, key): """ :param key: :return: """ return self.extra_selections_dict.get(key, []) def set_extra_selections(self, key, extra_selections): """ :param key: :param extra_selections: """ self.extra_selections_dict[key] = extra_selections self.extra_selections_dict = OrderedDict( sorted( self.extra_selections_dict.items(), key=lambda s: self.extra_selection_length(s[0]), reverse=True, ) ) def update_extra_selections(self): """ """ extra_selections = [] # Python 3 compatibility (weird): current line has to be # highlighted first if "current_cell" in self.extra_selections_dict: extra_selections.extend(self.extra_selections_dict["current_cell"]) if "current_line" in self.extra_selections_dict: extra_selections.extend(self.extra_selections_dict["current_line"]) for key, extra in list(self.extra_selections_dict.items()): if not (key == "current_line" or key == "current_cell"): extra_selections.extend(extra) self.setExtraSelections(extra_selections) def clear_extra_selections(self, key): """ :param key: """ self.extra_selections_dict[key] = [] self.update_extra_selections() def changed(self): """Emit changed signal""" self.modificationChanged.emit(self.document().isModified()) # ------Highlight current line def highlight_current_line(self): """Highlight current line""" selection = QTextEdit.ExtraSelection() selection.format.setProperty(QTextFormat.FullWidthSelection, True) selection.format.setBackground(self.currentline_color) selection.cursor = self.textCursor() selection.cursor.clearSelection() self.set_extra_selections("current_line", [selection]) self.update_extra_selections() def unhighlight_current_line(self): """Unhighlight current line""" self.clear_extra_selections("current_line") # ------Highlight current cell def highlight_current_cell(self): """Highlight current cell""" if self.cell_separators is None or not self.highlight_current_cell_enabled: return selection = QTextEdit.ExtraSelection() selection.format.setProperty(QTextFormat.FullWidthSelection, True) selection.format.setBackground(self.currentcell_color) ( selection.cursor, whole_file_selected, whole_screen_selected, ) = self.select_current_cell_in_visible_portion() if whole_file_selected: self.clear_extra_selections("current_cell") elif whole_screen_selected: if self.has_cell_separators: self.set_extra_selections("current_cell", [selection]) self.update_extra_selections() else: self.clear_extra_selections("current_cell") else: self.set_extra_selections("current_cell", [selection]) self.update_extra_selections() def unhighlight_current_cell(self): """Unhighlight current cell""" self.clear_extra_selections("current_cell") # ------Brace matching def find_brace_match(self, position, brace, forward): """ :param position: :param brace: :param forward: :return: """ start_pos, end_pos = self.BRACE_MATCHING_SCOPE if forward: bracemap = {"(": ")", "[": "]", "{": "}"} text = self.get_text(position, end_pos) i_start_open = 1 i_start_close = 1 else: bracemap = {")": "(", "]": "[", "}": "{"} text = self.get_text(start_pos, position) i_start_open = len(text) - 1 i_start_close = len(text) - 1 while True: if forward: i_close = text.find(bracemap[brace], i_start_close) else: i_close = text.rfind(bracemap[brace], 0, i_start_close + 1) if i_close > -1: if forward: i_start_close = i_close + 1 i_open = text.find(brace, i_start_open, i_close) else: i_start_close = i_close - 1 i_open = text.rfind(brace, i_close, i_start_open + 1) if i_open > -1: if forward: i_start_open = i_open + 1 else: i_start_open = i_open - 1 else: # found matching brace if forward: return position + i_close else: return position - (len(text) - i_close) else: # no matching brace return def __highlight(self, positions, color=None, cancel=False): if cancel: self.clear_extra_selections("brace_matching") return extra_selections = [] for position in positions: if position > self.get_position("eof"): return selection = QTextEdit.ExtraSelection() selection.format.setBackground(color) selection.cursor = self.textCursor() selection.cursor.clearSelection() selection.cursor.setPosition(position) selection.cursor.movePosition( QTextCursor.NextCharacter, QTextCursor.KeepAnchor ) extra_selections.append(selection) self.set_extra_selections("brace_matching", extra_selections) self.update_extra_selections() def cursor_position_changed(self): """Brace matching""" if self.bracepos is not None: self.__highlight(self.bracepos, cancel=True) self.bracepos = None cursor = self.textCursor() if cursor.position() == 0: return cursor.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor) text = str(cursor.selectedText()) pos1 = cursor.position() if text in (")", "]", "}"): pos2 = self.find_brace_match(pos1, text, forward=False) elif text in ("(", "[", "{"): pos2 = self.find_brace_match(pos1, text, forward=True) else: return if pos2 is not None: self.bracepos = (pos1, pos2) self.__highlight(self.bracepos, color=self.matched_p_color) else: self.bracepos = (pos1,) self.__highlight(self.bracepos, color=self.unmatched_p_color) # -----Widget setup and options def set_codecompletion_auto(self, state): """Set code completion state""" self.codecompletion_auto = state def set_codecompletion_case(self, state): """Case sensitive completion""" self.codecompletion_case = state self.completion_widget.case_sensitive = state def set_codecompletion_enter(self, state): """Enable Enter key to select completion""" self.codecompletion_enter = state self.completion_widget.enter_select = state def set_calltips(self, state): """Set calltips state""" self.calltips = state def set_wrap_mode(self, mode=None): """ Set wrap mode Valid *mode* values: None, 'word', 'character' """ if mode == "word": wrap_mode = QTextOption.WrapAtWordBoundaryOrAnywhere elif mode == "character": wrap_mode = QTextOption.WrapAnywhere else: wrap_mode = QTextOption.NoWrap self.setWordWrapMode(wrap_mode) # ------Reimplementing Qt methods @Slot() def copy(self): """ Reimplement Qt method Copy text to clipboard with correct EOL chars """ if self.get_selected_text(): QApplication.clipboard().setText(self.get_selected_text()) def toPlainText(self): """ Reimplement Qt method Fix PyQt4 bug on Windows and Python 3 """ # Fix what appears to be a PyQt4 bug when getting file # contents under Windows and PY3. This bug leads to # corruptions when saving files with certain combinations # of unicode chars on them (like the one attached on # Issue 1546) if os.name == "nt": text = self.get_text("sof", "eof") return ( text.replace("\u2028", "\n") .replace("\u2029", "\n") .replace("\u0085", "\n") ) else: return super(TextEditBaseWidget, self).toPlainText() def keyPressEvent(self, event): """ :param event: """ text, key = event.text(), event.key() ctrl = event.modifiers() & Qt.ControlModifier meta = event.modifiers() & Qt.MetaModifier # Use our own copy method for {Ctrl,Cmd}+C to avoid Qt # copying text in HTML (See Issue 2285) if (ctrl or meta) and key == Qt.Key_C: self.copy() else: super(TextEditBaseWidget, self).keyPressEvent(event) # ------Text: get, set, ... def get_selection_as_executable_code(self): """Return selected text as a processed text, to be executable in a Python/IPython interpreter""" ls = self.get_line_separator() _indent = lambda line: len(line) - len(line.lstrip()) line_from, line_to = self.get_selection_bounds() text = self.get_selected_text() if not text: return lines = text.split(ls) if len(lines) > 1: # Multiline selection -> eventually fixing indentation original_indent = _indent(self.get_text_line(line_from)) text = (" " * (original_indent - _indent(lines[0]))) + text # If there is a common indent to all lines, find it. # Moving from bottom line to top line ensures that blank # lines inherit the indent of the line *below* it, # which is the desired behavior. min_indent = 999 current_indent = 0 lines = text.split(ls) for i in range(len(lines) - 1, -1, -1): line = lines[i] if line.strip(): current_indent = _indent(line) min_indent = min(current_indent, min_indent) else: lines[i] = " " * current_indent if min_indent: lines = [line[min_indent:] for line in lines] # Remove any leading whitespace or comment lines # since they confuse the reserved word detector that follows below while lines: first_line = lines[0].lstrip() if first_line == "" or first_line[0] == "#": lines.pop(0) else: break # Add an EOL character after indentation blocks that start with some # Python reserved words, so that it gets evaluated automatically # by the console varname = re.compile(r"[a-zA-Z0-9_]*") # Matches valid variable names. maybe = False nextexcept = () for n, line in enumerate(lines): if not _indent(line): word = varname.match(line).group() if maybe and word not in nextexcept: lines[n - 1] += ls maybe = False if word: if word in ("def", "for", "while", "with", "class"): maybe = True nextexcept = () elif word == "if": maybe = True nextexcept = ("elif", "else") elif word == "try": maybe = True nextexcept = ("except", "finally") if maybe: if lines[-1].strip() == "": lines[-1] += ls else: lines.append(ls) return ls.join(lines) def __exec_cell(self): init_cursor = QTextCursor(self.textCursor()) start_pos, end_pos = self.__save_selection() cursor, whole_file_selected = self.select_current_cell() if not whole_file_selected: self.setTextCursor(cursor) text = self.get_selection_as_executable_code() self.last_cursor_cell = init_cursor self.__restore_selection(start_pos, end_pos) if text is not None: text = text.rstrip() return text def get_cell_as_executable_code(self): """Return cell contents as executable code""" return self.__exec_cell() def get_last_cell_as_executable_code(self): """ :return: """ text = None if self.last_cursor_cell: self.setTextCursor(self.last_cursor_cell) self.highlight_current_cell() text = self.__exec_cell() return text def is_cell_separator(self, cursor=None, block=None): """Return True if cursor (or text block) is on a block separator""" assert cursor is not None or block is not None if cursor is not None: cursor0 = QTextCursor(cursor) cursor0.select(QTextCursor.BlockUnderCursor) text = str(cursor0.selectedText()) else: text = str(block.text()) if self.cell_separators is None: return False else: return text.lstrip().startswith(self.cell_separators) def select_current_cell(self): """Select cell under cursor cell = group of lines separated by CELL_SEPARATORS returns the textCursor and a boolean indicating if the entire file is selected""" cursor = self.textCursor() cursor.movePosition(QTextCursor.StartOfBlock) cur_pos = prev_pos = cursor.position() # Moving to the next line that is not a separator, if we are # exactly at one of them while self.is_cell_separator(cursor): cursor.movePosition(QTextCursor.NextBlock) prev_pos = cur_pos cur_pos = cursor.position() if cur_pos == prev_pos: return cursor, False prev_pos = cur_pos # If not, move backwards to find the previous separator while not self.is_cell_separator(cursor): cursor.movePosition(QTextCursor.PreviousBlock) prev_pos = cur_pos cur_pos = cursor.position() if cur_pos == prev_pos: if self.is_cell_separator(cursor): return cursor, False else: break cursor.setPosition(prev_pos) cell_at_file_start = cursor.atStart() # Once we find it (or reach the beginning of the file) # move to the next separator (or the end of the file) # so we can grab the cell contents while not self.is_cell_separator(cursor): cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor) cur_pos = cursor.position() if cur_pos == prev_pos: cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) break prev_pos = cur_pos cell_at_file_end = cursor.atEnd() return cursor, cell_at_file_start and cell_at_file_end def select_current_cell_in_visible_portion(self): """Select cell under cursor in the visible portion of the file cell = group of lines separated by CELL_SEPARATORS Returns: - the textCursor - a boolean indicating if the entire file is selected - a boolean indicating if the entire visible portion of the file is selected""" cursor = self.textCursor() cursor.movePosition(QTextCursor.StartOfBlock) cur_pos = prev_pos = cursor.position() beg_pos = self.cursorForPosition(QPoint(0, 0)).position() bottom_right = QPoint(self.viewport().width() - 1, self.viewport().height() - 1) end_pos = self.cursorForPosition(bottom_right).position() # Moving to the next line that is not a separator, if we are # exactly at one of them while self.is_cell_separator(cursor): cursor.movePosition(QTextCursor.NextBlock) prev_pos = cur_pos cur_pos = cursor.position() if cur_pos == prev_pos: return cursor, False, False prev_pos = cur_pos # If not, move backwards to find the previous separator while not self.is_cell_separator(cursor) and cursor.position() >= beg_pos: cursor.movePosition(QTextCursor.PreviousBlock) prev_pos = cur_pos cur_pos = cursor.position() if cur_pos == prev_pos: if self.is_cell_separator(cursor): return cursor, False, False else: break cell_at_screen_start = cursor.position() <= beg_pos cursor.setPosition(prev_pos) cell_at_file_start = cursor.atStart() # Selecting cell header if not cell_at_file_start: cursor.movePosition(QTextCursor.PreviousBlock) cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor) # Once we find it (or reach the beginning of the file) # move to the next separator (or the end of the file) # so we can grab the cell contents while not self.is_cell_separator(cursor) and cursor.position() <= end_pos: cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor) cur_pos = cursor.position() if cur_pos == prev_pos: cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) break prev_pos = cur_pos cell_at_file_end = cursor.atEnd() cell_at_screen_end = cursor.position() >= end_pos return ( cursor, cell_at_file_start and cell_at_file_end, cell_at_screen_start and cell_at_screen_end, ) def go_to_next_cell(self): """Go to the next cell of lines""" cursor = self.textCursor() cursor.movePosition(QTextCursor.NextBlock) cur_pos = prev_pos = cursor.position() while not self.is_cell_separator(cursor): # Moving to the next code cell cursor.movePosition(QTextCursor.NextBlock) prev_pos = cur_pos cur_pos = cursor.position() if cur_pos == prev_pos: return self.setTextCursor(cursor) def go_to_previous_cell(self): """Go to the previous cell of lines""" cursor = self.textCursor() cur_pos = prev_pos = cursor.position() if self.is_cell_separator(cursor): # Move to the previous cell cursor.movePosition(QTextCursor.PreviousBlock) cur_pos = prev_pos = cursor.position() while not self.is_cell_separator(cursor): # Move to the previous cell or the beginning of the current cell cursor.movePosition(QTextCursor.PreviousBlock) prev_pos = cur_pos cur_pos = cursor.position() if cur_pos == prev_pos: return self.setTextCursor(cursor) def get_line_count(self): """Return document total line number""" return self.blockCount() def __save_selection(self): """Save current cursor selection and return position bounds""" cursor = self.textCursor() return cursor.selectionStart(), cursor.selectionEnd() def __restore_selection(self, start_pos, end_pos): """Restore cursor selection from position bounds""" cursor = self.textCursor() cursor.setPosition(start_pos) cursor.setPosition(end_pos, QTextCursor.KeepAnchor) self.setTextCursor(cursor) def __duplicate_line_or_selection(self, after_current_line=True): """Duplicate current line or selected text""" cursor = self.textCursor() cursor.beginEditBlock() start_pos, end_pos = self.__save_selection() if str(cursor.selectedText()): cursor.setPosition(end_pos) # Check if end_pos is at the start of a block: if so, starting # changes from the previous block cursor.movePosition(QTextCursor.StartOfBlock, QTextCursor.KeepAnchor) if not str(cursor.selectedText()): cursor.movePosition(QTextCursor.PreviousBlock) end_pos = cursor.position() cursor.setPosition(start_pos) cursor.movePosition(QTextCursor.StartOfBlock) while cursor.position() <= end_pos: cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) if cursor.atEnd(): cursor_temp = QTextCursor(cursor) cursor_temp.clearSelection() cursor_temp.insertText(self.get_line_separator()) break cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor) text = cursor.selectedText() cursor.clearSelection() if not after_current_line: # Moving cursor before current line/selected text cursor.setPosition(start_pos) cursor.movePosition(QTextCursor.StartOfBlock) start_pos += len(text) end_pos += len(text) cursor.insertText(text) cursor.endEditBlock() self.setTextCursor(cursor) self.__restore_selection(start_pos, end_pos) def duplicate_line(self): """ Duplicate current line or selected text Paste the duplicated text *after* the current line/selected text """ self.__duplicate_line_or_selection(after_current_line=True) def copy_line(self): """ Copy current line or selected text Paste the duplicated text *before* the current line/selected text """ self.__duplicate_line_or_selection(after_current_line=False) def __move_line_or_selection(self, after_current_line=True): """Move current line or selected text""" cursor = self.textCursor() cursor.beginEditBlock() start_pos, end_pos = self.__save_selection() last_line = False # ------ Select text # Get selection start location cursor.setPosition(start_pos) cursor.movePosition(QTextCursor.StartOfBlock) start_pos = cursor.position() # Get selection end location cursor.setPosition(end_pos) if not cursor.atBlockStart() or end_pos == start_pos: cursor.movePosition(QTextCursor.EndOfBlock) cursor.movePosition(QTextCursor.NextBlock) end_pos = cursor.position() # Check if selection ends on the last line of the document if cursor.atEnd(): if not cursor.atBlockStart() or end_pos == start_pos: last_line = True # ------ Stop if at document boundary cursor.setPosition(start_pos) if cursor.atStart() and not after_current_line: # Stop if selection is already at top of the file while moving up cursor.endEditBlock() self.setTextCursor(cursor) self.__restore_selection(start_pos, end_pos) return cursor.setPosition(end_pos, QTextCursor.KeepAnchor) if last_line and after_current_line: # Stop if selection is already at end of the file while moving down cursor.endEditBlock() self.setTextCursor(cursor) self.__restore_selection(start_pos, end_pos) return # ------ Move text sel_text = str(cursor.selectedText()) cursor.removeSelectedText() if after_current_line: # Shift selection down text = str(cursor.block().text()) sel_text = os.linesep + sel_text[0:-1] # Move linesep at the start cursor.movePosition(QTextCursor.EndOfBlock) start_pos += len(text) + 1 end_pos += len(text) if not cursor.atEnd(): end_pos += 1 else: # Shift selection up if last_line: # Remove the last linesep and add it to the selected text cursor.deletePreviousChar() sel_text = sel_text + os.linesep cursor.movePosition(QTextCursor.StartOfBlock) end_pos += 1 else: cursor.movePosition(QTextCursor.PreviousBlock) text = str(cursor.block().text()) start_pos -= len(text) + 1 end_pos -= len(text) + 1 cursor.insertText(sel_text) cursor.endEditBlock() self.setTextCursor(cursor) self.__restore_selection(start_pos, end_pos) def move_line_up(self): """Move up current line or selected text""" self.__move_line_or_selection(after_current_line=False) def move_line_down(self): """Move down current line or selected text""" self.__move_line_or_selection(after_current_line=True) def go_to_new_line(self): """Go to the end of the current line and create a new line""" self.stdkey_end(False, False) self.insert_text(self.get_line_separator()) def extend_selection_to_complete_lines(self): """Extend current selection to complete lines""" cursor = self.textCursor() start_pos, end_pos = cursor.selectionStart(), cursor.selectionEnd() cursor.setPosition(start_pos) cursor.setPosition(end_pos, QTextCursor.KeepAnchor) if cursor.atBlockStart(): cursor.movePosition(QTextCursor.PreviousBlock, QTextCursor.KeepAnchor) cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) self.setTextCursor(cursor) def delete_line(self): """Delete current line""" cursor = self.textCursor() if self.has_selected_text(): self.extend_selection_to_complete_lines() start_pos, end_pos = cursor.selectionStart(), cursor.selectionEnd() cursor.setPosition(start_pos) else: start_pos = end_pos = cursor.position() cursor.beginEditBlock() cursor.setPosition(start_pos) cursor.movePosition(QTextCursor.StartOfBlock) while cursor.position() <= end_pos: cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) if cursor.atEnd(): break cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor) cursor.removeSelectedText() cursor.endEditBlock() self.ensureCursorVisible() def set_selection(self, start, end): """ :param start: :param end: """ cursor = self.textCursor() cursor.setPosition(start) cursor.setPosition(end, QTextCursor.KeepAnchor) self.setTextCursor(cursor) def truncate_selection(self, position_from): """Unselect read-only parts in shell, like prompt""" position_from = self.get_position(position_from) cursor = self.textCursor() start, end = cursor.selectionStart(), cursor.selectionEnd() if start < end: start = max([position_from, start]) else: end = max([position_from, end]) self.set_selection(start, end) def restrict_cursor_position(self, position_from, position_to): """In shell, avoid editing text except between prompt and EOF""" position_from = self.get_position(position_from) position_to = self.get_position(position_to) cursor = self.textCursor() cursor_position = cursor.position() if cursor_position < position_from or cursor_position > position_to: self.set_cursor_position(position_to) # ------Code completion / Calltips def hide_tooltip_if_necessary(self, key): """Hide calltip when necessary""" try: calltip_char = self.get_character(self.calltip_position) before = self.is_cursor_before(self.calltip_position, char_offset=1) other = key in (Qt.Key_ParenRight, Qt.Key_Period, Qt.Key_Tab) if calltip_char not in ("?", "(") or before or other: QToolTip.hideText() except (IndexError, TypeError): QToolTip.hideText() def show_completion_widget(self, textlist, automatic=True): """Show completion widget""" self.completion_widget.show_list(textlist, automatic=automatic) def hide_completion_widget(self): """Hide completion widget""" self.completion_widget.hide() def show_completion_list(self, completions, completion_text="", automatic=True): """Display the possible completions""" if not completions: return if not isinstance(completions[0], tuple): completions = [(c, "") for c in completions] if len(completions) == 1 and completions[0][0] == completion_text: return self.completion_text = completion_text # Sorting completion list (entries starting with underscore are # put at the end of the list): underscore = set( [(comp, t) for (comp, t) in completions if comp.startswith("_")] ) completions = sorted(set(completions) - underscore, key=lambda x: x[0].lower()) completions += sorted(underscore, key=lambda x: x[0].lower()) self.show_completion_widget(completions, automatic=automatic) def select_completion_list(self): """Completion list is active, Enter was just pressed""" self.completion_widget.item_selected() def insert_completion(self, text): """ :param text: """ if text: cursor = self.textCursor() cursor.movePosition( QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor, len(self.completion_text), ) cursor.removeSelectedText() self.insert_text(text) def is_completion_widget_visible(self): """Return True is completion list widget is visible""" return self.completion_widget.isVisible() # ------Standard keys def stdkey_clear(self): """ """ if not self.has_selected_text(): self.moveCursor(QTextCursor.NextCharacter, QTextCursor.KeepAnchor) self.remove_selected_text() def stdkey_backspace(self): """ """ if not self.has_selected_text(): self.moveCursor(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor) self.remove_selected_text() def __get_move_mode(self, shift): return QTextCursor.KeepAnchor if shift else QTextCursor.MoveAnchor def stdkey_up(self, shift): """ :param shift: """ self.moveCursor(QTextCursor.Up, self.__get_move_mode(shift)) def stdkey_down(self, shift): """ :param shift: """ self.moveCursor(QTextCursor.Down, self.__get_move_mode(shift)) def stdkey_tab(self): """ """ self.insert_text(self.indent_chars) def stdkey_home(self, shift, ctrl, prompt_pos=None): """Smart HOME feature: cursor is first moved at indentation position, then at the start of the line""" move_mode = self.__get_move_mode(shift) if ctrl: self.moveCursor(QTextCursor.Start, move_mode) else: cursor = self.textCursor() if prompt_pos is None: start_position = self.get_position("sol") else: start_position = self.get_position(prompt_pos) text = self.get_text(start_position, "eol") indent_pos = start_position + len(text) - len(text.lstrip()) if cursor.position() != indent_pos: cursor.setPosition(indent_pos, move_mode) else: cursor.setPosition(start_position, move_mode) self.setTextCursor(cursor) def stdkey_end(self, shift, ctrl): """ :param shift: :param ctrl: """ move_mode = self.__get_move_mode(shift) if ctrl: self.moveCursor(QTextCursor.End, move_mode) else: self.moveCursor(QTextCursor.EndOfBlock, move_mode) def stdkey_pageup(self): """ """ pass def stdkey_pagedown(self): """ """ pass def stdkey_escape(self): """ """ pass # ----Qt Events def mousePressEvent(self, event): """Reimplement Qt method""" if sys.platform.startswith("linux") and event.button() == Qt.MidButton: self.calltip_widget.hide() self.setFocus() event = QMouseEvent( QEvent.MouseButtonPress, event.pos(), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier, ) QPlainTextEdit.mousePressEvent(self, event) QPlainTextEdit.mouseReleaseEvent(self, event) # Send selection text to clipboard to be able to use # the paste method and avoid the strange Issue 1445 # NOTE: This issue seems a focusing problem but it # seems really hard to track mode_clip = QClipboard.Clipboard mode_sel = QClipboard.Selection text_clip = QApplication.clipboard().text(mode=mode_clip) text_sel = QApplication.clipboard().text(mode=mode_sel) QApplication.clipboard().setText(text_sel, mode=mode_clip) self.paste() QApplication.clipboard().setText(text_clip, mode=mode_clip) else: self.calltip_widget.hide() QPlainTextEdit.mousePressEvent(self, event) def focusInEvent(self, event): """Reimplemented to handle focus""" self.focus_changed.emit() self.focus_in.emit() self.highlight_current_cell() QPlainTextEdit.focusInEvent(self, event) def focusOutEvent(self, event): """Reimplemented to handle focus""" self.focus_changed.emit() QPlainTextEdit.focusOutEvent(self, event) def wheelEvent(self, event): """Reimplemented to emit zoom in/out signals when Ctrl is pressed""" # This feature is disabled on MacOS, see Issue 1510 if sys.platform != "darwin": if event.modifiers() & Qt.ControlModifier: if hasattr(event, "angleDelta"): if event.angleDelta().y() < 0: self.zoom_out.emit() elif event.angleDelta().y() > 0: self.zoom_in.emit() elif hasattr(event, "delta"): if event.delta() < 0: self.zoom_out.emit() elif event.delta() > 0: self.zoom_in.emit() return QPlainTextEdit.wheelEvent(self, event) self.highlight_current_cell() class QtANSIEscapeCodeHandler(ANSIEscapeCodeHandler): """ """ def __init__(self): ANSIEscapeCodeHandler.__init__(self) self.base_format = None self.current_format = None def set_light_background(self, state): """ :param state: """ if state: self.default_foreground_color = 30 self.default_background_color = 47 else: self.default_foreground_color = 37 self.default_background_color = 40 def set_base_format(self, base_format): """ :param base_format: """ self.base_format = base_format def get_format(self): """ :return: """ return self.current_format def set_style(self): """ Set font style with the following attributes: 'foreground_color', 'background_color', 'italic', 'bold' and 'underline' """ if self.current_format is None: assert self.base_format is not None self.current_format = QTextCharFormat(self.base_format) # Foreground color if self.foreground_color is None: qcolor = self.base_format.foreground() else: cstr = self.ANSI_COLORS[self.foreground_color - 30][self.intensity] qcolor = QColor(cstr) self.current_format.setForeground(qcolor) # Background color if self.background_color is None: qcolor = self.base_format.background() else: cstr = self.ANSI_COLORS[self.background_color - 40][self.intensity] qcolor = QColor(cstr) self.current_format.setBackground(qcolor) font = self.current_format.font() # Italic if self.italic is None: italic = self.base_format.fontItalic() else: italic = self.italic font.setItalic(italic) # Bold if self.bold is None: bold = self.base_format.font().bold() else: bold = self.bold font.setBold(bold) # Underline if self.underline is None: underline = self.base_format.font().underline() else: underline = self.underline font.setUnderline(underline) self.current_format.setFont(font) def inverse_color(color): """ :param color: """ color.setHsv(color.hue(), color.saturation(), 255 - color.value()) class ConsoleFontStyle(object): """ """ def __init__(self, foregroundcolor, backgroundcolor, bold, italic, underline): self.foregroundcolor = foregroundcolor self.backgroundcolor = backgroundcolor self.bold = bold self.italic = italic self.underline = underline self.format = None def apply_style(self, font, light_background, is_default): """ :param font: :param light_background: :param is_default: """ self.format = QTextCharFormat() self.format.setFont(font) foreground = QColor(self.foregroundcolor) if not light_background and is_default: inverse_color(foreground) self.format.setForeground(foreground) background = QColor(self.backgroundcolor) if not light_background: inverse_color(background) self.format.setBackground(background) font = self.format.font() font.setBold(self.bold) font.setItalic(self.italic) font.setUnderline(self.underline) self.format.setFont(font) class ConsoleBaseWidget(TextEditBaseWidget): """Console base widget""" BRACE_MATCHING_SCOPE = ("sol", "eol") COLOR_PATTERN = re.compile(r"\x01?\x1b\[(.*?)m\x02?") exception_occurred = Signal(str, bool) userListActivated = Signal(int, str) completion_widget_activated = Signal(str) def __init__(self, parent=None): TextEditBaseWidget.__init__(self, parent) self.light_background = True self.setMaximumBlockCount(300) # ANSI escape code handler self.ansi_handler = QtANSIEscapeCodeHandler() # Disable undo/redo (nonsense for a console widget...): self.setUndoRedoEnabled(False) self.userListActivated.connect( lambda user_id, text: self.completion_widget_activated.emit(text) ) self.default_style = ConsoleFontStyle( foregroundcolor=0x000000, backgroundcolor=0xFFFFFF, bold=False, italic=False, underline=False, ) self.error_style = ConsoleFontStyle( foregroundcolor=0xFF0000, backgroundcolor=0xFFFFFF, bold=False, italic=False, underline=False, ) self.traceback_link_style = ConsoleFontStyle( foregroundcolor=0x0000FF, backgroundcolor=0xFFFFFF, bold=True, italic=False, underline=True, ) self.prompt_style = ConsoleFontStyle( foregroundcolor=0x00AA00, backgroundcolor=0xFFFFFF, bold=True, italic=False, underline=False, ) self.font_styles = ( self.default_style, self.error_style, self.traceback_link_style, self.prompt_style, ) self.set_pythonshell_font() self.setMouseTracking(True) def set_light_background(self, state): """ :param state: """ self.light_background = state if state: self.set_palette( background=QColor(Qt.white), foreground=QColor(Qt.darkGray) ) else: self.set_palette( background=QColor(Qt.black), foreground=QColor(Qt.lightGray) ) self.ansi_handler.set_light_background(state) self.set_pythonshell_font() # ------Python shell def insert_text(self, text): """Reimplement TextEditBaseWidget method""" # Eventually this maybe should wrap to insert_text_to if # backspace-handling is required self.textCursor().insertText(text, self.default_style.format) def paste(self): """Reimplement Qt method""" if self.has_selected_text(): self.remove_selected_text() self.insert_text(QApplication.clipboard().text()) def append_text_to_shell(self, text, error, prompt): """ Append text to Python shell In a way, this method overrides the method 'insert_text' when text is inserted at the end of the text widget for a Python shell Handles error messages and show blue underlined links Handles ANSI color sequences Handles ANSI FF sequence """ cursor = self.textCursor() cursor.movePosition(QTextCursor.End) if "\r" in text: # replace \r\n with \n text = text.replace("\r\n", "\n") text = text.replace("\r", "\n") while True: index = text.find(chr(12)) if index == -1: break text = text[index + 1 :] self.clear() if error: is_traceback = False for text in text.splitlines(True): if text.startswith(" File") and not text.startswith(' File "<'): is_traceback = True # Show error links in blue underlined text cursor.insertText(" ", self.default_style.format) cursor.insertText(text[2:], self.traceback_link_style.format) else: # Show error/warning messages in red cursor.insertText(text, self.error_style.format) self.exception_occurred.emit(text, is_traceback) elif prompt: # Show prompt in green insert_text_to(cursor, text, self.prompt_style.format) else: # Show other outputs in black last_end = 0 for match in self.COLOR_PATTERN.finditer(text): insert_text_to( cursor, text[last_end : match.start()], self.default_style.format ) last_end = match.end() try: for code in [int(_c) for _c in match.group(1).split(";")]: self.ansi_handler.set_code(code) except ValueError: pass self.default_style.format = self.ansi_handler.get_format() insert_text_to(cursor, text[last_end:], self.default_style.format) # # Slower alternative: # segments = self.COLOR_PATTERN.split(text) # cursor.insertText(segments.pop(0), self.default_style.format) # if segments: # for ansi_tags, text in zip(segments[::2], segments[1::2]): # for ansi_tag in ansi_tags.split(';'): # self.ansi_handler.set_code(int(ansi_tag)) # self.default_style.format = self.ansi_handler.get_format() # cursor.insertText(text, self.default_style.format) self.set_cursor_position("eof") self.setCurrentCharFormat(self.default_style.format) def set_pythonshell_font(self, font=None): """Python Shell only""" if font is None: font = QFont() for style in self.font_styles: style.apply_style( font=font, light_background=self.light_background, is_default=style is self.default_style, ) self.ansi_handler.set_base_format(self.default_style.format) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/console/calltip.py0000666000000000000000000003066600000000000016567 0ustar00# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2010 IPython Development Team # Copyright (c) 2013- Spyder Project Contributors # # Distributed under the terms of the Modified BSD License # (BSD 3-clause; see NOTICE.txt in the Spyder root directory for details). # ----------------------------------------------------------------------------- """ Calltip widget used only to show signatures. Adapted from IPython/frontend/qt/console/call_tip_widget.py of the `IPython Project `_. Now located at qtconsole/call_tip_widget.py as part of the `Jupyter QtConsole Project `_. """ from unicodedata import category from qtpy.QtWidgets import ( QFrame, QLabel, QPlainTextEdit, QStyle, QStyleOptionFrame, QStylePainter, QTextEdit, QToolTip, ) from qtpy.QtGui import ( QCursor, QPalette, ) from qtpy.QtCore import ( QBasicTimer, QEvent, QCoreApplication, Qt, ) class CallTipWidget(QLabel): """Shows call tips by parsing the current text of Q[Plain]TextEdit.""" # -------------------------------------------------------------------------- # 'QObject' interface # -------------------------------------------------------------------------- def __init__(self, text_edit, hide_timer_on=False): """Create a call tip manager that is attached to the specified Qt text edit widget. """ assert isinstance(text_edit, (QTextEdit, QPlainTextEdit)) super(CallTipWidget, self).__init__(None, Qt.ToolTip) self.app = QCoreApplication.instance() self.hide_timer_on = hide_timer_on self.tip = None self._hide_timer = QBasicTimer() self._text_edit = text_edit self.setFont(text_edit.document().defaultFont()) self.setForegroundRole(QPalette.ToolTipText) self.setBackgroundRole(QPalette.ToolTipBase) self.setPalette(QToolTip.palette()) self.setAlignment(Qt.AlignLeft) self.setIndent(1) self.setFrameStyle(QFrame.NoFrame) self.setMargin( 1 + self.style().pixelMetric(QStyle.PM_ToolTipLabelFrameWidth, None, self) ) def eventFilter(self, obj, event): """Reimplemented to hide on certain key presses and on text edit focus changes. """ if obj == self._text_edit: etype = event.type() if etype == QEvent.KeyPress: key = event.key() cursor = self._text_edit.textCursor() prev_char = self._text_edit.get_character(cursor.position(), offset=-1) if key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Down, Qt.Key_Up): self.hide() elif key == Qt.Key_Escape: self.hide() return True elif prev_char == ")": self.hide() elif etype == QEvent.FocusOut: self.hide() elif etype == QEvent.Enter: if ( self._hide_timer.isActive() and self.app.topLevelAt(QCursor.pos()) == self ): self._hide_timer.stop() elif etype == QEvent.Leave: self._leave_event_hide() return super(CallTipWidget, self).eventFilter(obj, event) def timerEvent(self, event): """Reimplemented to hide the widget when the hide timer fires.""" if event.timerId() == self._hide_timer.timerId(): self._hide_timer.stop() self.hide() # -------------------------------------------------------------------------- # 'QWidget' interface # -------------------------------------------------------------------------- def enterEvent(self, event): """Reimplemented to cancel the hide timer.""" super(CallTipWidget, self).enterEvent(event) if self._hide_timer.isActive() and self.app.topLevelAt(QCursor.pos()) == self: self._hide_timer.stop() def hideEvent(self, event): """Reimplemented to disconnect signal handlers and event filter.""" super(CallTipWidget, self).hideEvent(event) self._text_edit.cursorPositionChanged.disconnect(self._cursor_position_changed) self._text_edit.removeEventFilter(self) def leaveEvent(self, event): """Reimplemented to start the hide timer.""" super(CallTipWidget, self).leaveEvent(event) self._leave_event_hide() def mousePressEvent(self, event): """ :param event: """ super(CallTipWidget, self).mousePressEvent(event) self.hide() def paintEvent(self, event): """Reimplemented to paint the background panel.""" painter = QStylePainter(self) option = QStyleOptionFrame() option.initFrom(self) painter.drawPrimitive(QStyle.PE_PanelTipLabel, option) painter.end() super(CallTipWidget, self).paintEvent(event) def setFont(self, font): """Reimplemented to allow use of this method as a slot.""" super(CallTipWidget, self).setFont(font) def showEvent(self, event): """Reimplemented to connect signal handlers and event filter.""" super(CallTipWidget, self).showEvent(event) self._text_edit.cursorPositionChanged.connect(self._cursor_position_changed) self._text_edit.installEventFilter(self) def focusOutEvent(self, event): """Reimplemented to hide it when focus goes out of the main window. """ self.hide() # -------------------------------------------------------------------------- # 'CallTipWidget' interface # -------------------------------------------------------------------------- def show_tip(self, point, tip, wrapped_tiplines): """Attempts to show the specified tip at the current cursor location.""" # Don't attempt to show it if it's already visible and the text # to be displayed is the same as the one displayed before. if self.isVisible(): if self.tip == tip: return True else: self.hide() # Attempt to find the cursor position at which to show the call tip. text_edit = self._text_edit cursor = text_edit.textCursor() search_pos = cursor.position() - 1 self._start_position, _ = self._find_parenthesis(search_pos, forward=False) if self._start_position == -1: return False if self.hide_timer_on: self._hide_timer.stop() # Logic to decide how much time to show the calltip depending # on the amount of text present if len(wrapped_tiplines) == 1: args = wrapped_tiplines[0].split("(")[1] nargs = len(args.split(",")) if nargs == 1: hide_time = 1400 elif nargs == 2: hide_time = 1600 else: hide_time = 1800 elif len(wrapped_tiplines) == 2: args1 = wrapped_tiplines[1].strip() nargs1 = len(args1.split(",")) if nargs1 == 1: hide_time = 2500 else: hide_time = 2800 else: hide_time = 3500 self._hide_timer.start(hide_time, self) # Set the text and resize the widget accordingly. self.tip = tip self.setText(tip) self.resize(self.sizeHint()) # Locate and show the widget. Place the tip below the current line # unless it would be off the screen. In that case, decide the best # location based trying to minimize the area that goes off-screen. padding = 3 # Distance in pixels between cursor bounds and tip box. cursor_rect = text_edit.cursorRect(cursor) screen_rect = self.app.desktop().screenGeometry(text_edit) point.setY(point.y() + padding) tip_height = self.size().height() tip_width = self.size().width() vertical = "bottom" horizontal = "Right" if point.y() + tip_height > screen_rect.height() + screen_rect.y(): point_ = text_edit.mapToGlobal(cursor_rect.topRight()) # If tip is still off screen, check if point is in top or bottom # half of screen. if point_.y() - tip_height < padding: # If point is in upper half of screen, show tip below it. # otherwise above it. if 2 * point.y() < screen_rect.height(): vertical = "bottom" else: vertical = "top" else: vertical = "top" if point.x() + tip_width > screen_rect.width() + screen_rect.x(): point_ = text_edit.mapToGlobal(cursor_rect.topRight()) # If tip is still off-screen, check if point is in the right or # left half of the screen. if point_.x() - tip_width < padding: if 2 * point.x() < screen_rect.width(): horizontal = "Right" else: horizontal = "Left" else: horizontal = "Left" pos = getattr(cursor_rect, "%s%s" % (vertical, horizontal)) adjusted_point = text_edit.mapToGlobal(pos()) if vertical == "top": point.setY(adjusted_point.y() - tip_height - padding) if horizontal == "Left": point.setX(adjusted_point.x() - tip_width - padding) self.move(point) self.show() return True # -------------------------------------------------------------------------- # Protected interface # -------------------------------------------------------------------------- def _find_parenthesis(self, position, forward=True): """If 'forward' is True (resp. False), proceed forwards (resp. backwards) through the line that contains 'position' until an unmatched closing (resp. opening) parenthesis is found. Returns a tuple containing the position of this parenthesis (or -1 if it is not found) and the number commas (at depth 0) found along the way. """ commas = depth = 0 document = self._text_edit.document() char = str(document.characterAt(position)) # Search until a match is found or a non-printable character is # encountered. while category(char) != "Cc" and position > 0: if char == "," and depth == 0: commas += 1 elif char == ")": if forward and depth == 0: break depth += 1 elif char == "(": if not forward and depth == 0: break depth -= 1 position += 1 if forward else -1 char = str(document.characterAt(position)) else: position = -1 return position, commas def _leave_event_hide(self): """Hides the tooltip after some time has passed (assuming the cursor is not over the tooltip). """ if ( self.hide_timer_on and not self._hide_timer.isActive() and # If Enter events always came after Leave events, we wouldn't need # this check. But on Mac OS, it sometimes happens the other way # around when the tooltip is created. self.app.topLevelAt(QCursor.pos()) != self ): self._hide_timer.start(800, self) # ------ Signal handlers ---------------------------------------------------- def _cursor_position_changed(self): """Updates the tip based on user cursor movement.""" cursor = self._text_edit.textCursor() position = cursor.position() document = self._text_edit.document() char = str(document.characterAt(position - 1)) if position <= self._start_position: self.hide() elif char == ")": pos, _ = self._find_parenthesis(position - 1, forward=False) if pos == -1: self.hide() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/console/dochelpers.py0000666000000000000000000002744400000000000017267 0ustar00# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2009- Spyder Kernels Contributors # # Licensed under the terms of the MIT License # (see spyder_kernels/__init__.py for details) # ----------------------------------------------------------------------------- """Utilities and wrappers around inspect module""" import builtins import inspect import re SYMBOLS = r"[^\'\"a-zA-Z0-9_.]" def getobj(txt, last=False): """Return the last valid object name in string""" txt_end = "" for startchar, endchar in ["[]", "()"]: if txt.endswith(endchar): pos = txt.rfind(startchar) if pos: txt_end = txt[pos:] txt = txt[:pos] tokens = re.split(SYMBOLS, txt) token = None try: while token is None or re.match(SYMBOLS, token): token = tokens.pop() if token.endswith("."): token = token[:-1] if token.startswith("."): # Invalid object name return None if last: # XXX: remove this statement as well as the "last" argument token += txt[txt.rfind(token) + len(token)] token += txt_end if token: return token except IndexError: return None def getobjdir(obj): """ For standard objects, will simply return dir(obj) In special cases (e.g. WrapITK package), will return only string elements of result returned by dir(obj) """ return [item for item in dir(obj) if isinstance(item, str)] def getdoc(obj): """ Return text documentation from an object. This comes in a form of dictionary with four keys: name: The name of the inspected object argspec: It's argspec note: A phrase describing the type of object (function or method) we are inspecting, and the module it belongs to. docstring: It's docstring """ docstring = inspect.getdoc(obj) or inspect.getcomments(obj) or "" # Most of the time doc will only contain ascii characters, but there are # some docstrings that contain non-ascii characters. Not all source files # declare their encoding in the first line, so querying for that might not # yield anything, either. So assume the most commonly used # multi-byte file encoding (which also covers ascii). try: docstring = str(docstring) except: pass # Doc dict keys doc = {"name": "", "argspec": "", "note": "", "docstring": docstring} if callable(obj): try: name = obj.__name__ except AttributeError: doc["docstring"] = docstring return doc if inspect.ismethod(obj): imclass = obj.__self__.__class__ if obj.__self__ is not None: doc["note"] = "Method of %s instance" % obj.__self__.__class__.__name__ else: doc["note"] = "Unbound %s method" % imclass.__name__ obj = obj.__func__ elif hasattr(obj, "__module__"): doc["note"] = "Function of %s module" % obj.__module__ else: doc["note"] = "Function" doc["name"] = obj.__name__ if inspect.isfunction(obj): ( args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations, ) = inspect.getfullargspec(obj) doc["argspec"] = inspect.formatargspec( args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations, formatvalue=lambda o: "=" + repr(o), ) if name == "": doc["name"] = name + " lambda " doc["argspec"] = doc["argspec"][1:-1] # remove parentheses else: argspec = getargspecfromtext(doc["docstring"]) if argspec: doc["argspec"] = argspec # Many scipy and numpy docstrings begin with a function # signature on the first line. This ends up begin redundant # when we are using title and argspec to create the # rich text "Definition:" field. We'll carefully remove this # redundancy but only under a strict set of conditions: # Remove the starting charaters of the 'doc' portion *iff* # the non-whitespace characters on the first line # match *exactly* the combined function title # and argspec we determined above. signature = doc["name"] + doc["argspec"] docstring_blocks = doc["docstring"].split("\n\n") first_block = docstring_blocks[0].strip() if first_block == signature: doc["docstring"] = ( doc["docstring"].replace(signature, "", 1).lstrip() ) else: doc["argspec"] = "(...)" # Remove self from argspec argspec = doc["argspec"] doc["argspec"] = argspec.replace("(self)", "()").replace("(self, ", "(") return doc def getsource(obj): """Wrapper around inspect.getsource""" try: try: src = str(inspect.getsource(obj)) except TypeError: if hasattr(obj, "__class__"): src = str(inspect.getsource(obj.__class__)) else: # Bindings like VTK or ITK require this case src = getdoc(obj) return src except (TypeError, IOError): return def getsignaturefromtext(text, objname): """Get object signatures from text (object documentation) Return a list containing a single string in most cases Example of multiple signatures: PyQt5 objects""" if isinstance(text, dict): text = text.get("docstring", "") # Regexps oneline_re = objname + r'\([^\)].+?(?<=[\w\]\}\'"])\)(?!,)' multiline_re = objname + r'\([^\)]+(?<=[\w\]\}\'"])\)(?!,)' multiline_end_parenleft_re = r'(%s\([^\)]+(\),\n.+)+(?<=[\w\]\}\'"])\))' # Grabbing signatures if not text: text = "" sigs_1 = re.findall(oneline_re + "|" + multiline_re, text) sigs_2 = [g[0] for g in re.findall(multiline_end_parenleft_re % objname, text)] all_sigs = sigs_1 + sigs_2 # The most relevant signature is usually the first one. There could be # others in doctests but those are not so important if all_sigs: return all_sigs[0] else: return "" # Fix for Issue 1953 # TODO: Add more signatures and remove this hack in 2.4 getsignaturesfromtext = getsignaturefromtext def getargspecfromtext(text): """ Try to get the formatted argspec of a callable from the first block of its docstring This will return something like '(foo, bar, k=1)' """ blocks = text.split("\n\n") first_block = blocks[0].strip() return getsignaturefromtext(first_block, "") def getargsfromtext(text, objname): """Get arguments from text (object documentation)""" signature = getsignaturefromtext(text, objname) if signature: argtxt = signature[signature.find("(") + 1 : -1] return argtxt.split(",") def getargsfromdoc(obj): """Get arguments from object doc""" if obj.__doc__ is not None: return getargsfromtext(obj.__doc__, obj.__name__) def getargs(obj): """Get the names and default values of a function's arguments""" if inspect.isfunction(obj) or inspect.isbuiltin(obj): func_obj = obj elif inspect.ismethod(obj): func_obj = obj.__func__ elif inspect.isclass(obj) and hasattr(obj, "__init__"): func_obj = getattr(obj, "__init__") else: return [] if not hasattr(func_obj, "func_code"): # Builtin: try to extract info from doc args = getargsfromdoc(func_obj) if args is not None: return args else: # Example: PyQt5 return getargsfromdoc(obj) args, _, _ = inspect.getargs(func_obj.func_code) if not args: return getargsfromdoc(obj) # Supporting tuple arguments in def statement: for i_arg, arg in enumerate(args): if isinstance(arg, list): args[i_arg] = "(%s)" % ", ".join(arg) defaults = func_obj.__defaults__ if defaults is not None: for index, default in enumerate(defaults): args[index + len(args) - len(defaults)] += "=" + repr(default) if inspect.isclass(obj) or inspect.ismethod(obj): if len(args) == 1: return None if "self" in args: args.remove("self") return args def getargtxt(obj, one_arg_per_line=True): """ Get the names and default values of a function's arguments Return list with separators (', ') formatted for calltips """ args = getargs(obj) if args: sep = ", " textlist = None for i_arg, arg in enumerate(args): if textlist is None: textlist = [""] textlist[-1] += arg if i_arg < len(args) - 1: textlist[-1] += sep if len(textlist[-1]) >= 32 or one_arg_per_line: textlist.append("") if inspect.isclass(obj) or inspect.ismethod(obj): if len(textlist) == 1: return None if "self" + sep in textlist: textlist.remove("self" + sep) return textlist def isdefined(obj, force_import=False, namespace=None): """Return True if object is defined in namespace If namespace is None --> namespace = locals()""" if namespace is None: namespace = locals() attr_list = obj.split(".") base = attr_list.pop(0) if len(base) == 0: return False if base not in builtins.__dict__ and base not in namespace: if force_import: try: module = __import__(base, globals(), namespace) if base not in globals(): globals()[base] = module namespace[base] = module except Exception: return False else: return False for attr in attr_list: try: attr_not_found = not hasattr(eval(base, namespace), attr) except (SyntaxError, AttributeError): return False if attr_not_found: if force_import: try: __import__(base + "." + attr, globals(), namespace) except (ImportError, SyntaxError): return False else: return False base += "." + attr return True if __name__ == "__main__": class Test(object): def method(self, x, y=2): """ :param x: :param y: """ pass print(getargtxt(Test.__init__)) # spyder: test-skip print(getargtxt(Test.method)) # spyder: test-skip print(isdefined("numpy.take", force_import=True)) # spyder: test-skip print(isdefined("__import__")) # spyder: test-skip print(isdefined(".keys", force_import=True)) # spyder: test-skip print(getobj("globals")) # spyder: test-skip print(getobj("globals().keys")) # spyder: test-skip print(getobj("+scipy.signal.")) # spyder: test-skip print(getobj("4.")) # spyder: test-skip print(getdoc(sorted)) # spyder: test-skip print(getargtxt(sorted)) # spyder: test-skip ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/console/internalshell.py0000666000000000000000000004167200000000000020002 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """Internal shell widget : PythonShellWidget + Interpreter""" # pylint: disable=C0103 # pylint: disable=R0903 # pylint: disable=R0911 # pylint: disable=R0201 # FIXME: Internal shell MT: for i in range(100000): print i -> bug import builtins import os import platform import sys import threading from time import time from qtpy.QtWidgets import QMessageBox from qtpy.QtCore import ( QEventLoop, QObject, Signal, Slot, ) from guidata.config import CONF, _, IS_DARK from guidata.widgets.console.dochelpers import getargtxt, getdoc, getobjdir, getsource from guidata.widgets.console.interpreter import Interpreter from guidata.utils import getcwd_or_home from guidata.widgets.console.shell import PythonShellWidget from guidata.utils import run_program from guidata.qthelpers import get_std_icon, create_action from guidata.widgets.objecteditor import oedit builtins.oedit = oedit def create_banner(message): """Create internal shell banner""" system = platform.system() if message is None: bitness = 64 if sys.maxsize > 2 ** 32 else 32 return "Python %s %dbits [%s]" % (platform.python_version(), bitness, system) else: return message class SysOutput(QObject): """Handle standard I/O queue""" data_avail = Signal() def __init__(self): QObject.__init__(self) self.queue = [] self.lock = threading.Lock() def write(self, val): """ :param val: """ self.lock.acquire() self.queue.append(val) self.lock.release() self.data_avail.emit() def empty_queue(self): """ :return: """ self.lock.acquire() s = "".join(self.queue) self.queue = [] self.lock.release() return s # We need to add this method to fix Issue 1789 def flush(self): """ """ pass # This is needed to fix Issue 2984 @property def closed(self): """ :return: """ return False class WidgetProxyData(object): pass class WidgetProxy(QObject): """Handle Shell widget refresh signal""" sig_new_prompt = Signal(str) sig_set_readonly = Signal(bool) sig_edit = Signal(str, bool) sig_wait_input = Signal(str) def __init__(self, input_condition): QObject.__init__(self) self.input_data = None self.input_condition = input_condition def new_prompt(self, prompt): """ :param prompt: """ self.sig_new_prompt.emit(prompt) def set_readonly(self, state): """ :param state: """ self.sig_set_readonly.emit(state) def edit(self, filename, external_editor=False): """ :param filename: :param external_editor: """ self.sig_edit.emit(filename, external_editor) def data_available(self): """Return True if input data is available""" return self.input_data is not WidgetProxyData def wait_input(self, prompt=""): """ :param prompt: """ self.input_data = WidgetProxyData self.sig_wait_input.emit(prompt) def end_input(self, cmd): """ :param cmd: """ self.input_condition.acquire() self.input_data = cmd self.input_condition.notify() self.input_condition.release() class InternalShell(PythonShellWidget): """Shell base widget: link between PythonShellWidget and Interpreter""" status = Signal(str) refresh = Signal() go_to_error = Signal(str) focus_changed = Signal() sig_oedit = Signal(object, bool, object) def __init__( self, parent=None, namespace=None, commands=[], message=None, max_line_count=300, font=None, exitfunc=None, profile=False, multithreaded=True, light_background=not IS_DARK, debug=False, ): PythonShellWidget.__init__(self, parent, profile) self.debug = debug self.set_light_background(light_background) self.multithreaded = multithreaded self.setMaximumBlockCount(max_line_count) if font is not None: self.set_font(font) # Allow raw_input support: self.input_loop = None self.input_mode = False # KeyboardInterrupt support self.interrupted = False # used only for not-multithreaded mode self.sig_keyboard_interrupt.connect(self.keyboard_interrupt) # Code completion / calltips getcfg = lambda option: CONF.get("internal_console", option) case_sensitive = getcfg("codecompletion/case_sensitive") self.set_codecompletion_case(case_sensitive) # keyboard events management self.eventqueue = [] # oedit management in multithread environment # It's not possible to run oedit directly because it creates Qt widgets. # To run oedit in the main thread, a replacement function emits a signal # sig_oedit. The main thread connects to this signal, run oedit and # puts the result in oedit_queue. self.oedit_queue = [] self.oedit_condition = threading.Condition() self.sig_oedit.connect(self.run_oedit) if multithreaded: namespace = namespace or {} namespace["oedit"] = self.oedit # Init interpreter self.exitfunc = exitfunc self.commands = commands self.message = message self.interpreter = None self.start_interpreter(namespace) # Clear status bar self.status.emit("") def run_oedit(self, obj, modal, namespace): """Run oedit(obj, modal, namespace) and put the result in `oedit_queue`.""" with self.oedit_condition: result = oedit(obj, modal, namespace) self.oedit_queue.append(result) self.oedit_condition.notify() def oedit(self, obj, modal=True, namespace=None): """Edit the object 'obj' in a GUI-based editor and return the edited copy (if Cancel is pressed, return None) The object 'obj' is a container Supported container types: dict, list, tuple, str/unicode or numpy.array (instantiate a new QApplication if necessary, so it can be called directly from the interpreter) """ self.sig_oedit.emit(obj, modal, namespace) with self.oedit_condition: self.oedit_condition.wait() return self.oedit_queue.pop() # ------ Interpreter def start_interpreter(self, namespace): """Start Python interpreter""" self.clear() if self.interpreter is not None: self.interpreter.closing() self.interpreter = Interpreter( namespace, self.exitfunc, SysOutput, WidgetProxy, self.debug ) self.interpreter.stdout_write.data_avail.connect(self.stdout_avail) self.interpreter.stderr_write.data_avail.connect(self.stderr_avail) self.interpreter.widget_proxy.sig_set_readonly.connect(self.setReadOnly) self.interpreter.widget_proxy.sig_new_prompt.connect(self.new_prompt) self.interpreter.widget_proxy.sig_edit.connect(self.edit_script) self.interpreter.widget_proxy.sig_wait_input.connect(self.wait_input) if self.multithreaded: self.interpreter.start() # Interpreter banner banner = create_banner(self.message) self.write(banner, prompt=True) # Initial commands for cmd in self.commands: self.run_command(cmd, history=False, new_prompt=False) # First prompt self.new_prompt(self.interpreter.p1) self.refresh.emit() return self.interpreter def exit_interpreter(self): """Exit interpreter""" self.interpreter.exit_flag = True if self.multithreaded: self.interpreter.stdin_write.write(b"\n") self.interpreter.restore_stds() def edit_script(self, filename, external_editor): """ :param filename: :param external_editor: """ filename = str(filename) if external_editor: self.external_editor(filename) else: self.parent().edit_script(filename) def stdout_avail(self): """Data is available in stdout, let's empty the queue and write it!""" data = self.interpreter.stdout_write.empty_queue() if data: self.write(data) def stderr_avail(self): """Data is available in stderr, let's empty the queue and write it!""" data = self.interpreter.stderr_write.empty_queue() if data: self.write(data, error=True) self.flush(error=True) # ------Raw input support def wait_input(self, prompt=""): """Wait for input (raw_input support)""" self.new_prompt(prompt) self.setFocus() self.input_mode = True self.input_loop = QEventLoop() self.input_loop.exec_() self.input_loop = None def end_input(self, cmd): """End of wait_input mode""" self.input_mode = False self.input_loop.exit() self.interpreter.widget_proxy.end_input(cmd) # ----- Menus, actions, ... def setup_context_menu(self): """Reimplement PythonShellWidget method""" PythonShellWidget.setup_context_menu(self) self.help_action = create_action( self, _("Help..."), icon=get_std_icon("DialogHelpButton"), triggered=self.help, ) self.menu.addAction(self.help_action) @Slot() def help(self): """Help on Spyder console""" QMessageBox.about( self, _("Help"), """%s

%s
edit foobar.py

%s
xedit foobar.py

%s
run foobar.py

%s
clear x, y

%s
!ls

%s
object?

%s
result = oedit(object) """ % ( _("Shell special commands:"), _("Internal editor:"), _("External editor:"), _("Run script:"), _("Remove references:"), _("System commands:"), _("Python help:"), _("GUI-based editor:"), ), ) def external_editor(self, filename, goto=-1): """Edit in an external editor Recommended: SciTE (e.g. to go to line where an error did occur)""" editor_path = CONF.get("internal_console", "external_editor/path") goto_option = CONF.get("internal_console", "external_editor/gotoline") try: args = [filename] if goto > 0 and goto_option: args.append("%s%d".format(goto_option, goto)) run_program(editor_path, args) except OSError: self.write_error("External editor was not found:" " %s\n" % editor_path) # ------ I/O def flush(self, error=False, prompt=False): """Reimplement ShellBaseWidget method""" PythonShellWidget.flush(self, error=error, prompt=prompt) if self.interrupted: self.interrupted = False raise KeyboardInterrupt # ------ Clear terminal def clear_terminal(self): """Reimplement ShellBaseWidget method""" self.clear() self.new_prompt( self.interpreter.p2 if self.interpreter.more else self.interpreter.p1 ) # ------ Keyboard events def on_enter(self, command): """on_enter""" if self.profile: # Simple profiling test t0 = time() for _ in range(10): self.execute_command(command) self.insert_text(u"\n<Δt>=%dms\n" % (1e2 * (time() - t0))) self.new_prompt(self.interpreter.p1) else: self.execute_command(command) self.__flush_eventqueue() def keyPressEvent(self, event): """ Reimplement Qt Method Enhanced keypress event handler """ if self.preprocess_keyevent(event): # Event was accepted in self.preprocess_keyevent return self.postprocess_keyevent(event) def __flush_eventqueue(self): """Flush keyboard event queue""" while self.eventqueue: past_event = self.eventqueue.pop(0) self.postprocess_keyevent(past_event) # ------ Command execution def keyboard_interrupt(self): """Simulate keyboard interrupt""" if self.multithreaded: self.interpreter.raise_keyboard_interrupt() else: if self.interpreter.more: self.write_error("\nKeyboardInterrupt\n") self.interpreter.more = False self.new_prompt(self.interpreter.p1) self.interpreter.resetbuffer() else: self.interrupted = True def execute_lines(self, lines): """ Execute a set of lines as multiple command lines: multiple lines of text to be executed as single commands """ for line in lines.splitlines(): stripped_line = line.strip() if stripped_line.startswith("#"): continue self.write(line + os.linesep, flush=True) self.execute_command(line + "\n") self.flush() def execute_command(self, cmd): """ Execute a command :param cmd: one-line command only, with ``'\n'`` at the end """ if self.input_mode: self.end_input(cmd) return if cmd.endswith("\n"): cmd = cmd[:-1] # cls command if cmd == "cls": self.clear_terminal() return self.run_command(cmd) def run_command(self, cmd, history=True, new_prompt=True): """Run command in interpreter""" if not cmd: cmd = "" else: if history: self.add_to_history(cmd) if not self.multithreaded: if "input" not in cmd: self.interpreter.stdin_write.write(bytes(cmd + "\n", "utf-8")) self.interpreter.run_line() self.refresh.emit() else: self.write( _( 'In order to use commands like "raw_input" ' 'or "input" run Spyder with the multithread ' "option (--multithread) from a system terminal" ), error=True, ) else: self.interpreter.stdin_write.write(bytes(cmd + "\n", "utf-8")) # ------ Code completion / Calltips def _eval(self, text): """Is text a valid object?""" return self.interpreter.eval(text) def get_dir(self, objtxt): """Return dir(object)""" obj, valid = self._eval(objtxt) if valid: return getobjdir(obj) def get_globals_keys(self): """Return shell globals() keys""" return list(self.interpreter.namespace.keys()) def get_cdlistdir(self): """Return shell current directory list dir""" return os.listdir(getcwd_or_home()) def iscallable(self, objtxt): """Is object callable?""" obj, valid = self._eval(objtxt) if valid: return callable(obj) def get_arglist(self, objtxt): """Get func/method argument list""" obj, valid = self._eval(objtxt) if valid: return getargtxt(obj) def get__doc__(self, objtxt): """Get object __doc__""" obj, valid = self._eval(objtxt) if valid: return obj.__doc__ def get_doc(self, objtxt): """Get object documentation dictionary""" obj, valid = self._eval(objtxt) if valid: return getdoc(obj) def get_source(self, objtxt): """Get object source""" obj, valid = self._eval(objtxt) if valid: return getsource(obj) def is_defined(self, objtxt, force_import=False): """Return True if object is defined""" return self.interpreter.is_defined(objtxt, force_import) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/console/interpreter.py0000666000000000000000000003044400000000000017474 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """Shell Interpreter""" import atexit import ctypes import os import os.path as osp import pydoc import re import sys import threading from code import InteractiveConsole from guidata.utils import run_shell_command, getcwd_or_home, remove_backslashes from guidata.widgets.console.dochelpers import isdefined sys.path.insert(0, "") def guess_filename(filename): """Guess filename""" if osp.isfile(filename): return filename if not filename.endswith(".py"): filename += ".py" for path in [getcwd_or_home()] + sys.path: fname = osp.join(path, filename) if osp.isfile(fname): return fname elif osp.isfile(fname + ".py"): return fname + ".py" elif osp.isfile(fname + ".pyw"): return fname + ".pyw" return filename class Interpreter(InteractiveConsole, threading.Thread): """Interpreter, executed in a separate thread""" p1 = ">>> " p2 = "... " def __init__( self, namespace=None, exitfunc=None, Output=None, WidgetProxy=None, debug=False ): """ namespace: locals send to InteractiveConsole object commands: list of commands executed at startup """ InteractiveConsole.__init__(self, namespace) threading.Thread.__init__(self) self._id = None self.exit_flag = False self.debug = debug # Execution Status self.more = False if exitfunc is not None: atexit.register(exitfunc) self.namespace = self.locals self.namespace["__name__"] = "__main__" self.namespace["execfile"] = self.execfile self.namespace["runfile"] = self.runfile self.namespace["raw_input"] = self.raw_input_replacement self.namespace["help"] = self.help_replacement # Capture all interactive input/output self.initial_stdout = sys.stdout self.initial_stderr = sys.stderr self.initial_stdin = sys.stdin # Create communication pipes pr, pw = os.pipe() self.stdin_read = os.fdopen(pr, "r") self.stdin_write = os.fdopen(pw, "wb", 0) self.stdout_write = Output() self.stderr_write = Output() self.input_condition = threading.Condition() self.widget_proxy = WidgetProxy(self.input_condition) self.redirect_stds() # ------ Standard input/output def redirect_stds(self): """Redirects stds""" if not self.debug: sys.stdout = self.stdout_write sys.stderr = self.stderr_write sys.stdin = self.stdin_read def restore_stds(self): """Restore stds""" if not self.debug: sys.stdout = self.initial_stdout sys.stderr = self.initial_stderr sys.stdin = self.initial_stdin def raw_input_replacement(self, prompt=""): """For raw_input builtin function emulation""" self.widget_proxy.wait_input(prompt) self.input_condition.acquire() while not self.widget_proxy.data_available(): self.input_condition.wait() inp = self.widget_proxy.input_data self.input_condition.release() return inp def help_replacement(self, text=None, interactive=False): """For help builtin function emulation""" if text is not None and not interactive: return pydoc.help(text) elif text is None: pyver = "%d.%d" % (sys.version_info[0], sys.version_info[1]) self.write( """ Welcome to Python %s! This is the online help utility. If this is your first time using Python, you should definitely check out the tutorial on the Internet at https://www.python.org/about/gettingstarted/ Enter the name of any module, keyword, or topic to get help on writing Python programs and using Python modules. To quit this help utility and return to the interpreter, just type "quit". To get a list of available modules, keywords, or topics, type "modules", "keywords", or "topics". Each module also comes with a one-line summary of what it does; to list the modules whose summaries contain a given word such as "spam", type "modules spam". """ % pyver ) else: text = text.strip() try: eval("pydoc.help(%s)" % text) except (NameError, SyntaxError): print( "no Python documentation found for '%r'" % text ) # spyder: test-skip self.write(os.linesep) self.widget_proxy.new_prompt("help> ") inp = self.raw_input_replacement() if inp.strip(): self.help_replacement(inp, interactive=True) else: self.write( """ You are now leaving help and returning to the Python interpreter. If you want to ask for help on a particular object directly from the interpreter, you can type "help(object)". Executing "help('string')" has the same effect as typing a particular string at the help> prompt. """ ) def run_command(self, cmd, new_prompt=True): """Run command in interpreter""" if cmd == "exit()": self.exit_flag = True self.write("\n") return # -- Special commands type I # (transformed into commands executed in the interpreter) # ? command special_pattern = r"^%s (?:r\')?(?:u\')?\"?\'?([a-zA-Z0-9_\.]+)" run_match = re.match(special_pattern % "run", cmd) help_match = re.match(r"^([a-zA-Z0-9_\.]+)\?$", cmd) cd_match = re.match(r"^\!cd \"?\'?([a-zA-Z0-9_ \.]+)", cmd) if help_match: cmd = "help(%s)" % help_match.group(1) # run command elif run_match: filename = guess_filename(run_match.groups()[0]) cmd = "runfile('%s', args=None)" % remove_backslashes(filename) # !cd system command elif cd_match: cmd = 'import os; os.chdir(r"%s")' % cd_match.groups()[0].strip() # -- End of Special commands type I # -- Special commands type II # (don't need code execution in interpreter) xedit_match = re.match(special_pattern % "xedit", cmd) edit_match = re.match(special_pattern % "edit", cmd) clear_match = re.match(r"^clear ([a-zA-Z0-9_, ]+)", cmd) # (external) edit command if xedit_match: filename = guess_filename(xedit_match.groups()[0]) self.widget_proxy.edit(filename, external_editor=True) # local edit command elif edit_match: filename = guess_filename(edit_match.groups()[0]) if osp.isfile(filename): self.widget_proxy.edit(filename) else: self.stderr_write.write("No such file or directory: %s\n" % filename) # remove reference (equivalent to MATLAB's clear command) elif clear_match: varnames = clear_match.groups()[0].replace(" ", "").split(",") for varname in varnames: try: self.namespace.pop(varname) except KeyError: pass # Execute command elif cmd.startswith("!"): # System ! command pipe = run_shell_command(cmd[1:]) out = pipe.stdout.read() err = pipe.stderr.read() try: out = out.decode("cp437") except UnicodeDecodeError: out = out.decode(erros="ignore") try: err = err.decode("cp437") except UnicodeDecodeError: err = err.decode(erros="ignore") if err: self.stderr_write.write(err) if out: self.stdout_write.write(out) self.stdout_write.write("\n") self.more = False # -- End of Special commands type II else: # Command executed in the interpreter # self.widget_proxy.set_readonly(True) self.more = self.push(cmd) # self.widget_proxy.set_readonly(False) if new_prompt: self.widget_proxy.new_prompt(self.p2 if self.more else self.p1) if not self.more: self.resetbuffer() def run(self): """Wait for input and run it""" while not self.exit_flag: self.run_line() def run_line(self): """ :return: """ line = self.stdin_read.readline() if self.exit_flag: return # Remove last character which is always '\n': self.run_command(line[:-1]) def get_thread_id(self): """Return thread id""" if self._id is None: for thread_id, obj in list(threading._active.items()): if obj is self: self._id = thread_id return self._id def raise_keyboard_interrupt(self): """ :return: """ if self.isAlive(): ctypes.pythonapi.PyThreadState_SetAsyncExc( self.get_thread_id(), ctypes.py_object(KeyboardInterrupt) ) return True else: return False def closing(self): """Actions to be done before restarting this interpreter""" pass def execfile(self, filename): """Exec filename""" source = open(filename, "r").read() try: try: name = filename.encode("ascii") except UnicodeEncodeError: name = "" code = compile(source, name, "exec") except (OverflowError, SyntaxError): InteractiveConsole.showsyntaxerror(self, filename) else: self.runcode(code) def runfile(self, filename, args=None): """ Run filename args: command line arguments (string) """ if args is not None and not isinstance(args, str): raise TypeError("expected a character buffer object") self.namespace["__file__"] = filename sys.argv = [filename] if args is not None: for arg in args.split(): sys.argv.append(arg) self.execfile(filename) sys.argv = [""] self.namespace.pop("__file__") def eval(self, text): """ Evaluate text and return (obj, valid) where *obj* is the object represented by *text* and *valid* is True if object evaluation did not raise any exception """ assert isinstance(text, str) try: return eval(text, self.locals), True except: return None, False def is_defined(self, objtxt, force_import=False): """Return True if object is defined""" return isdefined(objtxt, force_import=force_import, namespace=self.locals) # =========================================================================== # InteractiveConsole API # =========================================================================== def push(self, line): """ Push a line of source text to the interpreter The line should not have a trailing newline; it may have internal newlines. The line is appended to a buffer and the interpreter’s runsource() method is called with the concatenated contents of the buffer as source. If this indicates that the command was executed or invalid, the buffer is reset; otherwise, the command is incomplete, and the buffer is left as it was after the line was appended. The return value is True if more input is required, False if the line was dealt with in some way (this is the same as runsource()). """ return InteractiveConsole.push(self, "#coding=utf-8\n" + line) def resetbuffer(self): """Remove any unhandled source text from the input buffer""" InteractiveConsole.resetbuffer(self) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/console/mixins.py0000666000000000000000000007402500000000000016443 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """Mix-in classes These classes were created to be able to provide Spyder's regular text and console widget features to an independant widget based on QTextEdit for the IPython console plugin. """ import os import os.path as osp import re import sre_constants import textwrap from xml.sax.saxutils import escape from qtpy.QtWidgets import ( QApplication, QToolTip, ) from qtpy.QtGui import ( QCursor, QTextCursor, QTextDocument, ) from qtpy.QtCore import Qt, QPoint, QRegularExpression from guidata.widgets.console.dochelpers import ( getargspecfromtext, getobj, getsignaturefromtext, ) from guidata import encoding from guidata.config import _ # Order is important: EOL_CHARS = (("\r\n", "nt"), ("\n", "posix"), ("\r", "mac")) def get_eol_chars(text): """Get text EOL characters""" for eol_chars, _os_name in EOL_CHARS: if text.find(eol_chars) > -1: return eol_chars def get_error_match(text): """Return error match""" import re return re.match(r' File "(.*)", line (\d*)', text) class BaseEditMixin(object): """ """ def __init__(self): self.eol_chars = None self.calltip_size = 600 # ------Line number area def get_linenumberarea_width(self): """Return line number area width""" # Implemented in CodeEditor, but needed for calltip/completion widgets return 0 # ------Calltips def _format_signature(self, text): formatted_lines = [] name = text.split("(")[0] rows = textwrap.wrap(text, width=50, subsequent_indent=" " * (len(name) + 1)) for r in rows: r = escape(r) # Escape most common html chars r = r.replace(" ", " ") for char in ["=", ",", "(", ")", "*", "**"]: r = r.replace( char, "" + char + "", ) formatted_lines.append(r) signature = "
".join(formatted_lines) return signature, rows def show_calltip( self, title, text, signature=False, color="#2D62FF", at_line=None, at_position=None, ): """Show calltip""" if text is None or len(text) == 0: return # Saving cursor position: if at_position is None: at_position = self.get_position("cursor") self.calltip_position = at_position # Preparing text: if signature: text, wrapped_textlines = self._format_signature(text) else: if isinstance(text, list): text = "\n ".join(text) text = text.replace("\n", "
") if len(text) > self.calltip_size: text = text[: self.calltip_size] + " ..." # Formatting text font = self.font() size = font.pointSize() family = font.family() format1 = "

" % ( family, size, color, ) format2 = "
" % ( family, size - 1 if size > 9 else size, ) tiptext = ( format1 + ("%s
" % title) + "
" + format2 + text + "
" ) # Showing tooltip at cursor position: cx, cy = self.get_coordinates("cursor") if at_line is not None: cx = 5 cursor = QTextCursor(self.document().findBlockByNumber(at_line - 1)) cy = self.cursorRect(cursor).top() point = self.mapToGlobal(QPoint(cx, cy)) point.setX(point.x() + self.get_linenumberarea_width()) point.setY(point.y() + font.pointSize() + 5) if signature: self.calltip_widget.show_tip(point, tiptext, wrapped_textlines) else: QToolTip.showText(point, tiptext) # ------EOL characters def set_eol_chars(self, text): """Set widget end-of-line (EOL) characters from text (analyzes text)""" eol_chars = get_eol_chars(text) is_document_modified = eol_chars is not None and self.eol_chars is not None self.eol_chars = eol_chars if is_document_modified: self.document().setModified(True) if self.sig_eol_chars_changed is not None: self.sig_eol_chars_changed.emit(eol_chars) def get_line_separator(self): """Return line separator based on current EOL mode""" if self.eol_chars is not None: return self.eol_chars else: return os.linesep def get_text_with_eol(self): """Same as 'toPlainText', replace '\n' by correct end-of-line characters""" utext = str(self.toPlainText()) lines = utext.splitlines() linesep = self.get_line_separator() txt = linesep.join(lines) if utext.endswith("\n"): txt += linesep return txt # ------Positions, coordinates (cursor, EOF, ...) def get_position(self, subject): """Get offset in character for the given subject from the start of text edit area""" cursor = self.textCursor() if subject == "cursor": pass elif subject == "sol": cursor.movePosition(QTextCursor.StartOfBlock) elif subject == "eol": cursor.movePosition(QTextCursor.EndOfBlock) elif subject == "eof": cursor.movePosition(QTextCursor.End) elif subject == "sof": cursor.movePosition(QTextCursor.Start) else: # Assuming that input argument was already a position return subject return cursor.position() def get_coordinates(self, position): """ :param position: :return: """ position = self.get_position(position) cursor = self.textCursor() cursor.setPosition(position) point = self.cursorRect(cursor).center() return point.x(), point.y() def get_cursor_line_column(self): """Return cursor (line, column) numbers""" cursor = self.textCursor() return cursor.blockNumber(), cursor.columnNumber() def get_cursor_line_number(self): """Return cursor line number""" return self.textCursor().blockNumber() + 1 def set_cursor_position(self, position): """Set cursor position""" position = self.get_position(position) cursor = self.textCursor() cursor.setPosition(position) self.setTextCursor(cursor) self.ensureCursorVisible() def move_cursor(self, chars=0): """Move cursor to left or right (unit: characters)""" direction = QTextCursor.Right if chars > 0 else QTextCursor.Left for _i in range(abs(chars)): self.moveCursor(direction, QTextCursor.MoveAnchor) def is_cursor_on_first_line(self): """Return True if cursor is on the first line""" cursor = self.textCursor() cursor.movePosition(QTextCursor.StartOfBlock) return cursor.atStart() def is_cursor_on_last_line(self): """Return True if cursor is on the last line""" cursor = self.textCursor() cursor.movePosition(QTextCursor.EndOfBlock) return cursor.atEnd() def is_cursor_at_end(self): """Return True if cursor is at the end of the text""" return self.textCursor().atEnd() def is_cursor_before(self, position, char_offset=0): """Return True if cursor is before *position*""" position = self.get_position(position) + char_offset cursor = self.textCursor() cursor.movePosition(QTextCursor.End) if position < cursor.position(): cursor.setPosition(position) return self.textCursor() < cursor def __move_cursor_anchor(self, what, direction, move_mode): assert what in ("character", "word", "line") if what == "character": if direction == "left": self.moveCursor(QTextCursor.PreviousCharacter, move_mode) elif direction == "right": self.moveCursor(QTextCursor.NextCharacter, move_mode) elif what == "word": if direction == "left": self.moveCursor(QTextCursor.PreviousWord, move_mode) elif direction == "right": self.moveCursor(QTextCursor.NextWord, move_mode) elif what == "line": if direction == "down": self.moveCursor(QTextCursor.NextBlock, move_mode) elif direction == "up": self.moveCursor(QTextCursor.PreviousBlock, move_mode) def move_cursor_to_next(self, what="word", direction="left"): """ Move cursor to next *what* ('word' or 'character') toward *direction* ('left' or 'right') """ self.__move_cursor_anchor(what, direction, QTextCursor.MoveAnchor) # ------Selection def clear_selection(self): """Clear current selection""" cursor = self.textCursor() cursor.clearSelection() self.setTextCursor(cursor) def extend_selection_to_next(self, what="word", direction="left"): """ Extend selection to next *what* ('word' or 'character') toward *direction* ('left' or 'right') """ self.__move_cursor_anchor(what, direction, QTextCursor.KeepAnchor) # ------Text: get, set, ... def __select_text(self, position_from, position_to): position_from = self.get_position(position_from) position_to = self.get_position(position_to) cursor = self.textCursor() cursor.setPosition(position_from) cursor.setPosition(position_to, QTextCursor.KeepAnchor) return cursor def get_text_line(self, line_nb): """Return text line at line number *line_nb*""" # Taking into account the case when a file ends in an empty line, # since splitlines doesn't return that line as the last element # TODO: Make this function more efficient try: return str(self.toPlainText()).splitlines()[line_nb] except IndexError: return self.get_line_separator() def get_text(self, position_from, position_to): """ Return text between *position_from* and *position_to* Positions may be positions or 'sol', 'eol', 'sof', 'eof' or 'cursor' """ cursor = self.__select_text(position_from, position_to) text = str(cursor.selectedText()) all_text = position_from == "sof" and position_to == "eof" if text and not all_text: while text.endswith("\n"): text = text[:-1] while text.endswith(u"\u2029"): text = text[:-1] return text def get_character(self, position, offset=0): """Return character at *position* with the given offset.""" position = self.get_position(position) + offset cursor = self.textCursor() cursor.movePosition(QTextCursor.End) if position < cursor.position(): cursor.setPosition(position) cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor) return str(cursor.selectedText()) else: return "" def insert_text(self, text): """Insert text at cursor position""" if not self.isReadOnly(): self.textCursor().insertText(text) def replace_text(self, position_from, position_to, text): """ :param position_from: :param position_to: :param text: """ cursor = self.__select_text(position_from, position_to) cursor.removeSelectedText() cursor.insertText(text) def remove_text(self, position_from, position_to): """ :param position_from: :param position_to: """ cursor = self.__select_text(position_from, position_to) cursor.removeSelectedText() def get_current_word(self): """Return current word, i.e. word at cursor position""" cursor = self.textCursor() if cursor.hasSelection(): # Removes the selection and moves the cursor to the left side # of the selection: this is required to be able to properly # select the whole word under cursor (otherwise, the same word is # not selected when the cursor is at the right side of it): cursor.setPosition(min([cursor.selectionStart(), cursor.selectionEnd()])) else: # Checks if the first character to the right is a white space # and if not, moves the cursor one word to the left (otherwise, # if the character to the left do not match the "word regexp" # (see below), the word to the left of the cursor won't be # selected), but only if the first character to the left is not a # white space too. def is_space(move): """ :param move: :return: """ curs = self.textCursor() curs.movePosition(move, QTextCursor.KeepAnchor) return not str(curs.selectedText()).strip() if is_space(QTextCursor.NextCharacter): if is_space(QTextCursor.PreviousCharacter): return cursor.movePosition(QTextCursor.WordLeft) cursor.select(QTextCursor.WordUnderCursor) text = str(cursor.selectedText()) # find a valid python variable name match = re.findall(r"([^\d\W]\w*)", text, re.UNICODE) if match: return match[0] def get_current_line(self): """Return current line's text""" cursor = self.textCursor() cursor.select(QTextCursor.BlockUnderCursor) return str(cursor.selectedText()) def get_current_line_to_cursor(self): """Return text from prompt to cursor""" return self.get_text(self.current_prompt_pos, "cursor") def get_line_number_at(self, coordinates): """Return line number at *coordinates* (QPoint)""" cursor = self.cursorForPosition(coordinates) return cursor.blockNumber() - 1 def get_line_at(self, coordinates): """Return line at *coordinates* (QPoint)""" cursor = self.cursorForPosition(coordinates) cursor.select(QTextCursor.BlockUnderCursor) return str(cursor.selectedText()).replace(u"\u2029", "") def get_word_at(self, coordinates): """Return word at *coordinates* (QPoint)""" cursor = self.cursorForPosition(coordinates) cursor.select(QTextCursor.WordUnderCursor) return str(cursor.selectedText()) def get_block_indentation(self, block_nb): """Return line indentation (character number)""" text = str(self.document().findBlockByNumber(block_nb).text()) text = text.replace("\t", " " * self.tab_stop_width_spaces) return len(text) - len(text.lstrip()) def get_selection_bounds(self): """Return selection bounds (block numbers)""" cursor = self.textCursor() start, end = cursor.selectionStart(), cursor.selectionEnd() block_start = self.document().findBlock(start) block_end = self.document().findBlock(end) return sorted([block_start.blockNumber(), block_end.blockNumber()]) # ------Text selection def has_selected_text(self): """Returns True if some text is selected""" return bool(str(self.textCursor().selectedText())) def get_selected_text(self): """ Return text selected by current text cursor, converted in unicode Replace the unicode line separator character ``\u2029`` by the line separator characters returned by :py:meth:`get_line_separator` """ return str(self.textCursor().selectedText()).replace( u"\u2029", self.get_line_separator() ) def remove_selected_text(self): """Delete selected text""" self.textCursor().removeSelectedText() def replace(self, text, pattern=None): """Replace selected text by *text* If *pattern* is not None, replacing selected text using regular expression text substitution""" cursor = self.textCursor() cursor.beginEditBlock() if pattern is not None: seltxt = str(cursor.selectedText()) cursor.removeSelectedText() if pattern is not None: text = re.sub(str(pattern), str(text), str(seltxt)) cursor.insertText(text) cursor.endEditBlock() # ------Find/replace def find_multiline_pattern(self, regexp, cursor, findflag): """Reimplement QTextDocument's find method Add support for *multiline* regular expressions""" pattern = str(regexp.pattern()) text = str(self.toPlainText()) try: regobj = re.compile(pattern) except sre_constants.error: return if findflag & QTextDocument.FindBackward: # Find backward offset = min([cursor.selectionEnd(), cursor.selectionStart()]) text = text[:offset] matches = [_m for _m in regobj.finditer(text, 0, offset)] if matches: match = matches[-1] else: return else: # Find forward offset = max([cursor.selectionEnd(), cursor.selectionStart()]) match = regobj.search(text, offset) if match: pos1, pos2 = match.span() fcursor = self.textCursor() fcursor.setPosition(pos1) fcursor.setPosition(pos2, QTextCursor.KeepAnchor) return fcursor def find_text( self, text, changed=True, forward=True, case=False, words=False, regexp=False ): """Find text""" cursor = self.textCursor() findflag = QTextDocument.FindFlag() if not forward: findflag = findflag | QTextDocument.FindBackward if case: findflag = findflag | QTextDocument.FindCaseSensitively moves = [QTextCursor.NoMove] if forward: moves += [QTextCursor.NextWord, QTextCursor.Start] if changed: if str(cursor.selectedText()): new_position = min([cursor.selectionStart(), cursor.selectionEnd()]) cursor.setPosition(new_position) else: cursor.movePosition(QTextCursor.PreviousWord) else: moves += [QTextCursor.End] if regexp: text = str(text) else: text = re.escape(str(text)) pattern = QRegularExpression(u"\\b{}\\b".format(text) if words else text) if case: pattern.setPatternOptions(QRegularExpression.CaseInsensitiveOption) for move in moves: cursor.movePosition(move) if regexp and "\\n" in text: # Multiline regular expression found_cursor = self.find_multiline_pattern(pattern, cursor, findflag) else: # Single line find: using the QTextDocument's find function, # probably much more efficient than ours found_cursor = self.document().find(pattern, cursor, findflag) if found_cursor is not None and not found_cursor.isNull(): self.setTextCursor(found_cursor) return True return False def is_editor(self): """Needs to be overloaded in the codeeditor where it will be True""" return False def get_number_matches(self, pattern, source_text="", case=False, regexp=False): """Get the number of matches for the searched text.""" pattern = str(pattern) if not pattern: return 0 if not regexp: pattern = re.escape(pattern) if not source_text: source_text = str(self.toPlainText()) try: if case: regobj = re.compile(pattern) else: regobj = re.compile(pattern, re.IGNORECASE) except sre_constants.error: return None number_matches = 0 for match in regobj.finditer(source_text): number_matches += 1 return number_matches def get_match_number(self, pattern, case=False, regexp=False): """Get number of the match for the searched text.""" position = self.textCursor().position() source_text = self.get_text(position_from="sof", position_to=position) match_number = self.get_number_matches( pattern, source_text=source_text, case=case, regexp=regexp ) return match_number class TracebackLinksMixin(object): """ """ QT_CLASS = None go_to_error = None def __init__(self): self.__cursor_changed = False self.setMouseTracking(True) # ------Mouse events def mouseReleaseEvent(self, event): """Go to error""" self.QT_CLASS.mouseReleaseEvent(self, event) text = self.get_line_at(event.pos()) if get_error_match(text) and not self.has_selected_text(): if self.go_to_error is not None: self.go_to_error.emit(text) def mouseMoveEvent(self, event): """Show Pointing Hand Cursor on error messages""" text = self.get_line_at(event.pos()) if get_error_match(text): if not self.__cursor_changed: QApplication.setOverrideCursor(QCursor(Qt.PointingHandCursor)) self.__cursor_changed = True event.accept() return if self.__cursor_changed: QApplication.restoreOverrideCursor() self.__cursor_changed = False self.QT_CLASS.mouseMoveEvent(self, event) def leaveEvent(self, event): """If cursor has not been restored yet, do it now""" if self.__cursor_changed: QApplication.restoreOverrideCursor() self.__cursor_changed = False self.QT_CLASS.leaveEvent(self, event) class GetHelpMixin(object): """ """ def __init__(self): self.help = None self.help_enabled = False def set_help(self, help_plugin): """Set Help DockWidget reference""" self.help = help_plugin def set_help_enabled(self, state): """ :param state: """ self.help_enabled = state def inspect_current_object(self): """ """ text = "" text1 = self.get_text("sol", "cursor") tl1 = re.findall(r"([a-zA-Z_]+[0-9a-zA-Z_\.]*)", text1) if tl1 and text1.endswith(tl1[-1]): text += tl1[-1] text2 = self.get_text("cursor", "eol") tl2 = re.findall(r"([0-9a-zA-Z_\.]+[0-9a-zA-Z_\.]*)", text2) if tl2 and text2.startswith(tl2[0]): text += tl2[0] if text: self.show_object_info(text, force=True) def show_object_info(self, text, call=False, force=False): """Show signature calltip and/or docstring in the Help plugin""" text = str(text) # Show docstring help_enabled = self.help_enabled or force if force and self.help is not None: self.help.dockwidget.setVisible(True) self.help.dockwidget.raise_() if ( help_enabled and (self.help is not None) and (self.help.dockwidget.isVisible()) ): # Help widget exists and is visible if hasattr(self, "get_doc"): self.help.set_shell(self) else: self.help.set_shell(self.parent()) self.help.set_object_text(text, ignore_unknown=False) self.setFocus() # if help was not at top level, raising it to # top will automatically give it focus because of # the visibility_changed signal, so we must give # focus back to shell # Show calltip if call and self.calltips: # Display argument list if this is a function call iscallable = self.iscallable(text) if iscallable is not None: if iscallable: arglist = self.get_arglist(text) name = text.split(".")[-1] argspec = signature = "" if isinstance(arglist, bool): arglist = [] if arglist: argspec = "(" + "".join(arglist) + ")" else: doc = self.get__doc__(text) if doc is not None: # This covers cases like np.abs, whose docstring is # the same as np.absolute and because of that a # proper signature can't be obtained correctly argspec = getargspecfromtext(doc) if not argspec: signature = getsignaturefromtext(doc, name) if argspec or signature: if argspec: tiptext = name + argspec else: tiptext = signature self.show_calltip( _("Arguments"), tiptext, signature=True, color="#2D62FF" ) def get_last_obj(self, last=False): """ Return the last valid object on the current line """ return getobj(self.get_current_line_to_cursor(), last=last) class SaveHistoryMixin(object): """ """ INITHISTORY = None SEPARATOR = None HISTORY_FILENAMES = [] append_to_history = None def __init__(self, history_filename=""): self.history_filename = history_filename self.create_history_filename() def create_history_filename(self): """Create history_filename with INITHISTORY if it doesn't exist.""" if self.history_filename and not osp.isfile(self.history_filename): try: encoding.writelines(self.INITHISTORY, self.history_filename) except EnvironmentError: pass def add_to_history(self, command): """Add command to history""" command = str(command) if command in ["", "\n"] or command.startswith("Traceback"): return if command.endswith("\n"): command = command[:-1] self.histidx = None if len(self.history) > 0 and self.history[-1] == command: return self.history.append(command) text = os.linesep + command # When the first entry will be written in history file, # the separator will be append first: if self.history_filename not in self.HISTORY_FILENAMES: self.HISTORY_FILENAMES.append(self.history_filename) text = self.SEPARATOR + text # Needed to prevent errors when writing history to disk # See issue 6431 try: encoding.write(text, self.history_filename, mode="ab") except EnvironmentError: pass if self.append_to_history is not None: self.append_to_history.emit(self.history_filename, text) class BrowseHistoryMixin(object): """ """ def __init__(self): self.history = [] self.histidx = None self.hist_wholeline = False def clear_line(self): """Clear current line (without clearing console prompt)""" self.remove_text(self.current_prompt_pos, "eof") def browse_history(self, backward): """Browse history""" if self.is_cursor_before("eol") and self.hist_wholeline: self.hist_wholeline = False tocursor = self.get_current_line_to_cursor() text, self.histidx = self.find_in_history(tocursor, self.histidx, backward) if text is not None: if self.hist_wholeline: self.clear_line() self.insert_text(text) else: cursor_position = self.get_position("cursor") # Removing text from cursor to the end of the line self.remove_text("cursor", "eol") # Inserting history text self.insert_text(text) self.set_cursor_position(cursor_position) def find_in_history(self, tocursor, start_idx, backward): """Find text 'tocursor' in history, from index 'start_idx'""" if start_idx is None: start_idx = len(self.history) # Finding text in history step = -1 if backward else 1 idx = start_idx if len(tocursor) == 0 or self.hist_wholeline: idx += step if idx >= len(self.history) or len(self.history) == 0: return "", len(self.history) elif idx < 0: idx = 0 self.hist_wholeline = True return self.history[idx], idx else: for index in range(len(self.history)): idx = (start_idx + step * (index + 1)) % len(self.history) entry = self.history[idx] if entry.startswith(tocursor): return entry[len(tocursor) :], idx else: return None, start_idx def reset_search_pos(self): """Reset the position from which to search the history""" self.histidx = None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/console/shell.py0000666000000000000000000010045400000000000016237 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """Shell widgets: base, python and terminal""" # pylint: disable=C0103 # pylint: disable=R0903 # pylint: disable=R0911 # pylint: disable=R0201 import builtins import keyword import locale import os import os.path as osp import re import sys import time from qtpy.QtWidgets import ( QApplication, QToolTip, QMenu, ) from qtpy.QtGui import ( QTextCursor, QTextCharFormat, QTextCursor, QKeyEvent, ) from qtpy.QtCore import ( QCoreApplication, Qt, QTimer, Signal, Slot, Property, ) from qtpy.compat import getsavefilename from guidata.widgets.console.mixins import ( BrowseHistoryMixin, GetHelpMixin, SaveHistoryMixin, TracebackLinksMixin, ) from guidata.widgets.console.base import ConsoleBaseWidget from guidata.configtools import get_icon from guidata import encoding from guidata.qthelpers import ( add_actions, create_action, keybinding, ) from guidata.config import CONF, _ DEBUG = False STDERR = sys.stderr def tuple2keyevent(past_event): """Convert tuple into a QKeyEvent instance""" return QKeyEvent(*past_event) def restore_keyevent(event): """ :param event: :return: """ if isinstance(event, tuple): _, key, modifiers, text, _, _ = event event = tuple2keyevent(event) else: text = event.text() modifiers = event.modifiers() key = event.key() ctrl = modifiers & Qt.ControlModifier shift = modifiers & Qt.ShiftModifier return event, text, key, ctrl, shift class ShellBaseWidget(ConsoleBaseWidget, SaveHistoryMixin, BrowseHistoryMixin): """ Shell base widget """ redirect_stdio = Signal(bool) sig_keyboard_interrupt = Signal() execute = Signal(str) append_to_history = Signal(str, str) def __init__(self, parent, profile=False, initial_message=None): """ parent : specifies the parent widget """ ConsoleBaseWidget.__init__(self, parent) SaveHistoryMixin.__init__(self) BrowseHistoryMixin.__init__(self) self.historylog_filename = "history.log" # Prompt position: tuple (line, index) self.current_prompt_pos = None self.new_input_line = True # Context menu self.menu = None self.setup_context_menu() # Simple profiling test self.profile = profile # Buffer to increase performance of write/flush operations self.__buffer = [] if initial_message: self.__buffer.append(initial_message) self.__timestamp = 0.0 self.__flushtimer = QTimer(self) self.__flushtimer.setSingleShot(True) self.__flushtimer.timeout.connect(self.flush) # Give focus to widget self.setFocus() # Cursor width self.setCursorWidth(CONF.get("internal_console", "cursor/width")) def toggle_wrap_mode(self, enable): """Enable/disable wrap mode""" self.set_wrap_mode("character" if enable else None) def set_font(self, font): """Set shell styles font""" self.setFont(font) self.set_pythonshell_font(font) cursor = self.textCursor() cursor.select(QTextCursor.Document) charformat = QTextCharFormat() charformat.setFontFamily(font.family()) charformat.setFontPointSize(font.pointSize()) cursor.mergeCharFormat(charformat) # ------ Context menu def setup_context_menu(self): """Setup shell context menu""" self.menu = QMenu(self) self.cut_action = create_action( self, _("Cut"), shortcut=keybinding("Cut"), icon=get_icon("editcut.png"), triggered=self.cut, ) self.copy_action = create_action( self, _("Copy"), shortcut=keybinding("Copy"), icon=get_icon("editcopy.png"), triggered=self.copy, ) paste_action = create_action( self, _("Paste"), shortcut=keybinding("Paste"), icon=get_icon("editpaste.png"), triggered=self.paste, ) save_action = create_action( self, _("Save history log..."), icon=get_icon("filesave.png"), tip=_( "Save current history log (i.e. all " "inputs and outputs) in a text file" ), triggered=self.save_historylog, ) self.delete_action = create_action( self, _("Delete"), shortcut=keybinding("Delete"), icon=get_icon("editdelete.png"), triggered=self.delete, ) selectall_action = create_action( self, _("Select All"), shortcut=keybinding("SelectAll"), icon=get_icon("selectall.png"), triggered=self.selectAll, ) add_actions( self.menu, ( self.cut_action, self.copy_action, paste_action, self.delete_action, None, selectall_action, None, save_action, ), ) def contextMenuEvent(self, event): """Reimplement Qt method""" state = self.has_selected_text() self.copy_action.setEnabled(state) self.cut_action.setEnabled(state) self.delete_action.setEnabled(state) self.menu.popup(event.globalPos()) event.accept() # ------ Input buffer def get_current_line_from_cursor(self): """ :return: """ return self.get_text("cursor", "eof") def _select_input(self): """Select current line (without selecting console prompt)""" line, index = self.get_position("eof") if self.current_prompt_pos is None: pline, pindex = line, index else: pline, pindex = self.current_prompt_pos self.setSelection(pline, pindex, line, index) @Slot() def clear_terminal(self): """ Clear terminal window Child classes reimplement this method to write prompt """ self.clear() # The buffer being edited def _set_input_buffer(self, text): """Set input buffer""" if self.current_prompt_pos is not None: self.replace_text(self.current_prompt_pos, "eol", text) else: self.insert(text) self.set_cursor_position("eof") def _get_input_buffer(self): """Return input buffer""" input_buffer = "" if self.current_prompt_pos is not None: input_buffer = self.get_text(self.current_prompt_pos, "eol") input_buffer = input_buffer.replace(os.linesep, "\n") return input_buffer input_buffer = Property("QString", _get_input_buffer, _set_input_buffer) # ------ Prompt def new_prompt(self, prompt): """ Print a new prompt and save its (line, index) position """ if self.get_cursor_line_column()[1] != 0: self.write("\n") self.write(prompt, prompt=True) # now we update our cursor giving end of prompt self.current_prompt_pos = self.get_position("cursor") self.ensureCursorVisible() self.new_input_line = False def check_selection(self): """ Check if selected text is r/w, otherwise remove read-only parts of selection """ if self.current_prompt_pos is None: self.set_cursor_position("eof") else: self.truncate_selection(self.current_prompt_pos) # ------ Copy / Keyboard interrupt @Slot() def copy(self): """Copy text to clipboard... or keyboard interrupt""" if self.has_selected_text(): ConsoleBaseWidget.copy(self) elif not sys.platform == "darwin": self.interrupt() def interrupt(self): """Keyboard interrupt""" self.sig_keyboard_interrupt.emit() @Slot() def cut(self): """Cut text""" self.check_selection() if self.has_selected_text(): ConsoleBaseWidget.cut(self) @Slot() def delete(self): """Remove selected text""" self.check_selection() if self.has_selected_text(): ConsoleBaseWidget.remove_selected_text(self) @Slot() def save_historylog(self): """Save current history log (all text in console)""" title = _("Save history log") self.redirect_stdio.emit(False) filename, _selfilter = getsavefilename( self, title, self.historylog_filename, "%s (*.log)" % _("History logs") ) self.redirect_stdio.emit(True) if filename: filename = osp.normpath(filename) try: encoding.write(str(self.get_text_with_eol()), filename) self.historylog_filename = filename except EnvironmentError: pass # ------ Basic keypress event handler def on_enter(self, command): """on_enter""" self.execute_command(command) def execute_command(self, command): """ :param command: """ self.execute.emit(command) self.add_to_history(command) self.new_input_line = True def on_new_line(self): """On new input line""" self.set_cursor_position("eof") self.current_prompt_pos = self.get_position("cursor") self.new_input_line = False @Slot() def paste(self): """Reimplemented slot to handle multiline paste action""" if self.new_input_line: self.on_new_line() ConsoleBaseWidget.paste(self) def keyPressEvent(self, event): """ Reimplement Qt Method Basic keypress event handler (reimplemented in InternalShell to add more sophisticated features) """ if self.preprocess_keyevent(event): # Event was accepted in self.preprocess_keyevent return self.postprocess_keyevent(event) def preprocess_keyevent(self, event): """Pre-process keypress event: return True if event is accepted, false otherwise""" # Copy must be done first to be able to copy read-only text parts # (otherwise, right below, we would remove selection # if not on current line) ctrl = event.modifiers() & Qt.ControlModifier meta = event.modifiers() & Qt.MetaModifier # meta=ctrl in OSX if event.key() == Qt.Key_C and ( (Qt.MetaModifier | Qt.ControlModifier) & event.modifiers() ): if meta and sys.platform == "darwin": self.interrupt() elif ctrl: self.copy() event.accept() return True if self.new_input_line and ( len(event.text()) or event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right) ): self.on_new_line() return False def postprocess_keyevent(self, event): """Post-process keypress event: in InternalShell, this is method is called when shell is ready""" event, text, key, ctrl, shift = restore_keyevent(event) # Is cursor on the last line? and after prompt? if len(text): # XXX: Shouldn't it be: `if len(unicode(text).strip(os.linesep))` ? if self.has_selected_text(): self.check_selection() self.restrict_cursor_position(self.current_prompt_pos, "eof") cursor_position = self.get_position("cursor") if key in (Qt.Key_Return, Qt.Key_Enter): if self.is_cursor_on_last_line(): self._key_enter() # add and run selection else: self.insert_text(self.get_selected_text(), at_end=True) elif key == Qt.Key_Insert and not shift and not ctrl: self.setOverwriteMode(not self.overwriteMode()) elif key == Qt.Key_Delete: if self.has_selected_text(): self.check_selection() self.remove_selected_text() elif self.is_cursor_on_last_line(): self.stdkey_clear() elif key == Qt.Key_Backspace: self._key_backspace(cursor_position) elif key == Qt.Key_Tab: self._key_tab() elif key == Qt.Key_Space and ctrl: self._key_ctrl_space() elif key == Qt.Key_Left: if self.current_prompt_pos == cursor_position: # Avoid moving cursor on prompt return method = ( self.extend_selection_to_next if shift else self.move_cursor_to_next ) method("word" if ctrl else "character", direction="left") elif key == Qt.Key_Right: if self.is_cursor_at_end(): return method = ( self.extend_selection_to_next if shift else self.move_cursor_to_next ) method("word" if ctrl else "character", direction="right") elif (key == Qt.Key_Home) or ((key == Qt.Key_Up) and ctrl): self._key_home(shift, ctrl) elif (key == Qt.Key_End) or ((key == Qt.Key_Down) and ctrl): self._key_end(shift, ctrl) elif key == Qt.Key_Up: if not self.is_cursor_on_last_line(): self.set_cursor_position("eof") y_cursor = self.get_coordinates(cursor_position)[1] y_prompt = self.get_coordinates(self.current_prompt_pos)[1] if y_cursor > y_prompt: self.stdkey_up(shift) else: self.browse_history(backward=True) elif key == Qt.Key_Down: if not self.is_cursor_on_last_line(): self.set_cursor_position("eof") y_cursor = self.get_coordinates(cursor_position)[1] y_end = self.get_coordinates("eol")[1] if y_cursor < y_end: self.stdkey_down(shift) else: self.browse_history(backward=False) elif key in (Qt.Key_PageUp, Qt.Key_PageDown): # XXX: Find a way to do this programmatically instead of calling # widget keyhandler (this won't work if the *event* is coming from # the event queue - i.e. if the busy buffer is ever implemented) ConsoleBaseWidget.keyPressEvent(self, event) elif key == Qt.Key_Escape and shift: self.clear_line() elif key == Qt.Key_Escape: self._key_escape() elif key == Qt.Key_L and ctrl: self.clear_terminal() elif key == Qt.Key_V and ctrl: self.paste() elif key == Qt.Key_X and ctrl: self.cut() elif key == Qt.Key_Z and ctrl: self.undo() elif key == Qt.Key_Y and ctrl: self.redo() elif key == Qt.Key_A and ctrl: self.selectAll() elif key == Qt.Key_Question and not self.has_selected_text(): self._key_question(text) elif key == Qt.Key_ParenLeft and not self.has_selected_text(): self._key_parenleft(text) elif key == Qt.Key_Period and not self.has_selected_text(): self._key_period(text) elif len(text) and not self.isReadOnly(): self.hist_wholeline = False self.insert_text(text) self._key_other(text) else: # Let the parent widget handle the key press event ConsoleBaseWidget.keyPressEvent(self, event) # ------ Key handlers def _key_enter(self): command = self.input_buffer self.insert_text("\n", at_end=True) self.on_enter(command) self.flush() def _key_other(self, text): raise NotImplementedError def _key_backspace(self, cursor_position): raise NotImplementedError def _key_tab(self): raise NotImplementedError def _key_ctrl_space(self): raise NotImplementedError def _key_home(self, shift, ctrl): if self.is_cursor_on_last_line(): self.stdkey_home(shift, ctrl, self.current_prompt_pos) def _key_end(self, shift, ctrl): if self.is_cursor_on_last_line(): self.stdkey_end(shift, ctrl) def _key_pageup(self): raise NotImplementedError def _key_pagedown(self): raise NotImplementedError def _key_escape(self): raise NotImplementedError def _key_question(self, text): raise NotImplementedError def _key_parenleft(self, text): raise NotImplementedError def _key_period(self, text): raise NotImplementedError # ------ History Management def load_history(self): """Load history from a .py file in user home directory""" if osp.isfile(self.history_filename): rawhistory, _ = encoding.readlines(self.history_filename) rawhistory = [line.replace("\n", "") for line in rawhistory] if rawhistory[1] != self.INITHISTORY[1]: rawhistory[1] = self.INITHISTORY[1] else: rawhistory = self.INITHISTORY history = [line for line in rawhistory if line and not line.startswith("#")] # Truncating history to X entries: while len(history) >= CONF.get("historylog", "max_entries"): del history[0] while rawhistory[0].startswith("#"): del rawhistory[0] del rawhistory[0] # Saving truncated history: try: encoding.writelines(rawhistory, self.history_filename) except EnvironmentError: pass return history # ------ Simulation standards input/output def write_error(self, text): """Simulate stderr""" self.flush() self.write(text, flush=True, error=True) if DEBUG: STDERR.write(text) def write(self, text, flush=False, error=False, prompt=False): """Simulate stdout and stderr""" if prompt: self.flush() if not isinstance(text, str): # This test is useful to discriminate QStrings from decoded str text = str(text) self.__buffer.append(text) ts = time.time() if flush or prompt: self.flush(error=error, prompt=prompt) elif ts - self.__timestamp > 0.05: self.flush(error=error) self.__timestamp = ts # Timer to flush strings cached by last write() operation in series self.__flushtimer.start(50) def flush(self, error=False, prompt=False): """Flush buffer, write text to console""" # Fix for Issue 2452 try: text = "".join(self.__buffer) except TypeError: text = b"".join(self.__buffer) try: text = text.decode(locale.getdefaultlocale()[1]) except: pass self.__buffer = [] self.insert_text(text, at_end=True, error=error, prompt=prompt) QCoreApplication.processEvents() self.repaint() # Clear input buffer: self.new_input_line = True # ------ Text Insertion def insert_text(self, text, at_end=False, error=False, prompt=False): """ Insert text at the current cursor position or at the end of the command line """ if at_end: # Insert text at the end of the command line self.append_text_to_shell(text, error, prompt) else: # Insert text at current cursor position ConsoleBaseWidget.insert_text(self, text) # ------ Re-implemented Qt Methods def focusNextPrevChild(self, next): """ Reimplemented to stop Tab moving to the next window """ if next: return False return ConsoleBaseWidget.focusNextPrevChild(self, next) # ------ Drag and drop def dragEnterEvent(self, event): """Drag and Drop - Enter event""" event.setAccepted(event.mimeData().hasFormat("text/plain")) def dragMoveEvent(self, event): """Drag and Drop - Move event""" if event.mimeData().hasFormat("text/plain"): event.setDropAction(Qt.MoveAction) event.accept() else: event.ignore() def dropEvent(self, event): """Drag and Drop - Drop event""" if event.mimeData().hasFormat("text/plain"): text = str(event.mimeData().text()) if self.new_input_line: self.on_new_line() self.insert_text(text, at_end=True) self.setFocus() event.setDropAction(Qt.MoveAction) event.accept() else: event.ignore() def drop_pathlist(self, pathlist): """Drop path list""" raise NotImplementedError # Example how to debug complex interclass call chains: # # from spyder.utils.debug import log_methods_calls # log_methods_calls('log.log', ShellBaseWidget) class PythonShellWidget(TracebackLinksMixin, ShellBaseWidget, GetHelpMixin): """Python shell widget""" QT_CLASS = ShellBaseWidget INITHISTORY = [ "# -*- coding: utf-8 -*-", "# *** Spyder Python Console History Log ***", ] SEPARATOR = "%s##---(%s)---" % (os.linesep * 2, time.ctime()) go_to_error = Signal(str) def __init__(self, parent, profile=False, initial_message=None): ShellBaseWidget.__init__(self, parent, profile, initial_message) TracebackLinksMixin.__init__(self) GetHelpMixin.__init__(self) # ------ Context menu def setup_context_menu(self): """Reimplements ShellBaseWidget method""" ShellBaseWidget.setup_context_menu(self) self.copy_without_prompts_action = create_action( self, _("Copy without prompts"), icon=get_icon("copywop.png"), triggered=self.copy_without_prompts, ) clear_line_action = create_action( self, _("Clear line"), icon=get_icon("editdelete.png"), tip=_("Clear line"), triggered=self.clear_line, ) clear_action = create_action( self, _("Clear shell"), icon=get_icon("editclear.png"), tip=_("Clear shell contents " "('cls' command)"), triggered=self.clear_terminal, ) add_actions( self.menu, (self.copy_without_prompts_action, clear_line_action, clear_action), ) def contextMenuEvent(self, event): """Reimplements ShellBaseWidget method""" state = self.has_selected_text() self.copy_without_prompts_action.setEnabled(state) ShellBaseWidget.contextMenuEvent(self, event) @Slot() def copy_without_prompts(self): """Copy text to clipboard without prompts""" text = self.get_selected_text() lines = text.split(os.linesep) for index, line in enumerate(lines): if line.startswith(">>> ") or line.startswith("... "): lines[index] = line[4:] text = os.linesep.join(lines) QApplication.clipboard().setText(text) # ------ Key handlers def postprocess_keyevent(self, event): """Process keypress event""" ShellBaseWidget.postprocess_keyevent(self, event) if QToolTip.isVisible(): _event, _text, key, _ctrl, _shift = restore_keyevent(event) self.hide_tooltip_if_necessary(key) def _key_other(self, text): """1 character key""" if self.is_completion_widget_visible(): self.completion_text += text def _key_backspace(self, cursor_position): """Action for Backspace key""" if self.has_selected_text(): self.check_selection() self.remove_selected_text() elif self.current_prompt_pos == cursor_position: # Avoid deleting prompt return elif self.is_cursor_on_last_line(): self.stdkey_backspace() if self.is_completion_widget_visible(): # Removing only last character because if there was a selection # the completion widget would have been canceled self.completion_text = self.completion_text[:-1] def _key_tab(self): """Action for TAB key""" if self.is_cursor_on_last_line(): empty_line = not self.get_current_line_to_cursor().strip() if empty_line: self.stdkey_tab() else: self.show_code_completion(automatic=False) def _key_ctrl_space(self): """Action for Ctrl+Space""" if not self.is_completion_widget_visible(): self.show_code_completion(automatic=False) def _key_pageup(self): """Action for PageUp key""" pass def _key_pagedown(self): """Action for PageDown key""" pass def _key_escape(self): """Action for ESCAPE key""" if self.is_completion_widget_visible(): self.hide_completion_widget() def _key_question(self, text): """Action for '?'""" if self.get_current_line_to_cursor(): last_obj = self.get_last_obj() if last_obj and not last_obj.isdigit(): self.show_object_info(last_obj) self.insert_text(text) # In case calltip and completion are shown at the same time: if self.is_completion_widget_visible(): self.completion_text += "?" def _key_parenleft(self, text): """Action for '('""" self.hide_completion_widget() if self.get_current_line_to_cursor(): last_obj = self.get_last_obj() if last_obj and not last_obj.isdigit(): self.insert_text(text) self.show_object_info(last_obj, call=True) return self.insert_text(text) def _key_period(self, text): """Action for '.'""" self.insert_text(text) if self.codecompletion_auto: # Enable auto-completion only if last token isn't a float last_obj = self.get_last_obj() if last_obj and not last_obj.isdigit(): self.show_code_completion(automatic=True) # ------ Paste def paste(self): """Reimplemented slot to handle multiline paste action""" text = str(QApplication.clipboard().text()) if len(text.splitlines()) > 1: # Multiline paste if self.new_input_line: self.on_new_line() self.remove_selected_text() # Remove selection, eventually end = self.get_current_line_from_cursor() lines = self.get_current_line_to_cursor() + text + end self.clear_line() self.execute_lines(lines) self.move_cursor(-len(end)) else: # Standard paste ShellBaseWidget.paste(self) # ------ Code Completion / Calltips # Methods implemented in child class: # (e.g. InternalShell) def get_dir(self, objtxt): """Return dir(object)""" raise NotImplementedError def get_module_completion(self, objtxt): """Return module completion list associated to object name""" pass def get_globals_keys(self): """Return shell globals() keys""" raise NotImplementedError def get_cdlistdir(self): """Return shell current directory list dir""" raise NotImplementedError def iscallable(self, objtxt): """Is object callable?""" raise NotImplementedError def get_arglist(self, objtxt): """Get func/method argument list""" raise NotImplementedError def get__doc__(self, objtxt): """Get object __doc__""" raise NotImplementedError def get_doc(self, objtxt): """Get object documentation dictionary""" raise NotImplementedError def get_source(self, objtxt): """Get object source""" raise NotImplementedError def is_defined(self, objtxt, force_import=False): """Return True if object is defined""" raise NotImplementedError def show_code_completion(self, automatic): """Display a completion list based on the current line""" # Note: unicode conversion is needed only for ExternalShellBase text = str(self.get_current_line_to_cursor()) last_obj = self.get_last_obj() if not text: return if text.startswith("import "): obj_list = self.get_module_completion(text) words = text.split(" ") if "," in words[-1]: words = words[-1].split(",") self.show_completion_list( obj_list, completion_text=words[-1], automatic=automatic ) return elif text.startswith("from "): obj_list = self.get_module_completion(text) if obj_list is None: return words = text.split(" ") if "(" in words[-1]: words = words[:-2] + words[-1].split("(") if "," in words[-1]: words = words[:-2] + words[-1].split(",") self.show_completion_list( obj_list, completion_text=words[-1], automatic=automatic ) return obj_dir = self.get_dir(last_obj) if last_obj and obj_dir and text.endswith("."): self.show_completion_list(obj_dir, automatic=automatic) return # Builtins and globals if ( not text.endswith(".") and last_obj and re.match(r"[a-zA-Z_0-9]*$", last_obj) ): b_k_g = dir(builtins) + self.get_globals_keys() + keyword.kwlist for objname in b_k_g: if objname.startswith(last_obj) and objname != last_obj: self.show_completion_list( b_k_g, completion_text=last_obj, automatic=automatic ) return else: return # Looking for an incomplete completion if last_obj is None: last_obj = text dot_pos = last_obj.rfind(".") if dot_pos != -1: if dot_pos == len(last_obj) - 1: completion_text = "" else: completion_text = last_obj[dot_pos + 1 :] last_obj = last_obj[:dot_pos] completions = self.get_dir(last_obj) if completions is not None: self.show_completion_list( completions, completion_text=completion_text, automatic=automatic ) return # Looking for ' or ": filename completion q_pos = max([text.rfind("'"), text.rfind('"')]) if q_pos != -1: completions = self.get_cdlistdir() if completions: self.show_completion_list( completions, completion_text=text[q_pos + 1 :], automatic=automatic ) return # ------ Drag'n Drop def drop_pathlist(self, pathlist): """Drop path list""" if pathlist: files = ["r'%s'" % path for path in pathlist] if len(files) == 1: text = files[0] else: text = "[" + ", ".join(files) + "]" if self.new_input_line: self.on_new_line() self.insert_text(text) self.setFocus() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/console/terminal.py0000666000000000000000000001000300000000000016731 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """Terminal emulation tools""" import os class ANSIEscapeCodeHandler(object): """ANSI Escape sequences handler""" if os.name == "nt": # Windows terminal colors: ANSI_COLORS = ( # Normal, Bright/Light ("#000000", "#808080"), # 0: black ("#800000", "#ff0000"), # 1: red ("#008000", "#00ff00"), # 2: green ("#808000", "#ffff00"), # 3: yellow ("#000080", "#0000ff"), # 4: blue ("#800080", "#ff00ff"), # 5: magenta ("#008080", "#00ffff"), # 6: cyan ("#c0c0c0", "#ffffff"), # 7: white ) elif os.name == "mac": # Terminal.app colors: ANSI_COLORS = ( # Normal, Bright/Light ("#000000", "#818383"), # 0: black ("#C23621", "#FC391F"), # 1: red ("#25BC24", "#25BC24"), # 2: green ("#ADAD27", "#EAEC23"), # 3: yellow ("#492EE1", "#5833FF"), # 4: blue ("#D338D3", "#F935F8"), # 5: magenta ("#33BBC8", "#14F0F0"), # 6: cyan ("#CBCCCD", "#E9EBEB"), # 7: white ) else: # xterm colors: ANSI_COLORS = ( # Normal, Bright/Light ("#000000", "#7F7F7F"), # 0: black ("#CD0000", "#ff0000"), # 1: red ("#00CD00", "#00ff00"), # 2: green ("#CDCD00", "#ffff00"), # 3: yellow ("#0000EE", "#5C5CFF"), # 4: blue ("#CD00CD", "#ff00ff"), # 5: magenta ("#00CDCD", "#00ffff"), # 6: cyan ("#E5E5E5", "#ffffff"), # 7: white ) def __init__(self): self.intensity = 0 self.italic = None self.bold = None self.underline = None self.foreground_color = None self.background_color = None self.default_foreground_color = 30 self.default_background_color = 47 def set_code(self, code): """ :param code: """ assert isinstance(code, int) if code == 0: # Reset all settings self.reset() elif code == 1: # Text color intensity self.intensity = 1 # The following line is commented because most terminals won't # change the font weight, against ANSI standard recommendation: # self.bold = True elif code == 3: # Italic on self.italic = True elif code == 4: # Underline simple self.underline = True elif code == 22: # Normal text color intensity self.intensity = 0 self.bold = False elif code == 23: # No italic self.italic = False elif code == 24: # No underline self.underline = False elif code >= 30 and code <= 37: # Text color self.foreground_color = code elif code == 39: # Default text color self.foreground_color = self.default_foreground_color elif code >= 40 and code <= 47: # Background color self.background_color = code elif code == 49: # Default background color self.background_color = self.default_background_color self.set_style() def set_style(self): """ Set font style with the following attributes: 'foreground_color', 'background_color', 'italic', 'bold' and 'underline' """ raise NotImplementedError def reset(self): """ """ self.current_format = None self.intensity = 0 self.italic = False self.bold = False self.underline = False self.foreground_color = None self.background_color = None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/dataframeeditor.py0000666000000000000000000007727600000000000016640 0ustar00# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2011-2012 Lambda Foundry, Inc. and PyData Development Team # Copyright (c) 2013 Jev Kuznetsov and contributors # Copyright (c) 2014- Spyder Project Contributors # # Distributed under the terms of the New BSD License # (BSD 3-clause; see NOTICE.txt in the Spyder root directory for details). # ----------------------------------------------------------------------------- """ guidata.widgets.dataframeeditor =============================== This package provides a DataFrameModel based on the class ArrayModel from array editor and the class DataFrameModel from the pandas project. Present in pandas.sandbox.qtpandas in v0.13.1. .. autoclass:: DataFrameEditor Originally based on pandas/sandbox/qtpandas.py of the `pandas project `_. The current version is qtpandas/models/DataFrameModel.py of the `QtPandas project `_. """ import io import numpy as np from pandas import DataFrame, DatetimeIndex, Series from guidata.configtools import get_font, get_icon from guidata.qthelpers import ( add_actions, create_action, keybinding, win32_fix_title_bar_background, ) from guidata.config import CONF, _ from qtpy.QtWidgets import ( QApplication, QCheckBox, QGridLayout, QHBoxLayout, QDialog, QHeaderView, QInputDialog, QLineEdit, QMenu, QMessageBox, QPushButton, QTableView, QShortcut, ) from qtpy.QtGui import ( QKeySequence, QColor, QCursor, ) from qtpy.QtCore import ( QAbstractTableModel, QModelIndex, Qt, Signal, Slot, ) from guidata.widgets.arrayeditor import get_idx_rect try: from pandas._libs.tslib import OutOfBoundsDatetime except ImportError: # For pandas version < 0.20 from pandas.tslib import OutOfBoundsDatetime # Supported Numbers and complex numbers REAL_NUMBER_TYPES = (float, int, np.int64, np.int32) COMPLEX_NUMBER_TYPES = (complex, np.complex64, np.complex128) # Used to convert bool intrance to false since bool('False') will return True _bool_false = ["false", "f", "0", "0.", "0.0", " "] # Default format for data frames with floats DEFAULT_FORMAT = "%.6g" # Limit at which dataframe is considered so large that it is loaded on demand LARGE_SIZE = 5e5 LARGE_NROWS = 1e5 LARGE_COLS = 60 # Background colours BACKGROUND_NUMBER_MINHUE = 0.66 # hue for largest number BACKGROUND_NUMBER_HUERANGE = 0.33 # (hue for smallest) minus (hue for largest) BACKGROUND_NUMBER_SATURATION = 0.7 BACKGROUND_NUMBER_VALUE = 1.0 BACKGROUND_NUMBER_ALPHA = 0.6 BACKGROUND_NONNUMBER_COLOR = Qt.lightGray BACKGROUND_INDEX_ALPHA = 0.8 BACKGROUND_STRING_ALPHA = 0.05 BACKGROUND_MISC_ALPHA = 0.3 def bool_false_check(value): """ Used to convert bool intrance to false since any string in bool('') will return True """ if value.lower() in _bool_false: value = "" return value def global_max(col_vals, index): """Returns the global maximum and minimum""" col_vals_without_None = [x for x in col_vals if x is not None] max_col, min_col = zip(*col_vals_without_None) return max(max_col), min(min_col) class DataFrameModel(QAbstractTableModel): """DataFrame Table Model""" ROWS_TO_LOAD = 500 COLS_TO_LOAD = 40 def __init__(self, dataFrame, format=DEFAULT_FORMAT, parent=None): QAbstractTableModel.__init__(self) self.dialog = parent self.df = dataFrame self.df_index = dataFrame.index.tolist() self.df_header = dataFrame.columns.tolist() self._format = format self.complex_intran = None self.display_error_idxs = [] self.total_rows = self.df.shape[0] self.total_cols = self.df.shape[1] size = self.total_rows * self.total_cols self.max_min_col = None if size < LARGE_SIZE: self.max_min_col_update() self.colum_avg_enabled = True self.bgcolor_enabled = True self.colum_avg(1) else: self.colum_avg_enabled = False self.bgcolor_enabled = False self.colum_avg(0) # Use paging when the total size, number of rows or number of # columns is too large if size > LARGE_SIZE: self.rows_loaded = self.ROWS_TO_LOAD self.cols_loaded = self.COLS_TO_LOAD else: if self.total_rows > LARGE_NROWS: self.rows_loaded = self.ROWS_TO_LOAD else: self.rows_loaded = self.total_rows if self.total_cols > LARGE_COLS: self.cols_loaded = self.COLS_TO_LOAD else: self.cols_loaded = self.total_cols def max_min_col_update(self): """ Determines the maximum and minimum number in each column. The result is a list whose k-th entry is [vmax, vmin], where vmax and vmin denote the maximum and minimum of the k-th column (ignoring NaN). This list is stored in self.max_min_col. If the k-th column has a non-numerical dtype, then the k-th entry is set to None. If the dtype is complex, then compute the maximum and minimum of the absolute values. If vmax equals vmin, then vmin is decreased by one. """ if self.df.shape[0] == 0: # If no rows to compute max/min then return return self.max_min_col = [] for dummy, col in self.df.items(): if col.dtype in REAL_NUMBER_TYPES + COMPLEX_NUMBER_TYPES: if col.dtype in REAL_NUMBER_TYPES: vmax = col.max(skipna=True) vmin = col.min(skipna=True) else: vmax = col.abs().max(skipna=True) vmin = col.abs().min(skipna=True) if vmax != vmin: max_min = [vmax, vmin] else: max_min = [vmax, vmin - 1] else: max_min = None self.max_min_col.append(max_min) def get_format(self): """Return current format""" # Avoid accessing the private attribute _format from outside return self._format def set_format(self, format): """Change display format""" self._format = format self.reset() def bgcolor(self, state): """Toggle backgroundcolor""" self.bgcolor_enabled = state > 0 self.reset() def colum_avg(self, state): """Toggle backgroundcolor""" self.colum_avg_enabled = state > 0 if self.colum_avg_enabled: self.return_max = lambda col_vals, index: col_vals[index] else: self.return_max = global_max self.reset() def headerData(self, section, orientation, role=Qt.DisplayRole): """Set header data""" if role != Qt.DisplayRole: return None if orientation == Qt.Horizontal: if section == 0: return "Index" elif type(self.df_header[section - 1]) in (bytes, str): # Don't perform any conversion on strings because it # leads to differences between the data present in # the dataframe and what is shown by Spyder return self.df_header[section - 1] else: return str(self.df_header[section - 1]) else: return None def get_bgcolor(self, index): """Background color depending on value""" column = index.column() if column == 0: color = QColor(BACKGROUND_NONNUMBER_COLOR) color.setAlphaF(BACKGROUND_INDEX_ALPHA) return color if not self.bgcolor_enabled: return value = self.get_value(index.row(), column - 1) if self.max_min_col[column - 1] is None: color = QColor(BACKGROUND_NONNUMBER_COLOR) if isinstance(value, str): color.setAlphaF(BACKGROUND_STRING_ALPHA) else: color.setAlphaF(BACKGROUND_MISC_ALPHA) else: if isinstance(value, COMPLEX_NUMBER_TYPES): color_func = abs else: color_func = float vmax, vmin = self.return_max(self.max_min_col, column - 1) hue = BACKGROUND_NUMBER_MINHUE + BACKGROUND_NUMBER_HUERANGE * ( vmax - color_func(value) ) / (vmax - vmin) hue = float(abs(hue)) if hue > 1: hue = 1 color = QColor.fromHsvF( hue, BACKGROUND_NUMBER_SATURATION, BACKGROUND_NUMBER_VALUE, BACKGROUND_NUMBER_ALPHA, ) return color def get_value(self, row, column): """Returns the value of the DataFrame""" # To increase the performance iat is used but that requires error # handling, so fallback uses iloc try: value = self.df.iat[row, column] except OutOfBoundsDatetime: value = self.df.iloc[:, column].astype(str).iat[row] except: value = self.df.iloc[row, column] return value def update_df_index(self): """ "Update the DataFrame index""" self.df_index = self.df.index.tolist() def data(self, index, role=Qt.DisplayRole): """Cell content""" if not index.isValid(): return None if role == Qt.DisplayRole or role == Qt.EditRole: column = index.column() row = index.row() if column == 0: df_idx = self.df_index[row] if type(df_idx) in (bytes, str): # Don't perform any conversion on strings # because it leads to differences between # the data present in the dataframe and # what is shown by Spyder return df_idx else: return str(df_idx) else: value = self.get_value(row, column - 1) if isinstance(value, float): try: return self._format % value except (ValueError, TypeError): # may happen if format = '%d' and value = NaN; # see issue 4139 return DEFAULT_FORMAT % value elif type(value) in (bytes, str): # Don't perform any conversion on strings # because it leads to differences between # the data present in the dataframe and # what is shown by Spyder return value else: try: return str(value) except Exception: self.display_error_idxs.append(index) return u"Display Error!" elif role == Qt.BackgroundColorRole: return self.get_bgcolor(index) elif role == Qt.FontRole: return get_font(CONF, "arrayeditor", "font") elif role == Qt.ToolTipRole: if index in self.display_error_idxs: return _( "It is not possible to display this value because\n" "an error ocurred while trying to do it" ) return None def sort(self, column, order=Qt.AscendingOrder): """Overriding sort method""" if self.complex_intran is not None: if self.complex_intran.any(axis=0).iloc[column - 1]: QMessageBox.critical( self.dialog, "Error", "TypeError error: no ordering " "relation is defined for complex numbers", ) return False try: ascending = order == Qt.AscendingOrder if column > 0: try: self.df.sort_values( by=self.df.columns[column - 1], ascending=ascending, inplace=True, kind="mergesort", ) except AttributeError: # for pandas version < 0.17 self.df.sort( columns=self.df.columns[column - 1], ascending=ascending, inplace=True, kind="mergesort", ) except ValueError as e: # Not possible to sort on duplicate columns #5225 QMessageBox.critical( self.dialog, "Error", "ValueError: %s" % str(e) ) except SystemError as e: # Not possible to sort on category dtypes #5361 QMessageBox.critical( self.dialog, "Error", "SystemError: %s" % str(e) ) self.update_df_index() else: self.df.sort_index(inplace=True, ascending=ascending) self.update_df_index() except TypeError as e: QMessageBox.critical(self.dialog, "Error", "TypeError error: %s" % str(e)) return False self.reset() return True def flags(self, index): """Set flags""" if index.column() == 0: return Qt.ItemIsEnabled | Qt.ItemIsSelectable return Qt.ItemFlags(QAbstractTableModel.flags(self, index) | Qt.ItemIsEditable) def setData(self, index, value, role=Qt.EditRole, change_type=None): """Cell content change""" column = index.column() row = index.row() if index in self.display_error_idxs: return False if change_type is not None: try: val = self.data(index, role=Qt.DisplayRole) if change_type is bool: val = bool_false_check(val) self.df.iloc[row, column - 1] = change_type(val) except ValueError: self.df.iloc[row, column - 1] = change_type("0") else: val = value current_value = self.get_value(row, column - 1) if isinstance(current_value, (bool, np.bool_)): val = bool_false_check(val) supported_types = (bool, np.bool_) + REAL_NUMBER_TYPES if isinstance(current_value, supported_types) or isinstance( current_value, str ): try: self.df.iloc[row, column - 1] = current_value.__class__(val) except (ValueError, OverflowError) as e: QMessageBox.critical( self.dialog, "Error", str(type(e).__name__) + ": " + str(e) ) return False else: QMessageBox.critical( self.dialog, "Error", "Editing dtype {0!s} not yet supported.".format( type(current_value).__name__ ), ) return False self.max_min_col_update() self.dataChanged.emit(index, index) return True def get_data(self): """Return data""" return self.df def rowCount(self, index=QModelIndex()): """DataFrame row number""" if self.total_rows <= self.rows_loaded: return self.total_rows else: return self.rows_loaded def can_fetch_more(self, rows=False, columns=False): """ :param rows: :param columns: :return: """ if rows: if self.total_rows > self.rows_loaded: return True else: return False if columns: if self.total_cols > self.cols_loaded: return True else: return False def fetch_more(self, rows=False, columns=False): """ :param rows: :param columns: """ if self.can_fetch_more(rows=rows): reminder = self.total_rows - self.rows_loaded items_to_fetch = min(reminder, self.ROWS_TO_LOAD) self.beginInsertRows( QModelIndex(), self.rows_loaded, self.rows_loaded + items_to_fetch - 1 ) self.rows_loaded += items_to_fetch self.endInsertRows() if self.can_fetch_more(columns=columns): reminder = self.total_cols - self.cols_loaded items_to_fetch = min(reminder, self.COLS_TO_LOAD) self.beginInsertColumns( QModelIndex(), self.cols_loaded, self.cols_loaded + items_to_fetch - 1 ) self.cols_loaded += items_to_fetch self.endInsertColumns() def columnCount(self, index=QModelIndex()): """DataFrame column number""" # This is done to implement series if len(self.df.shape) == 1: return 2 elif self.total_cols <= self.cols_loaded: return self.total_cols + 1 else: return self.cols_loaded + 1 def reset(self): """ """ self.beginResetModel() self.endResetModel() class FrozenTableView(QTableView): """This class implements a table with its first column frozen For more information please see: https://doc.qt.io/qt-5/qtwidgets-itemviews-frozencolumn-example.html""" def __init__(self, parent): """Constructor.""" QTableView.__init__(self, parent) self.parent = parent self.setModel(parent.model()) self.setFocusPolicy(Qt.NoFocus) self.verticalHeader().hide() try: self.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed) except: # support for qtpy<1.2.0 self.horizontalHeader().setResizeMode(QHeaderView.Fixed) parent.viewport().stackUnder(self) self.setSelectionModel(parent.selectionModel()) for col in range(1, parent.model().columnCount()): self.setColumnHidden(col, True) self.setColumnWidth(0, parent.columnWidth(0)) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.show() self.setVerticalScrollMode(1) def update_geometry(self): """Update the frozen column size when an update occurs in its parent table""" self.setGeometry( self.parent.verticalHeader().width() + self.parent.frameWidth(), self.parent.frameWidth(), self.parent.columnWidth(0), self.parent.viewport().height() + self.parent.horizontalHeader().height(), ) class DataFrameView(QTableView): """Data Frame view class""" def __init__(self, parent, model): QTableView.__init__(self, parent) self.setModel(model) self.frozen_table_view = FrozenTableView(self) self.frozen_table_view.update_geometry() self.setHorizontalScrollMode(1) self.setVerticalScrollMode(1) self.horizontalHeader().sectionResized.connect(self.update_section_width) self.verticalHeader().sectionResized.connect(self.update_section_height) self.frozen_table_view.verticalScrollBar().valueChanged.connect( self.verticalScrollBar().setValue ) self.sort_old = [None] self.header_class = self.horizontalHeader() self.header_class.sectionClicked.connect(self.sortByColumn) self.menu = self.setup_menu() QShortcut(QKeySequence(QKeySequence.Copy), self, self.copy) self.horizontalScrollBar().valueChanged.connect( lambda val: self.load_more_data(val, columns=True) ) self.verticalScrollBar().valueChanged.connect( lambda val: self.load_more_data(val, rows=True) ) self.verticalScrollBar().valueChanged.connect( self.frozen_table_view.verticalScrollBar().setValue ) def update_section_width(self, logical_index, old_size, new_size): """Update the horizontal width of the frozen column when a change takes place in the first column of the table""" if logical_index == 0: self.frozen_table_view.setColumnWidth(0, new_size) self.frozen_table_view.update_geometry() def update_section_height(self, logical_index, old_size, new_size): """Update the vertical width of the frozen column when a change takes place on any of the rows""" self.frozen_table_view.setRowHeight(logical_index, new_size) def resizeEvent(self, event): """Update the frozen column dimensions. Updates takes place when the enclosing window of this table reports a dimension change """ QTableView.resizeEvent(self, event) self.frozen_table_view.update_geometry() def moveCursor(self, cursor_action, modifiers): """Update the table position. Updates the position along with the frozen column when the cursor (selector) changes its position """ current = QTableView.moveCursor(self, cursor_action, modifiers) col_width = self.frozen_table_view.columnWidth( 0 ) + self.frozen_table_view.columnWidth(1) topleft_x = self.visualRect(current).topLeft().x() overflow = self.MoveLeft and current.column() > 1 overflow = overflow and topleft_x < col_width if cursor_action == overflow: new_value = self.horizontalScrollBar().value() + topleft_x - col_width self.horizontalScrollBar().setValue(new_value) return current def scrollTo(self, index, hint): """Scroll the table. It is necessary to ensure that the item at index is visible. The view will try to position the item according to the given hint. This method does not takes effect only if the frozen column is scrolled. """ if index.column() > 1: QTableView.scrollTo(self, index, hint) def load_more_data(self, value, rows=False, columns=False): """ :param value: :param rows: :param columns: """ if rows and value == self.verticalScrollBar().maximum(): self.model().fetch_more(rows=rows) if columns and value == self.horizontalScrollBar().maximum(): self.model().fetch_more(columns=columns) def sortByColumn(self, index): """Implement a Column sort""" if self.sort_old == [None]: self.header_class.setSortIndicatorShown(True) sort_order = self.header_class.sortIndicatorOrder() if not self.model().sort(index, sort_order): if len(self.sort_old) != 2: self.header_class.setSortIndicatorShown(False) else: self.header_class.setSortIndicator(self.sort_old[0], self.sort_old[1]) return self.sort_old = [index, self.header_class.sortIndicatorOrder()] def contextMenuEvent(self, event): """Reimplement Qt method""" self.menu.popup(event.globalPos()) event.accept() def setup_menu(self): """Setup context menu""" copy_action = create_action( self, _("Copy"), shortcut=keybinding("Copy"), icon=get_icon("editcopy.png"), triggered=self.copy, context=Qt.WidgetShortcut, ) functions = ( (_("To bool"), bool), (_("To complex"), complex), (_("To int"), int), (_("To float"), float), (_("To str"), str), ) types_in_menu = [copy_action] for name, func in functions: # QAction.triggered works differently for PySide and PyQt slot = lambda _checked, func=func: self.change_type(func) types_in_menu += [ create_action(self, name, triggered=slot, context=Qt.WidgetShortcut) ] menu = QMenu(self) add_actions(menu, types_in_menu) return menu def change_type(self, func): """A function that changes types of cells""" model = self.model() index_list = self.selectedIndexes() [model.setData(i, "", change_type=func) for i in index_list] @Slot() def copy(self): """Copy text to clipboard""" if not self.selectedIndexes(): return (row_min, row_max, col_min, col_max) = get_idx_rect(self.selectedIndexes()) index = header = False if col_min == 0: col_min = 1 index = True df = self.model().df if col_max == 0: # To copy indices contents = "\n".join( map(str, df.index.tolist()[slice(row_min, row_max + 1)]) ) else: # To copy DataFrame if (col_min == 0 or col_min == 1) and (df.shape[1] == col_max): header = True obj = df.iloc[slice(row_min, row_max + 1), slice(col_min - 1, col_max)] output = io.StringIO() obj.to_csv(output, sep="\t", index=index, header=header) contents = output.getvalue() output.close() clipboard = QApplication.clipboard() clipboard.setText(contents) class DataFrameEditor(QDialog): """ Dialog for displaying and editing DataFrame and related objects. Signals ------- sig_option_changed(str, object): Raised if an option is changed. Arguments are name of option and its new value. """ sig_option_changed = Signal(str, object) def __init__(self, parent=None): QDialog.__init__(self, parent) win32_fix_title_bar_background(self) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.is_series = False self.layout = None def setup_and_check(self, data, title=""): """ Setup DataFrameEditor: return False if data is not supported, True otherwise. Supported types for data are DataFrame, Series and DatetimeIndex. """ self.layout = QGridLayout() self.setLayout(self.layout) self.setWindowIcon(get_icon("arredit.png")) if title: title = str(title) + " - %s" % data.__class__.__name__ else: title = _("%s editor") % data.__class__.__name__ if isinstance(data, Series): self.is_series = True data = data.to_frame() elif isinstance(data, DatetimeIndex): data = DataFrame(data) self.setWindowTitle(title) self.resize(600, 500) self.dataModel = DataFrameModel(data, parent=self) self.dataModel.dataChanged.connect(self.save_and_close_enable) self.dataTable = DataFrameView(self, self.dataModel) self.layout.addWidget(self.dataTable) self.setLayout(self.layout) self.setMinimumSize(400, 300) # Make the dialog act as a window self.setWindowFlags(Qt.Window) btn_layout = QHBoxLayout() btn = QPushButton(_("Format")) # disable format button for int type btn_layout.addWidget(btn) btn.clicked.connect(self.change_format) btn = QPushButton(_("Resize")) btn_layout.addWidget(btn) btn.clicked.connect(self.resize_to_contents) bgcolor = QCheckBox(_("Background color")) bgcolor.setChecked(self.dataModel.bgcolor_enabled) bgcolor.setEnabled(self.dataModel.bgcolor_enabled) bgcolor.stateChanged.connect(self.change_bgcolor_enable) btn_layout.addWidget(bgcolor) self.bgcolor_global = QCheckBox(_("Column min/max")) self.bgcolor_global.setChecked(self.dataModel.colum_avg_enabled) self.bgcolor_global.setEnabled( not self.is_series and self.dataModel.bgcolor_enabled ) self.bgcolor_global.stateChanged.connect(self.dataModel.colum_avg) btn_layout.addWidget(self.bgcolor_global) btn_layout.addStretch() self.btn_save_and_close = QPushButton(_("Save and Close")) self.btn_save_and_close.setDisabled(True) self.btn_save_and_close.clicked.connect(self.accept) btn_layout.addWidget(self.btn_save_and_close) self.btn_close = QPushButton(_("Close")) self.btn_close.setAutoDefault(True) self.btn_close.setDefault(True) self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) self.layout.addLayout(btn_layout, 2, 0) return True @Slot(QModelIndex, QModelIndex) def save_and_close_enable(self, top_left, bottom_right): """Handle the data change event to enable the save and close button.""" self.btn_save_and_close.setEnabled(True) self.btn_save_and_close.setAutoDefault(True) self.btn_save_and_close.setDefault(True) def change_bgcolor_enable(self, state): """ This is implementet so column min/max is only active when bgcolor is """ self.dataModel.bgcolor(state) self.bgcolor_global.setEnabled(not self.is_series and state > 0) def change_format(self): """ Ask user for display format for floats and use it. This function also checks whether the format is valid and emits `sig_option_changed`. """ format, valid = QInputDialog.getText( self, _("Format"), _("Float formatting"), QLineEdit.Normal, self.dataModel.get_format(), ) if valid: format = str(format) try: format % 1.1 except: msg = _("Format ({}) is incorrect").format(format) QMessageBox.critical(self, _("Error"), msg) return if not format.startswith("%"): msg = _("Format ({}) should start with '%'").format(format) QMessageBox.critical(self, _("Error"), msg) return self.dataModel.set_format(format) self.sig_option_changed.emit("dataframe_format", format) def get_value(self): """Return modified Dataframe -- this is *not* a copy""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute df = self.dataModel.get_data() if self.is_series: return df.iloc[:, 0] else: return df def resize_to_contents(self): """ """ QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) self.dataTable.resizeColumnsToContents() self.dataModel.fetch_more(columns=True) self.dataTable.resizeColumnsToContents() QApplication.restoreOverrideCursor() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/importwizard.py0000666000000000000000000005717000000000000016227 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """ Text data Importing Wizard based on Qt """ # ----date and datetime objects support import datetime import io from functools import partial as ft_partial from itertools import zip_longest from numpy import nan from guidata.configtools import get_icon from guidata.qthelpers import add_actions, create_action, win32_fix_title_bar_background from guidata.config import _ from qtpy.QtWidgets import ( QCheckBox, QDialog, QFrame, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QMenu, QMessageBox, QPushButton, QRadioButton, QSizePolicy, QSpacerItem, QTableView, QTabWidget, QTextEdit, QVBoxLayout, QWidget, ) from qtpy.QtGui import ( QColor, QIntValidator, ) from qtpy.QtCore import ( QAbstractTableModel, QModelIndex, Qt, Signal, Slot, ) try: import pandas as pd except: pd = None def try_to_parse(value): """ :param value: :return: """ _types = ("int", "float") for _t in _types: try: _val = eval("%s('%s')" % (_t, value)) return _val except (ValueError, SyntaxError): pass return value def try_to_eval(value): """ :param value: :return: """ try: return eval(value) except (NameError, SyntaxError, ImportError): return value # ----Numpy arrays support class FakeObject(object): """Fake class used in replacement of missing modules""" pass try: from numpy import ndarray, array except: class ndarray(FakeObject): # analysis:ignore """Fake ndarray""" pass try: from dateutil.parser import parse as dateparse except: def dateparse(datestr, dayfirst=True): # analysis:ignore """Just for 'day/month/year' strings""" _a, _b, _c = list(map(int, datestr.split("/"))) if dayfirst: return datetime.datetime(_c, _b, _a) return datetime.datetime(_c, _a, _b) def datestr_to_datetime(value, dayfirst=True): """ :param value: :param dayfirst: :return: """ return dateparse(value, dayfirst=dayfirst) # ----Background colors for supported types COLORS = { bool: Qt.magenta, (float, int): Qt.blue, list: Qt.yellow, dict: Qt.cyan, tuple: Qt.lightGray, (str,): Qt.darkRed, ndarray: Qt.green, datetime.date: Qt.darkYellow, } def get_color(value, alpha): """Return color depending on value type""" color = QColor() for typ in COLORS: if isinstance(value, typ): color = QColor(COLORS[typ]) color.setAlphaF(alpha) return color class ContentsWidget(QWidget): """Import wizard contents widget""" asDataChanged = Signal(bool) def __init__(self, parent, text): QWidget.__init__(self, parent) self.text_editor = QTextEdit(self) self.text_editor.setText(text) self.text_editor.setReadOnly(True) # Type frame type_layout = QHBoxLayout() type_label = QLabel(_("Import as")) type_layout.addWidget(type_label) data_btn = QRadioButton(_("data")) data_btn.setChecked(True) self._as_data = True type_layout.addWidget(data_btn) code_btn = QRadioButton(_("code")) self._as_code = False type_layout.addWidget(code_btn) txt_btn = QRadioButton(_("text")) type_layout.addWidget(txt_btn) h_spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) type_layout.addItem(h_spacer) type_frame = QFrame() type_frame.setLayout(type_layout) # Opts frame grid_layout = QGridLayout() grid_layout.setSpacing(0) col_label = QLabel(_("Column separator:")) grid_layout.addWidget(col_label, 0, 0) col_w = QWidget() col_btn_layout = QHBoxLayout() self.tab_btn = QRadioButton(_("Tab")) self.tab_btn.setChecked(False) col_btn_layout.addWidget(self.tab_btn) self.ws_btn = QRadioButton(_("Whitespace")) self.ws_btn.setChecked(False) col_btn_layout.addWidget(self.ws_btn) other_btn_col = QRadioButton(_("other")) other_btn_col.setChecked(True) col_btn_layout.addWidget(other_btn_col) col_w.setLayout(col_btn_layout) grid_layout.addWidget(col_w, 0, 1) self.line_edt = QLineEdit(",") self.line_edt.setMaximumWidth(30) self.line_edt.setEnabled(True) other_btn_col.toggled.connect(self.line_edt.setEnabled) grid_layout.addWidget(self.line_edt, 0, 2) row_label = QLabel(_("Row separator:")) grid_layout.addWidget(row_label, 1, 0) row_w = QWidget() row_btn_layout = QHBoxLayout() self.eol_btn = QRadioButton(_("EOL")) self.eol_btn.setChecked(True) row_btn_layout.addWidget(self.eol_btn) other_btn_row = QRadioButton(_("other")) row_btn_layout.addWidget(other_btn_row) row_w.setLayout(row_btn_layout) grid_layout.addWidget(row_w, 1, 1) self.line_edt_row = QLineEdit(";") self.line_edt_row.setMaximumWidth(30) self.line_edt_row.setEnabled(False) other_btn_row.toggled.connect(self.line_edt_row.setEnabled) grid_layout.addWidget(self.line_edt_row, 1, 2) grid_layout.setRowMinimumHeight(2, 15) other_group = QGroupBox(_("Additional options")) other_layout = QGridLayout() other_group.setLayout(other_layout) skiprows_label = QLabel(_("Skip rows:")) other_layout.addWidget(skiprows_label, 0, 0) self.skiprows_edt = QLineEdit("0") self.skiprows_edt.setMaximumWidth(30) intvalid = QIntValidator(0, len(str(text).splitlines()), self.skiprows_edt) self.skiprows_edt.setValidator(intvalid) other_layout.addWidget(self.skiprows_edt, 0, 1) other_layout.setColumnMinimumWidth(2, 5) comments_label = QLabel(_("Comments:")) other_layout.addWidget(comments_label, 0, 3) self.comments_edt = QLineEdit("#") self.comments_edt.setMaximumWidth(30) other_layout.addWidget(self.comments_edt, 0, 4) self.trnsp_box = QCheckBox(_("Transpose")) # self.trnsp_box.setEnabled(False) other_layout.addWidget(self.trnsp_box, 1, 0, 2, 0) grid_layout.addWidget(other_group, 3, 0, 2, 0) opts_frame = QFrame() opts_frame.setLayout(grid_layout) data_btn.toggled.connect(opts_frame.setEnabled) data_btn.toggled.connect(self.set_as_data) code_btn.toggled.connect(self.set_as_code) # self.connect(txt_btn, SIGNAL("toggled(bool)"), # self, SLOT("is_text(bool)")) # Final layout layout = QVBoxLayout() layout.addWidget(type_frame) layout.addWidget(self.text_editor) layout.addWidget(opts_frame) self.setLayout(layout) def get_as_data(self): """Return if data type conversion""" return self._as_data def get_as_code(self): """Return if code type conversion""" return self._as_code def get_as_num(self): """Return if numeric type conversion""" return self._as_num def get_col_sep(self): """Return the column separator""" if self.tab_btn.isChecked(): return u"\t" elif self.ws_btn.isChecked(): return None return str(self.line_edt.text()) def get_row_sep(self): """Return the row separator""" if self.eol_btn.isChecked(): return u"\n" return str(self.line_edt_row.text()) def get_skiprows(self): """Return number of lines to be skipped""" return int(str(self.skiprows_edt.text())) def get_comments(self): """Return comment string""" return str(self.comments_edt.text()) @Slot(bool) def set_as_data(self, as_data): """Set if data type conversion""" self._as_data = as_data self.asDataChanged.emit(as_data) @Slot(bool) def set_as_code(self, as_code): """Set if code type conversion""" self._as_code = as_code class PreviewTableModel(QAbstractTableModel): """Import wizard preview table model""" def __init__(self, data=[], parent=None): QAbstractTableModel.__init__(self, parent) self._data = data def rowCount(self, parent=QModelIndex()): """Return row count""" return len(self._data) def columnCount(self, parent=QModelIndex()): """Return column count""" return len(self._data[0]) def _display_data(self, index): """Return a data element""" return self._data[index.row()][index.column()] def data(self, index, role=Qt.DisplayRole): """Return a model data element""" if not index.isValid(): return None if role == Qt.DisplayRole: return self._display_data(index) elif role == Qt.BackgroundColorRole: return get_color(self._data[index.row()][index.column()], 0.2) elif role == Qt.TextAlignmentRole: return int(Qt.AlignRight | Qt.AlignVCenter) return None def setData(self, index, value, role=Qt.EditRole): """Set model data""" return False def get_data(self): """Return a copy of model data""" return self._data[:][:] def parse_data_type(self, index, **kwargs): """Parse a type to an other type""" if not index.isValid(): return False try: if kwargs["atype"] == "date": self._data[index.row()][index.column()] = datestr_to_datetime( self._data[index.row()][index.column()], kwargs["dayfirst"] ).date() elif kwargs["atype"] == "perc": _tmp = self._data[index.row()][index.column()].replace("%", "") self._data[index.row()][index.column()] = eval(_tmp) / 100.0 elif kwargs["atype"] == "account": _tmp = self._data[index.row()][index.column()].replace(",", "") self._data[index.row()][index.column()] = eval(_tmp) elif kwargs["atype"] == "unicode": self._data[index.row()][index.column()] = str( self._data[index.row()][index.column()] ) elif kwargs["atype"] == "int": self._data[index.row()][index.column()] = int( self._data[index.row()][index.column()] ) elif kwargs["atype"] == "float": self._data[index.row()][index.column()] = float( self._data[index.row()][index.column()] ) self.dataChanged.emit(index, index) except Exception as instance: print(instance) # spyder: test-skip def reset(self): """ """ self.beginResetModel() self.endResetModel() class PreviewTable(QTableView): """Import wizard preview widget""" def __init__(self, parent): QTableView.__init__(self, parent) self._model = None # Setting up actions self.date_dayfirst_action = create_action( self, "dayfirst", triggered=ft_partial(self.parse_to_type, atype="date", dayfirst=True), ) self.date_monthfirst_action = create_action( self, "monthfirst", triggered=ft_partial(self.parse_to_type, atype="date", dayfirst=False), ) self.perc_action = create_action( self, "perc", triggered=ft_partial(self.parse_to_type, atype="perc") ) self.acc_action = create_action( self, "account", triggered=ft_partial(self.parse_to_type, atype="account") ) self.str_action = create_action( self, "unicode", triggered=ft_partial(self.parse_to_type, atype="unicode") ) self.int_action = create_action( self, "int", triggered=ft_partial(self.parse_to_type, atype="int") ) self.float_action = create_action( self, "float", triggered=ft_partial(self.parse_to_type, atype="float") ) # Setting up menus self.date_menu = QMenu() self.date_menu.setTitle("Date") add_actions( self.date_menu, (self.date_dayfirst_action, self.date_monthfirst_action) ) self.parse_menu = QMenu(self) self.parse_menu.addMenu(self.date_menu) add_actions(self.parse_menu, (self.perc_action, self.acc_action)) self.parse_menu.setTitle("String to") self.opt_menu = QMenu(self) self.opt_menu.addMenu(self.parse_menu) add_actions( self.opt_menu, (self.str_action, self.int_action, self.float_action) ) def _shape_text( self, text, colsep=u"\t", rowsep=u"\n", transpose=False, skiprows=0, comments="#", ): """Decode the shape of the given text""" assert colsep != rowsep out = [] text_rows = text.split(rowsep)[skiprows:] for row in text_rows: stripped = str(row).strip() if len(stripped) == 0 or stripped.startswith(comments): continue line = str(row).split(colsep) line = [try_to_parse(str(x)) for x in line] out.append(line) # Replace missing elements with np.nan's or None's out = list(zip_longest(*out, fillvalue=nan)) # Tranpose the last result to get the expected one out = [[r[col] for r in out] for col in range(len(out[0]))] if transpose: return [[r[col] for r in out] for col in range(len(out[0]))] return out def get_data(self): """Return model data""" if self._model is None: return None return self._model.get_data() def process_data( self, text, colsep=u"\t", rowsep=u"\n", transpose=False, skiprows=0, comments="#", ): """Put data into table model""" data = self._shape_text(text, colsep, rowsep, transpose, skiprows, comments) self._model = PreviewTableModel(data) self.setModel(self._model) @Slot() def parse_to_type(self, **kwargs): """Parse to a given type""" indexes = self.selectedIndexes() if not indexes: return for index in indexes: self.model().parse_data_type(index, **kwargs) def contextMenuEvent(self, event): """Reimplement Qt method""" self.opt_menu.popup(event.globalPos()) event.accept() class PreviewWidget(QWidget): """Import wizard preview widget""" def __init__(self, parent): QWidget.__init__(self, parent) vert_layout = QVBoxLayout() # Type frame type_layout = QHBoxLayout() type_label = QLabel(_("Import as")) type_layout.addWidget(type_label) self.array_btn = array_btn = QRadioButton(_("array")) array_btn.setEnabled(ndarray is not FakeObject) array_btn.setChecked(ndarray is not FakeObject) type_layout.addWidget(array_btn) list_btn = QRadioButton(_("list")) list_btn.setChecked(not array_btn.isChecked()) type_layout.addWidget(list_btn) if pd: self.df_btn = df_btn = QRadioButton(_("DataFrame")) df_btn.setChecked(False) type_layout.addWidget(df_btn) h_spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) type_layout.addItem(h_spacer) type_frame = QFrame() type_frame.setLayout(type_layout) self._table_view = PreviewTable(self) vert_layout.addWidget(type_frame) vert_layout.addWidget(self._table_view) self.setLayout(vert_layout) def open_data( self, text, colsep=u"\t", rowsep=u"\n", transpose=False, skiprows=0, comments="#", ): """Open clipboard text as table""" if pd: self.pd_text = text self.pd_info = dict( sep=colsep, lineterminator=rowsep, skiprows=skiprows, comment=comments ) if colsep is None: self.pd_info = dict( lineterminator=rowsep, skiprows=skiprows, comment=comments, delim_whitespace=True, ) self._table_view.process_data( text, colsep, rowsep, transpose, skiprows, comments ) def get_data(self): """Return table data""" return self._table_view.get_data() class ImportWizard(QDialog): """Text data import wizard""" def __init__( self, parent, text, title=None, icon=None, contents_title=None, varname=None ): QDialog.__init__(self, parent) win32_fix_title_bar_background(self) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) if title is None: title = _("Import wizard") self.setWindowTitle(title) if icon is None: self.setWindowIcon(get_icon("fileimport.png")) if contents_title is None: contents_title = _("Raw text") if varname is None: varname = _("variable_name") self.var_name, self.clip_data = None, None # Setting GUI self.tab_widget = QTabWidget(self) self.text_widget = ContentsWidget(self, text) self.table_widget = PreviewWidget(self) self.tab_widget.addTab(self.text_widget, _("text")) self.tab_widget.setTabText(0, contents_title) self.tab_widget.addTab(self.table_widget, _("table")) self.tab_widget.setTabText(1, _("Preview")) self.tab_widget.setTabEnabled(1, False) name_layout = QHBoxLayout() name_label = QLabel(_("Variable Name")) name_layout.addWidget(name_label) self.name_edt = QLineEdit() self.name_edt.setText(varname) name_layout.addWidget(self.name_edt) btns_layout = QHBoxLayout() cancel_btn = QPushButton(_("Cancel")) btns_layout.addWidget(cancel_btn) cancel_btn.clicked.connect(self.reject) h_spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) btns_layout.addItem(h_spacer) self.back_btn = QPushButton(_("Previous")) self.back_btn.setEnabled(False) btns_layout.addWidget(self.back_btn) self.back_btn.clicked.connect(ft_partial(self._set_step, step=-1)) self.fwd_btn = QPushButton(_("Next")) if not text: self.fwd_btn.setEnabled(False) btns_layout.addWidget(self.fwd_btn) self.fwd_btn.clicked.connect(ft_partial(self._set_step, step=1)) self.done_btn = QPushButton(_("Done")) self.done_btn.setEnabled(False) btns_layout.addWidget(self.done_btn) self.done_btn.clicked.connect(self.process) self.text_widget.asDataChanged.connect(self.fwd_btn.setEnabled) self.text_widget.asDataChanged.connect(self.done_btn.setDisabled) layout = QVBoxLayout() layout.addLayout(name_layout) layout.addWidget(self.tab_widget) layout.addLayout(btns_layout) self.setLayout(layout) def _focus_tab(self, tab_idx): """Change tab focus""" for i in range(self.tab_widget.count()): self.tab_widget.setTabEnabled(i, False) self.tab_widget.setTabEnabled(tab_idx, True) self.tab_widget.setCurrentIndex(tab_idx) def _set_step(self, step): """Proceed to a given step""" new_tab = self.tab_widget.currentIndex() + step assert new_tab < self.tab_widget.count() and new_tab >= 0 if new_tab == self.tab_widget.count() - 1: try: self.table_widget.open_data( self._get_plain_text(), self.text_widget.get_col_sep(), self.text_widget.get_row_sep(), self.text_widget.trnsp_box.isChecked(), self.text_widget.get_skiprows(), self.text_widget.get_comments(), ) self.done_btn.setEnabled(True) self.done_btn.setDefault(True) self.fwd_btn.setEnabled(False) self.back_btn.setEnabled(True) except (SyntaxError, AssertionError) as error: QMessageBox.critical( self, _("Import wizard"), _( "Unable to proceed to next step" "

Please check your entries." "

Error message:
%s" ) % str(error), ) return elif new_tab == 0: self.done_btn.setEnabled(False) self.fwd_btn.setEnabled(True) self.back_btn.setEnabled(False) self._focus_tab(new_tab) def get_data(self): """Return processed data""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.var_name, self.clip_data def _simplify_shape(self, alist, rec=0): """Reduce the alist dimension if needed""" if rec != 0: if len(alist) == 1: return alist[-1] return alist if len(alist) == 1: return self._simplify_shape(alist[-1], 1) return [self._simplify_shape(al, 1) for al in alist] def _get_table_data(self): """Return clipboard processed as data""" data = self._simplify_shape(self.table_widget.get_data()) if self.table_widget.array_btn.isChecked(): return array(data) elif pd and self.table_widget.df_btn.isChecked(): info = self.table_widget.pd_info buf = io.StringIO(self.table_widget.pd_text) return pd.read_csv(buf, **info) return data def _get_plain_text(self): """Return clipboard as text""" return self.text_widget.text_editor.toPlainText() @Slot() def process(self): """Process the data from clipboard""" var_name = self.name_edt.text() try: self.var_name = str(var_name) except UnicodeEncodeError: self.var_name = str(var_name) if self.text_widget.get_as_data(): self.clip_data = self._get_table_data() elif self.text_widget.get_as_code(): self.clip_data = try_to_eval(str(self._get_plain_text())) else: self.clip_data = str(self._get_plain_text()) self.accept() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1628151432.0 guidata-2.0.2/guidata/widgets/nsview.py0000666000000000000000000005564400000000000015013 0ustar00# -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright (c) 2009- Spyder Kernels Contributors # # Licensed under the terms of the MIT License # (see spyder_kernels/__init__.py for details) # ----------------------------------------------------------------------------- """ Utilities """ # ============================================================================== # Date and datetime objects support # ============================================================================== import datetime import re from itertools import islice NUMERIC_TYPES = (int, float, complex) # ============================================================================== # FakeObject # ============================================================================== class FakeObject(object): """Fake class used in replacement of missing modules""" pass # ============================================================================== # Numpy arrays and numeric types support # ============================================================================== try: from numpy import ( ndarray, array, matrix, recarray, int64, int32, int16, int8, uint64, uint32, uint16, uint8, float64, float32, float16, complex64, complex128, bool_, ) from numpy.ma import MaskedArray from numpy import savetxt as np_savetxt from numpy import get_printoptions, set_printoptions except: ndarray = ( array ) = ( matrix ) = ( recarray ) = ( MaskedArray ) = ( np_savetxt ) = ( int64 ) = ( int32 ) = ( int16 ) = ( int8 ) = ( uint64 ) = ( uint32 ) = ( uint16 ) = ( uint8 ) = float64 = float32 = float16 = complex64 = complex128 = bool_ = FakeObject def get_numpy_dtype(obj): """Return NumPy data type associated to obj Return None if NumPy is not available or if obj is not a NumPy array or scalar""" if ndarray is not FakeObject: # NumPy is available import numpy as np if isinstance(obj, np.generic) or isinstance(obj, np.ndarray): # Numpy scalars all inherit from np.generic. # Numpy arrays all inherit from np.ndarray. # If we check that we are certain we have one of these # types then we are less likely to generate an exception below. try: return obj.dtype.type except (AttributeError, RuntimeError): # AttributeError: some NumPy objects have no dtype attribute # RuntimeError: happens with NetCDF objects (Issue 998) return # ============================================================================== # Pandas support # ============================================================================== try: from pandas import DataFrame, DatetimeIndex, Series except: DataFrame = DatetimeIndex = Series = FakeObject # ============================================================================== # PIL Images support # ============================================================================== try: import PIL.Image Image = PIL.Image.Image except: Image = FakeObject # analysis:ignore # ============================================================================== # BeautifulSoup support (see Issue 2448) # ============================================================================== try: import bs4 NavigableString = bs4.element.NavigableString except: NavigableString = FakeObject # analysis:ignore # ============================================================================== # Misc. # ============================================================================== def address(obj): """Return object address as a string: ''""" return "<%s @ %s>" % ( obj.__class__.__name__, hex(id(obj)).upper().replace("X", "x"), ) def try_to_eval(value): """Try to eval value""" try: return eval(value) except (NameError, SyntaxError, ImportError): return value def get_size(item): """Return size of an item of arbitrary type""" if isinstance(item, (list, tuple, dict)): return len(item) elif isinstance(item, (ndarray, MaskedArray)): return item.shape elif isinstance(item, Image): return item.size if isinstance(item, (DataFrame, DatetimeIndex, Series)): return item.shape else: return 1 def get_object_attrs(obj): """ Get the attributes of an object using dir. This filters protected attributes """ attrs = [k for k in dir(obj) if not k.startswith("__")] if not attrs: attrs = dir(obj) return attrs try: from dateutil.parser import parse as dateparse except: def dateparse(datestr): # analysis:ignore """Just for 'year, month, day' strings""" return datetime.datetime(*list(map(int, datestr.split(",")))) def datestr_to_datetime(value): """ :param value: :return: """ rp = value.rfind("(") + 1 v = dateparse(value[rp:-1]) print(value, "-->", v) # spyder: test-skip return v def str_to_timedelta(value): """Convert a string to a datetime.timedelta value. The following strings are accepted: - 'datetime.timedelta(1, 5, 12345)' - 'timedelta(1, 5, 12345)' - '(1, 5, 12345)' - '1, 5, 12345' - '1' if there are less then three parameters, the missing parameters are assumed to be 0. Variations in the spacing of the parameters are allowed. Raises: ValueError for strings not matching the above criterion. """ m = re.match(r"^(?:(?:datetime\.)?timedelta)?" r"\(?" r"([^)]*)" r"\)?$", value) if not m: raise ValueError("Invalid string for datetime.timedelta") args = [int(a.strip()) for a in m.group(1).split(",")] return datetime.timedelta(*args) # ============================================================================== # Background colors for supported types # ============================================================================== ARRAY_COLOR = "#00ff00" SCALAR_COLOR = "#0000ff" COLORS = { bool: "#ff00ff", NUMERIC_TYPES: SCALAR_COLOR, list: "#ffff00", dict: "#00ffff", tuple: "#c0c0c0", (str,): "#800000", (ndarray, MaskedArray, matrix, DataFrame, Series, DatetimeIndex): ARRAY_COLOR, Image: "#008000", datetime.date: "#808000", datetime.timedelta: "#808000", } CUSTOM_TYPE_COLOR = "#7755aa" UNSUPPORTED_COLOR = "#ffffff" def get_color_name(value): """Return color name depending on value type""" if not is_known_type(value): return CUSTOM_TYPE_COLOR for typ, name in list(COLORS.items()): if isinstance(value, typ): return name else: np_dtype = get_numpy_dtype(value) if np_dtype is None or not hasattr(value, "size"): return UNSUPPORTED_COLOR elif value.size == 1: return SCALAR_COLOR else: return ARRAY_COLOR def is_editable_type(value): """Return True if data type is editable with a standard GUI-based editor, like CollectionsEditor, ArrayEditor, QDateEdit or a simple QLineEdit""" return get_color_name(value) not in (UNSUPPORTED_COLOR, CUSTOM_TYPE_COLOR) # ============================================================================== # Sorting # ============================================================================== def sort_against(list1, list2, reverse=False): """ Arrange items of list1 in the same order as sorted(list2). In other words, apply to list1 the permutation which takes list2 to sorted(list2, reverse). """ try: return [ item for _, item in sorted( zip(list2, list1), key=lambda x: x[0], reverse=reverse ) ] except: return list1 def unsorted_unique(lista): """Removes duplicates from lista neglecting its initial ordering""" return list(set(lista)) # ============================================================================== # Display <--> Value # ============================================================================== def default_display(value, with_module=True): """Default display for unknown objects.""" object_type = type(value) try: name = object_type.__name__ module = object_type.__module__ if with_module: return name + " object of " + module + " module" else: return name except: type_str = str(object_type) return type_str[1:-1] def collections_display(value, level): """Display for collections (i.e. list, tuple and dict).""" is_dict = isinstance(value, dict) # Get elements if is_dict: elements = value.items() else: elements = value # Truncate values truncate = False if level == 1 and len(value) > 10: elements = islice(elements, 10) if is_dict else value[:10] truncate = True elif level == 2 and len(value) > 5: elements = islice(elements, 5) if is_dict else value[:5] truncate = True # Get display of each element if level <= 2: if is_dict: displays = [ value_to_display(k, level=level) + ":" + value_to_display(v, level=level) for (k, v) in list(elements) ] else: displays = [value_to_display(e, level=level) for e in elements] if truncate: displays.append("...") display = ", ".join(displays) else: display = "..." # Return display if is_dict: display = "{" + display + "}" elif isinstance(value, list): display = "[" + display + "]" else: display = "(" + display + ")" return display def value_to_display(value, minmax=False, level=0): """Convert value for display purpose""" # To save current Numpy threshold np_threshold = FakeObject try: numeric_numpy_types = ( int64, int32, int16, int8, uint64, uint32, uint16, uint8, float64, float32, float16, complex128, complex64, bool_, ) if ndarray is not FakeObject: # Save threshold np_threshold = get_printoptions().get("threshold") # Set max number of elements to show for Numpy arrays # in our display set_printoptions(threshold=10) if isinstance(value, recarray): if level == 0: fields = value.names display = "Field names: " + ", ".join(fields) else: display = "Recarray" elif isinstance(value, MaskedArray): display = "Masked array" elif isinstance(value, ndarray): if level == 0: if minmax: try: display = "Min: %r\nMax: %r" % (value.min(), value.max()) except (TypeError, ValueError): if value.dtype.type in numeric_numpy_types: display = str(value) else: display = default_display(value) elif value.dtype.type in numeric_numpy_types: display = str(value) else: display = default_display(value) else: display = "Numpy array" elif any([type(value) == t for t in [list, tuple, dict]]): display = collections_display(value, level + 1) elif isinstance(value, Image): if level == 0: display = "%s Mode: %s" % (address(value), value.mode) else: display = "Image" elif isinstance(value, DataFrame): if level == 0: cols = value.columns cols = [str(c) for c in cols] display = "Column names: " + ", ".join(list(cols)) else: display = "Dataframe" elif isinstance(value, NavigableString): # Fixes Issue 2448 display = str(value) if level > 0: display = u"'" + display + u"'" elif isinstance(value, DatetimeIndex): if level == 0: try: display = value._summary() except AttributeError: display = value.summary() else: display = "DatetimeIndex" elif isinstance(value, bytes): # We don't apply this to classes that extend string types # See issue 5636 if type(value) is bytes: try: display = str(value, "utf8") if level > 0: display = u"'" + display + u"'" except: display = value if level > 0: display = b"'" + display + b"'" else: display = default_display(value) elif isinstance(value, str): # We don't apply this to classes that extend string types # See issue 5636 if type(value) is str: display = value if level > 0: display = u"'" + display + u"'" else: display = default_display(value) elif isinstance(value, datetime.date) or isinstance(value, datetime.timedelta): display = str(value) elif ( isinstance(value, NUMERIC_TYPES) or isinstance(value, bool) or isinstance(value, numeric_numpy_types) ): display = repr(value) else: if level == 0: display = default_display(value) else: display = default_display(value, with_module=False) except: display = default_display(value) # Truncate display at 70 chars to avoid freezing Spyder # because of large displays if len(display) > 70: if isinstance(display, bytes): ellipses = b" ..." else: ellipses = u" ..." display = display[:70].rstrip() + ellipses # Restore Numpy threshold if np_threshold is not FakeObject: set_printoptions(threshold=np_threshold) return display def display_to_value(value, default_value, ignore_errors=True): """Convert back to value""" try: np_dtype = get_numpy_dtype(default_value) if isinstance(default_value, bool): # We must test for boolean before NumPy data types # because `bool` class derives from `int` class try: value = bool(float(value)) except ValueError: value = value.lower() == "true" elif np_dtype is not None: if "complex" in str(type(default_value)): value = np_dtype(complex(value)) else: value = np_dtype(value) elif isinstance(default_value, bytes): value = bytes(value, "utf8") elif isinstance(default_value, str): value = str(value) elif isinstance(default_value, complex): value = complex(value) elif isinstance(default_value, float): value = float(value) elif isinstance(default_value, int): try: value = int(value) except ValueError: value = float(value) elif isinstance(default_value, datetime.datetime): value = datestr_to_datetime(value) elif isinstance(default_value, datetime.date): value = datestr_to_datetime(value).date() elif isinstance(default_value, datetime.timedelta): value = str_to_timedelta(value) elif ignore_errors: value = try_to_eval(value) else: value = eval(value) except (ValueError, SyntaxError): if ignore_errors: value = try_to_eval(value) else: return default_value return value # ============================================================================= # Types # ============================================================================= def get_type_string(item): """Return type string of an object.""" if isinstance(item, DataFrame): return "DataFrame" if isinstance(item, DatetimeIndex): return "DatetimeIndex" if isinstance(item, Series): return "Series" found = re.findall(r"<(?:type|class) '(\S*)'>", str(type(item))) if found: return found[0] def is_known_type(item): """Return True if object has a known type""" # Unfortunately, the masked array case is specific return isinstance(item, MaskedArray) or get_type_string(item) is not None def get_human_readable_type(item): """Return human-readable type string of an item""" if isinstance(item, (ndarray, MaskedArray)): return item.dtype.name elif isinstance(item, Image): return "Image" else: text = get_type_string(item) if text is None: return "unknown" else: return text[text.find(".") + 1 :] # ============================================================================== # Globals filter: filter namespace dictionaries (to be edited in # CollectionsEditor) # ============================================================================== def is_supported(value, check_all=False, filters=None, iterate=False): """Return True if the value is supported, False otherwise""" assert filters is not None if value is None: return True if not is_editable_type(value): return False elif not isinstance(value, filters): return False elif iterate: if isinstance(value, (list, tuple, set)): valid_count = 0 for val in value: if is_supported(val, filters=filters, iterate=check_all): valid_count += 1 if not check_all: break return valid_count > 0 elif isinstance(value, dict): for key, val in list(value.items()): if not is_supported( key, filters=filters, iterate=check_all ) or not is_supported(val, filters=filters, iterate=check_all): return False if not check_all: break return True def globalsfilter( input_dict, check_all=False, filters=None, exclude_private=None, exclude_capitalized=None, exclude_uppercase=None, exclude_unsupported=None, excluded_names=None, ): """Keep only objects that can be pickled""" output_dict = {} for key, value in list(input_dict.items()): excluded = ( (exclude_private and key.startswith("_")) or (exclude_capitalized and key[0].isupper()) or ( exclude_uppercase and key.isupper() and len(key) > 1 and not key[1:].isdigit() ) or (key in excluded_names) or ( exclude_unsupported and not is_supported(value, check_all=check_all, filters=filters) ) ) if not excluded: output_dict[key] = value return output_dict # ============================================================================== # Create view to be displayed by NamespaceBrowser # ============================================================================== REMOTE_SETTINGS = ( "check_all", "exclude_private", "exclude_uppercase", "exclude_capitalized", "exclude_unsupported", "excluded_names", "minmax", ) def get_supported_types(): """ Return a dictionnary containing types lists supported by the namespace browser. """ from datetime import date, timedelta editable_types = [int, float, complex, list, dict, tuple, date, timedelta, str] try: from numpy import ndarray, matrix, generic editable_types += [ndarray, matrix, generic] except: pass try: from pandas import DataFrame, Series, DatetimeIndex editable_types += [DataFrame, Series, DatetimeIndex] except: pass picklable_types = editable_types[:] if Image is not FakeObject: editable_types.append(Image) return dict(picklable=picklable_types, editable=editable_types) def get_remote_data(data, settings, mode, more_excluded_names=None): """ Return globals according to filter described in *settings*: * data: data to be filtered (dictionary) * settings: variable explorer settings (dictionary) * mode (string): 'editable' or 'picklable' * more_excluded_names: additional excluded names (list) """ supported_types = get_supported_types() assert mode in list(supported_types.keys()) excluded_names = settings["excluded_names"] if more_excluded_names is not None: excluded_names += more_excluded_names return globalsfilter( data, check_all=settings["check_all"], filters=tuple(supported_types[mode]), exclude_private=settings["exclude_private"], exclude_uppercase=settings["exclude_uppercase"], exclude_capitalized=settings["exclude_capitalized"], exclude_unsupported=settings["exclude_unsupported"], excluded_names=excluded_names, ) def make_remote_view(data, settings, more_excluded_names=None): """ Make a remote view of dictionary *data* -> globals explorer """ data = get_remote_data( data, settings, mode="editable", more_excluded_names=more_excluded_names ) remote = {} for key, value in list(data.items()): view = value_to_display(value, minmax=settings["minmax"]) remote[key] = { "type": get_human_readable_type(value), "size": get_size(value), "color": get_color_name(value), "view": view, } return remote ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/objecteditor.py0000666000000000000000000001116400000000000016142 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """ guidata.widgets.objecteditor ============================ This package provides a generic object editor widget. .. autofunction:: oedit """ import PIL.Image from numpy.core.multiarray import ndarray from qtpy.QtCore import QObject from guidata.widgets.arrayeditor import ArrayEditor from guidata.widgets.collectionseditor import CollectionsEditor from guidata.widgets.nsview import ( DataFrame, FakeObject, Series, is_known_type, ) from guidata.widgets.texteditor import TextEditor try: from guidata.widgets.dataframeeditor import DataFrameEditor except ImportError: DataFrameEditor = FakeObject() class DialogKeeper(QObject): """ """ def __init__(self): QObject.__init__(self) self.dialogs = {} self.namespace = None def set_namespace(self, namespace): """ :param namespace: """ self.namespace = namespace def create_dialog(self, dialog, refname, func): """ :param dialog: :param refname: :param func: """ self.dialogs[id(dialog)] = dialog, refname, func dialog.accepted.connect(lambda eid=id(dialog): self.editor_accepted(eid)) dialog.rejected.connect(lambda eid=id(dialog): self.editor_rejected(eid)) dialog.show() dialog.activateWindow() dialog.raise_() def editor_accepted(self, dialog_id): """ :param dialog_id: """ dialog, refname, func = self.dialogs[dialog_id] self.namespace[refname] = func(dialog) self.dialogs.pop(dialog_id) def editor_rejected(self, dialog_id): """ :param dialog_id: """ self.dialogs.pop(dialog_id) keeper = DialogKeeper() def create_dialog(obj, obj_name): """Creates the editor dialog and returns a tuple (dialog, func) where func is the function to be called with the dialog instance as argument, after quitting the dialog box The role of this intermediate function is to allow easy monkey-patching. (uschmitt suggested this indirection here so that he can monkey patch oedit to show eMZed related data) """ conv_func = lambda data: data readonly = not is_known_type(obj) if isinstance(obj, ndarray): dialog = ArrayEditor() if not dialog.setup_and_check(obj, title=obj_name, readonly=readonly): return elif isinstance(obj, PIL.Image.Image): dialog = ArrayEditor() import numpy as np data = np.array(obj) if not dialog.setup_and_check(data, title=obj_name, readonly=readonly): return conv_func = lambda data: PIL.Image.fromarray(data, mode=obj.mode) elif isinstance(obj, (DataFrame, Series)) and DataFrame is not FakeObject: dialog = DataFrameEditor() if not dialog.setup_and_check(obj): return elif isinstance(obj, str): dialog = TextEditor(obj, title=obj_name, readonly=readonly) else: dialog = CollectionsEditor() dialog.setup(obj, title=obj_name, readonly=readonly) def end_func(dialog): """ :param dialog: :return: """ return conv_func(dialog.get_value()) return dialog, end_func def oedit(obj, modal=True, namespace=None): """Edit the object 'obj' in a GUI-based editor and return the edited copy (if Cancel is pressed, return None) The object 'obj' is a container Supported container types: dict, list, tuple, str/unicode or numpy.array (instantiate a new QApplication if necessary, so it can be called directly from the interpreter) """ from guidata import qapplication app = qapplication() if modal: obj_name = "" else: assert isinstance(obj, str) obj_name = obj if namespace is None: namespace = globals() keeper.set_namespace(namespace) obj = namespace[obj_name] # keep QApplication reference alive in the Python interpreter: namespace["__qapp__"] = app result = create_dialog(obj, obj_name) if result is None: return dialog, end_func = result if modal: if dialog.exec_(): return end_func(dialog) else: keeper.create_dialog(dialog, obj_name, end_func) import os if os.name == "nt": app.exec_() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/syntaxhighlighters.py0000666000000000000000000004102100000000000017410 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """ Editor widget syntax highlighters based on QtGui.QSyntaxHighlighter (Python syntax highlighting rules are inspired from idlelib) """ import builtins import keyword import re from guidata.config import CONF, _ from qtpy.QtWidgets import ( QApplication, ) from qtpy.QtGui import ( QColor, QCursor, QFont, QTextCharFormat, QTextOption, QSyntaxHighlighter, ) from qtpy.QtCore import Qt # ============================================================================= # Constants # ============================================================================= COLOR_SCHEME_KEYS = { "background": _("Background:"), "currentline": _("Current line:"), "currentcell": _("Current cell:"), "occurrence": _("Occurrence:"), "ctrlclick": _("Link:"), "sideareas": _("Side areas:"), "matched_p": _("Matched
parens:"), "unmatched_p": _("Unmatched
parens:"), "normal": _("Normal text:"), "keyword": _("Keyword:"), "builtin": _("Builtin:"), "definition": _("Definition:"), "comment": _("Comment:"), "string": _("String:"), "number": _("Number:"), "instance": _("Instance:"), } COLOR_SCHEME_NAMES = CONF.get("color_schemes", "names") # Mapping for file extensions that use Pygments highlighting but should use # different lexers than Pygments' autodetection suggests. Keys are file # extensions or tuples of extensions, values are Pygments lexer names. # ============================================================================== # Auxiliary functions # ============================================================================== def get_color_scheme(name): """Get a color scheme from config using its name""" name = name.lower() scheme = {} for key in COLOR_SCHEME_KEYS: try: scheme[key] = CONF.get("color_schemes", name + "/" + key) except: scheme[key] = CONF.get("color_schemes", "spyder/" + key) return scheme # ============================================================================== # Syntax highlighting color schemes # ============================================================================== class BaseSH(QSyntaxHighlighter): """Base Syntax Highlighter Class""" # Syntax highlighting rules: PROG = None BLANKPROG = re.compile(r"\s+") # Syntax highlighting states (from one text block to another): NORMAL = 0 # Syntax highlighting parameters. BLANK_ALPHA_FACTOR = 0.31 def __init__(self, parent, font=None, color_scheme=None): QSyntaxHighlighter.__init__(self, parent) self.font = font if color_scheme is None: color_scheme = CONF.get("color_schemes", "default") if isinstance(color_scheme, str): self.color_scheme = get_color_scheme(color_scheme) else: self.color_scheme = color_scheme self.background_color = None self.currentline_color = None self.currentcell_color = None self.occurrence_color = None self.ctrlclick_color = None self.sideareas_color = None self.matched_p_color = None self.unmatched_p_color = None self.formats = None self.setup_formats(font) self.cell_separators = None def get_background_color(self): """ :return: """ return QColor(self.background_color) def get_foreground_color(self): """Return foreground ('normal' text) color""" return self.formats["normal"].foreground().color() def get_currentline_color(self): """ :return: """ return QColor(self.currentline_color) def get_currentcell_color(self): """ :return: """ return QColor(self.currentcell_color) def get_occurrence_color(self): """ :return: """ return QColor(self.occurrence_color) def get_ctrlclick_color(self): """ :return: """ return QColor(self.ctrlclick_color) def get_sideareas_color(self): """ :return: """ return QColor(self.sideareas_color) def get_matched_p_color(self): """ :return: """ return QColor(self.matched_p_color) def get_unmatched_p_color(self): """ :return: """ return QColor(self.unmatched_p_color) def get_comment_color(self): """Return color for the comments""" return self.formats["comment"].foreground().color() def get_color_name(self, fmt): """Return color name assigned to a given format""" return self.formats[fmt].foreground().color().name() def setup_formats(self, font=None): """ :param font: """ base_format = QTextCharFormat() if font is not None: self.font = font if self.font is not None: base_format.setFont(self.font) self.formats = {} colors = self.color_scheme.copy() self.background_color = colors.pop("background") self.currentline_color = colors.pop("currentline") self.currentcell_color = colors.pop("currentcell") self.occurrence_color = colors.pop("occurrence") self.ctrlclick_color = colors.pop("ctrlclick") self.sideareas_color = colors.pop("sideareas") self.matched_p_color = colors.pop("matched_p") self.unmatched_p_color = colors.pop("unmatched_p") for name, (color, bold, italic) in list(colors.items()): format = QTextCharFormat(base_format) format.setForeground(QColor(color)) format.setBackground(QColor(self.background_color)) if bold: format.setFontWeight(QFont.Bold) format.setFontItalic(italic) self.formats[name] = format def set_color_scheme(self, color_scheme): """ :param color_scheme: """ if isinstance(color_scheme, str): self.color_scheme = get_color_scheme(color_scheme) else: self.color_scheme = color_scheme self.setup_formats() self.rehighlight() def highlightBlock(self, text): """ :param text: """ raise NotImplementedError def highlight_spaces(self, text, offset=0): """ Make blank space less apparent by setting the foreground alpha. This only has an effect when 'Show blank space' is turned on. Derived classes could call this function at the end of highlightBlock(). """ flags_text = self.document().defaultTextOption().flags() show_blanks = flags_text & QTextOption.ShowTabsAndSpaces if show_blanks: format_leading = self.formats.get("leading", None) format_trailing = self.formats.get("trailing", None) match = self.BLANKPROG.search(text, offset) while match: start, end = match.span() start = max([0, start + offset]) end = max([0, end + offset]) # Format trailing spaces at the end of the line. if end == len(text) and format_trailing is not None: self.setFormat(start, end, format_trailing) # Format leading spaces, e.g. indentation. if start == 0 and format_leading is not None: self.setFormat(start, end, format_leading) format = self.format(start) color_foreground = format.foreground().color() alpha_new = self.BLANK_ALPHA_FACTOR * color_foreground.alphaF() color_foreground.setAlphaF(alpha_new) self.setFormat(start, end - start, color_foreground) match = self.BLANKPROG.search(text, match.end()) def rehighlight(self): """ """ QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QSyntaxHighlighter.rehighlight(self) QApplication.restoreOverrideCursor() # ============================================================================== # Python syntax highlighter # ============================================================================== def any(name, alternates): "Return a named group pattern matching list of alternates." return "(?P<%s>" % name + "|".join(alternates) + ")" def make_python_patterns(additional_keywords=[], additional_builtins=[]): "Strongly inspired from idlelib.ColorDelegator.make_pat" kwlist = keyword.kwlist + additional_keywords builtinlist = [ str(name) for name in dir(builtins) if not name.startswith("_") ] + additional_builtins repeated = set(kwlist) & set(builtinlist) for repeated_element in repeated: kwlist.remove(repeated_element) kw = r"\b" + any("keyword", kwlist) + r"\b" builtin = r"([^.'\"\\#]\b|^)" + any("builtin", builtinlist) + r"\b" comment = any("comment", [r"#[^\n]*"]) instance = any( "instance", [ r"\bself\b", r"\bcls\b", (r"^\s*@([a-zA-Z_][a-zA-Z0-9_]*)" r"(\.[a-zA-Z_][a-zA-Z0-9_]*)*"), ], ) number_regex = [ r"\b[+-]?[0-9]+[lLjJ]?\b", r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", r"\b[+-]?0[oO][0-7]+[lL]?\b", r"\b[+-]?0[bB][01]+[lL]?\b", r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?[jJ]?\b", ] # Based on # https://github.com/python/cpython/blob/ # 81950495ba2c36056e0ce48fd37d514816c26747/Lib/tokenize.py#L117 # In order: Hexnumber, Binnumber, Octnumber, Decnumber, # Pointfloat + Exponent, Expfloat, Imagnumber number_regex = [ r"\b[+-]?0[xX](?:_?[0-9A-Fa-f])+[lL]?\b", r"\b[+-]?0[bB](?:_?[01])+[lL]?\b", r"\b[+-]?0[oO](?:_?[0-7])+[lL]?\b", r"\b[+-]?(?:0(?:_?0)*|[1-9](?:_?[0-9])*)[lL]?\b", r"\b((\.[0-9](?:_?[0-9])*')|\.[0-9](?:_?[0-9])*)" "([eE][+-]?[0-9](?:_?[0-9])*)?[jJ]?\b", r"\b[0-9](?:_?[0-9])*([eE][+-]?[0-9](?:_?[0-9])*)?[jJ]?\b", r"\b[0-9](?:_?[0-9])*[jJ]\b", ] number = any("number", number_regex) sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?" dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?' uf_sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*(\\)$(?!')$" uf_dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*(\\)$(?!")$' sq3string = r"(\b[rRuU])?'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?" dq3string = r'(\b[rRuU])?"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?' uf_sq3string = r"(\b[rRuU])?'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(\\)?(?!''')$" uf_dq3string = r'(\b[rRuU])?"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(\\)?(?!""")$' string = any("string", [sq3string, dq3string, sqstring, dqstring]) ufstring1 = any("uf_sqstring", [uf_sqstring]) ufstring2 = any("uf_dqstring", [uf_dqstring]) ufstring3 = any("uf_sq3string", [uf_sq3string]) ufstring4 = any("uf_dq3string", [uf_dq3string]) return "|".join( [ instance, kw, builtin, comment, ufstring1, ufstring2, ufstring3, ufstring4, string, number, any("SYNC", [r"\n"]), ] ) class PythonSH(BaseSH): """Python Syntax Highlighter""" # Syntax highlighting rules: add_kw = ["async", "await"] PROG = re.compile(make_python_patterns(additional_keywords=add_kw), re.S) IDPROG = re.compile(r"\s+(\w+)", re.S) ASPROG = re.compile(r".*?\b(as)\b") # Syntax highlighting states (from one text block to another): ( NORMAL, INSIDE_SQ3STRING, INSIDE_DQ3STRING, INSIDE_SQSTRING, INSIDE_DQSTRING, ) = list(range(5)) # Comments suitable for Outline Explorer OECOMMENT = re.compile(r"^(# ?--[-]+|##[#]+ )[ -]*[^- ]+") def __init__(self, parent, font=None, color_scheme=None): BaseSH.__init__(self, parent, font, color_scheme) self.import_statements = {} self.found_cell_separators = False def highlightBlock(self, text): """ :param text: """ text = str(text) prev_state = self.previousBlockState() if prev_state == self.INSIDE_DQ3STRING: offset = -4 text = r'""" ' + text elif prev_state == self.INSIDE_SQ3STRING: offset = -4 text = r"''' " + text elif prev_state == self.INSIDE_DQSTRING: offset = -2 text = r'" ' + text elif prev_state == self.INSIDE_SQSTRING: offset = -2 text = r"' " + text else: offset = 0 prev_state = self.NORMAL import_stmt = None self.setFormat(0, len(text), self.formats["normal"]) state = self.NORMAL match = self.PROG.search(text) while match: for key, value in list(match.groupdict().items()): if value: start, end = match.span(key) start = max([0, start + offset]) end = max([0, end + offset]) if key == "uf_sq3string": self.setFormat(start, end - start, self.formats["string"]) state = self.INSIDE_SQ3STRING elif key == "uf_dq3string": self.setFormat(start, end - start, self.formats["string"]) state = self.INSIDE_DQ3STRING elif key == "uf_sqstring": self.setFormat(start, end - start, self.formats["string"]) state = self.INSIDE_SQSTRING elif key == "uf_dqstring": self.setFormat(start, end - start, self.formats["string"]) state = self.INSIDE_DQSTRING else: self.setFormat(start, end - start, self.formats[key]) if key == "keyword": if value in ("def", "class"): match1 = self.IDPROG.match(text, end) if match1: start1, end1 = match1.span(1) self.setFormat( start1, end1 - start1, self.formats["definition"], ) elif value == "import": import_stmt = text.strip() # color all the "as" words on same line, except # if in a comment; cheap approximation to the # truth if "#" in text: endpos = text.index("#") else: endpos = len(text) while True: match1 = self.ASPROG.match(text, end, endpos) if not match1: break start, end = match1.span(1) self.setFormat( start, end - start, self.formats["keyword"] ) match = self.PROG.search(text, match.end()) self.setCurrentBlockState(state) # Use normal format for indentation and trailing spaces. self.formats["leading"] = self.formats["normal"] self.formats["trailing"] = self.formats["normal"] self.highlight_spaces(text, offset) if import_stmt is not None: block_nb = self.currentBlock().blockNumber() self.import_statements[block_nb] = import_stmt def get_import_statements(self): """ :return: """ return list(self.import_statements.values()) def rehighlight(self): """ """ self.import_statements = {} self.found_cell_separators = False BaseSH.rehighlight(self) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640594314.0 guidata-2.0.2/guidata/widgets/texteditor.py0000666000000000000000000000766600000000000015674 0ustar00# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """ guidata.widgets.texteditor ========================== This package provides a text editor widget based on QtGui.QPlainTextEdit. .. autoclass:: TextEditor """ from qtpy.QtWidgets import ( QDialog, QHBoxLayout, QPushButton, QTextEdit, QVBoxLayout, ) from qtpy.QtCore import ( Qt, Slot, ) from guidata.configtools import get_font, get_icon from guidata.config import CONF, _ from guidata.qthelpers import win32_fix_title_bar_background class TextEditor(QDialog): """Array Editor Dialog""" def __init__( self, text, title="", font=None, parent=None, readonly=False, size=(400, 300) ): QDialog.__init__(self, parent) win32_fix_title_bar_background(self) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.text = None self.btn_save_and_close = None # Display text as unicode if it comes as bytes, so users see # its right representation if isinstance(text, bytes): self.is_binary = True text = str(text, "utf8") else: self.is_binary = False self.layout = QVBoxLayout() self.setLayout(self.layout) # Text edit self.edit = QTextEdit(parent) self.edit.setReadOnly(readonly) self.edit.textChanged.connect(self.text_changed) self.edit.setPlainText(text) if font is None: font = get_font(CONF, "texteditor") self.edit.setFont(font) self.layout.addWidget(self.edit) # Buttons configuration btn_layout = QHBoxLayout() btn_layout.addStretch() if not readonly: self.btn_save_and_close = QPushButton(_("Save and Close")) self.btn_save_and_close.setDisabled(True) self.btn_save_and_close.clicked.connect(self.accept) btn_layout.addWidget(self.btn_save_and_close) self.btn_close = QPushButton(_("Close")) self.btn_close.setAutoDefault(True) self.btn_close.setDefault(True) self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) self.layout.addLayout(btn_layout) # Make the dialog act as a window self.setWindowFlags(Qt.Window) self.setWindowIcon(get_icon("edit.png")) self.setWindowTitle( _("Text editor") + "%s" % (" - " + str(title) if str(title) else "") ) self.resize(size[0], size[1]) @Slot() def text_changed(self): """Text has changed""" # Save text as bytes, if it was initially bytes if self.is_binary: self.text = bytes(self.edit.toPlainText(), "utf8") else: self.text = str(self.edit.toPlainText()) if self.btn_save_and_close: self.btn_save_and_close.setEnabled(True) self.btn_save_and_close.setAutoDefault(True) self.btn_save_and_close.setDefault(True) def get_value(self): """Return modified text""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.text def setup_and_check(self, value): """Verify if TextEditor is able to display strings passed to it.""" if isinstance(value, str): return True try: str(value, "utf8") return True except: return False ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640626111.8786583 guidata-2.0.2/guidata.egg-info/0000777000000000000000000000000000000000000013134 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640626109.0 guidata-2.0.2/guidata.egg-info/PKG-INFO0000666000000000000000000000473700000000000014244 0ustar00Metadata-Version: 2.1 Name: guidata Version: 2.0.2 Summary: Automatic graphical user interfaces generation for easy dataset editing and display Home-page: https://github.com/PierreRaybaut/guidata Author: Pierre Raybaut Author-email: pierre.raybaut@gmail.com License: CeCILL V2 Platform: UNKNOWN Classifier: Topic :: Scientific/Engineering Classifier: Development Status :: 5 - Production/Stable Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python :: 3 Provides-Extra: Doc License-File: Licence_CeCILL_V2-en.txt guidata: Automatic GUI generation for easy dataset editing and display with Python ====================================================================================== Simple example of ``guidata`` datasets embedded in an application window: .. image:: https://raw.githubusercontent.com/PierreRaybaut/guidata/master/doc/images/screenshots/editgroupbox.png See `documentation`_ for more details on the library and `changelog`_ for recent history of changes. Copyright © 2009-2021 CEA, Pierre Raybaut, licensed under the terms of the `CECILL License`_. .. _documentation: https://guidata.readthedocs.io/en/latest/ .. _changelog: https://github.com/PierreRaybaut/guidata/blob/master/CHANGELOG.md .. _CECILL License: https://github.com/PierreRaybaut/guidata/blob/master/Licence_CeCILL_V2-en.txt Overview -------- Based on the Qt library, ``guidata`` is a Python library generating graphical user interfaces for easy dataset editing and display. It also provides helpers and application development tools for Qt (PyQt5, PySide2, PyQt6, PySide6). Generate GUIs to edit and display all kind of objects: - integers, floats, strings ; - ndarrays (NumPy's n-dimensional arrays) ; - etc. Application development tools: - configuration management - internationalization (``gettext``) - deployment tools - HDF5 I/O helpers - misc. utils Building, installation, ... --------------------------- The following package is **required**: `PyQt5`_ (or `PySide2`_). .. _PyQt5: https://pypi.python.org/pypi/PyQt5 .. _PySide2: https://pypi.python.org/pypi/PySide2 .. _h5py: https://pypi.python.org/pypi/h5py See the `README`_ and `documentation`_ for more details. .. _README: https://github.com/PierreRaybaut/guidata/blob/master/README.md ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640626111.0 guidata-2.0.2/guidata.egg-info/SOURCES.txt0000666000000000000000000001140400000000000015020 0ustar00CHANGELOG.md Licence_CeCILL_V2-en.txt MANIFEST.in README.md setup.py C:/Python/guidata/doctmp/guidata.chm doc/basic_example.py doc/conf.py doc/development.rst doc/examples.rst doc/index.rst doc/installation.rst doc/overview.rst doc/_static/favicon.ico doc/images/basic_example.png doc/images/guidata.png doc/images/screenshots/__init__.png doc/images/screenshots/activable_dataset.png doc/images/screenshots/all_features.png doc/images/screenshots/all_items.png doc/images/screenshots/bool_selector.png doc/images/screenshots/datasetgroup.png doc/images/screenshots/editgroupbox.png doc/reference/configtools.rst doc/reference/dataset.rst doc/reference/disthelpers.rst doc/reference/index.rst doc/reference/qthelpers.rst doc/reference/userconfig.rst doc/reference/utils.rst guidata/__init__.py guidata/config.py guidata/configtools.py guidata/disthelpers.py guidata/encoding.py guidata/gettext_helpers.py guidata/guitest.py guidata/hdf5io.py guidata/qthelpers.py guidata/qtwidgets.py guidata/userconfig.py guidata/userconfigio.py guidata/utils.py guidata.egg-info/PKG-INFO guidata.egg-info/SOURCES.txt guidata.egg-info/dependency_links.txt guidata.egg-info/entry_points.txt guidata.egg-info/requires.txt guidata.egg-info/top_level.txt guidata/dataset/__init__.py guidata/dataset/dataitems.py guidata/dataset/datatypes.py guidata/dataset/qtitemwidgets.py guidata/dataset/qtwidgets.py guidata/dataset/textedit.py guidata/external/__init__.py guidata/external/darkdetect/__init__.py guidata/external/darkdetect/_dummy.py guidata/external/darkdetect/_linux_detect.py guidata/external/darkdetect/_mac_detect.py guidata/external/darkdetect/_windows_detect.py guidata/images/apply.png guidata/images/arredit.png guidata/images/busy.png guidata/images/cell_edit.png guidata/images/copy.png guidata/images/delete.png guidata/images/dictedit.png guidata/images/edit.png guidata/images/exit.png guidata/images/expander_down.png guidata/images/expander_right.png guidata/images/file.png guidata/images/fileclose.png guidata/images/fileimport.png guidata/images/filenew.png guidata/images/fileopen.png guidata/images/filesave.png guidata/images/filesaveas.png guidata/images/guidata.svg guidata/images/max.png guidata/images/min.png guidata/images/none.png guidata/images/not_found.png guidata/images/python.png guidata/images/quickview.png guidata/images/save_all.png guidata/images/selection.png guidata/images/settings.png guidata/images/shape.png guidata/images/xmax.png guidata/images/xmin.png guidata/images/editors/edit.png guidata/images/editors/edit_add.png guidata/images/editors/editcopy.png guidata/images/editors/editdelete.png guidata/images/editors/editpaste.png guidata/images/editors/fileimport.png guidata/images/editors/filesave.png guidata/images/editors/imshow.png guidata/images/editors/insert.png guidata/images/editors/plot.png guidata/images/editors/rename.png guidata/images/filetypes/doc.png guidata/images/filetypes/gif.png guidata/images/filetypes/html.png guidata/images/filetypes/jpg.png guidata/images/filetypes/pdf.png guidata/images/filetypes/png.png guidata/images/filetypes/pps.png guidata/images/filetypes/ps.png guidata/images/filetypes/tar.png guidata/images/filetypes/tgz.png guidata/images/filetypes/tif.png guidata/images/filetypes/txt.png guidata/images/filetypes/xls.png guidata/images/filetypes/zip.png guidata/locale/guidata.pot guidata/locale/fr/LC_MESSAGES/guidata.mo guidata/locale/fr/LC_MESSAGES/guidata.po guidata/tests/__init__.py guidata/tests/activable_dataset.py guidata/tests/activable_items.py guidata/tests/all_features.py guidata/tests/all_items.py guidata/tests/bool_selector.py guidata/tests/callbacks.py guidata/tests/config.py guidata/tests/data.py guidata/tests/datasetgroup.py guidata/tests/disthelpers.py guidata/tests/editgroupbox.py guidata/tests/hdf5.py guidata/tests/inheritance.py guidata/tests/item_order.py guidata/tests/rotatedlabel.py guidata/tests/test_arrayeditor.py guidata/tests/test_codeeditor.py guidata/tests/test_collectionseditor.py guidata/tests/test_console.py guidata/tests/test_dataframeeditor.py guidata/tests/test_importwizard.py guidata/tests/test_objecteditor.py guidata/tests/text.py guidata/tests/translations.py guidata/tests/userconfig_app.py guidata/widgets/__init__.py guidata/widgets/arrayeditor.py guidata/widgets/codeeditor.py guidata/widgets/collectionseditor.py guidata/widgets/dataframeeditor.py guidata/widgets/importwizard.py guidata/widgets/nsview.py guidata/widgets/objecteditor.py guidata/widgets/syntaxhighlighters.py guidata/widgets/texteditor.py guidata/widgets/console/__init__.py guidata/widgets/console/base.py guidata/widgets/console/calltip.py guidata/widgets/console/dochelpers.py guidata/widgets/console/internalshell.py guidata/widgets/console/interpreter.py guidata/widgets/console/mixins.py guidata/widgets/console/shell.py guidata/widgets/console/terminal.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640626109.0 guidata-2.0.2/guidata.egg-info/dependency_links.txt0000666000000000000000000000000100000000000017202 0ustar00 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640626109.0 guidata-2.0.2/guidata.egg-info/entry_points.txt0000666000000000000000000000006100000000000016427 0ustar00[gui_scripts] guidata-tests = guidata.tests:run ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640626109.0 guidata-2.0.2/guidata.egg-info/requires.txt0000666000000000000000000000003500000000000015532 0ustar00QtPy>=1.3 [Doc] Sphinx>=1.1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640626109.0 guidata-2.0.2/guidata.egg-info/top_level.txt0000666000000000000000000000001000000000000015655 0ustar00guidata ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1640626112.175523 guidata-2.0.2/setup.cfg0000666000000000000000000000005200000000000011642 0ustar00[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640602998.0 guidata-2.0.2/setup.py0000666000000000000000000001272100000000000011541 0ustar00# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guidata/__init__.py for details) """ guidata ======= Set of basic GUIs to edit and display objects of many kinds: - integers, floats, strings ; - ndarrays (NumPy's n-dimensional arrays) ; - etc. Copyright © 2009-2015 CEA Pierre Raybaut Licensed under the terms of the CECILL License (see guidata/__init__.py for details) """ import setuptools # analysis:ignore from distutils.core import setup import sys import os import os.path as osp import shutil import atexit import subprocess from guidata.utils import get_subpackages, get_package_data LIBNAME = "guidata" from guidata import __version__ as version DESCRIPTION = ( "Automatic graphical user interfaces generation for easy " "dataset editing and display" ) LONG_DESCRIPTION = """\ guidata: Automatic GUI generation for easy dataset editing and display with Python ====================================================================================== Simple example of ``guidata`` datasets embedded in an application window: .. image:: https://raw.githubusercontent.com/PierreRaybaut/guidata/master/doc/images/screenshots/editgroupbox.png See `documentation`_ for more details on the library and `changelog`_ for recent history of changes. Copyright © 2009-2021 CEA, Pierre Raybaut, licensed under the terms of the `CECILL License`_. .. _documentation: https://guidata.readthedocs.io/en/latest/ .. _changelog: https://github.com/PierreRaybaut/guidata/blob/master/CHANGELOG.md .. _CECILL License: https://github.com/PierreRaybaut/guidata/blob/master/Licence_CeCILL_V2-en.txt Overview -------- Based on the Qt library, ``guidata`` is a Python library generating graphical user interfaces for easy dataset editing and display. It also provides helpers and application development tools for Qt (PyQt5, PySide2, PyQt6, PySide6). Generate GUIs to edit and display all kind of objects: - integers, floats, strings ; - ndarrays (NumPy's n-dimensional arrays) ; - etc. Application development tools: - configuration management - internationalization (``gettext``) - deployment tools - HDF5 I/O helpers - misc. utils Building, installation, ... --------------------------- The following package is **required**: `PyQt5`_ (or `PySide2`_). .. _PyQt5: https://pypi.python.org/pypi/PyQt5 .. _PySide2: https://pypi.python.org/pypi/PySide2 .. _h5py: https://pypi.python.org/pypi/h5py See the `README`_ and `documentation`_ for more details. .. _README: https://github.com/PierreRaybaut/guidata/blob/master/README.md""" KEYWORDS = "" CLASSIFIERS = ["Topic :: Scientific/Engineering"] if "beta" in version or "b" in version: CLASSIFIERS += ["Development Status :: 4 - Beta"] elif "alpha" in version or "a" in version: CLASSIFIERS += ["Development Status :: 3 - Alpha"] else: CLASSIFIERS += ["Development Status :: 5 - Production/Stable"] def build_chm_doc(libname): """Return CHM documentation file (on Windows only), which is copied under {PythonInstallDir}\Doc, hence allowing Spyder to add an entry for opening package documentation in "Help" menu. This has no effect on a source distribution.""" args = "".join(sys.argv) if os.name == "nt" and ("bdist" in args or "build" in args): try: import sphinx # analysis:ignore except ImportError: print( "Warning: `sphinx` is required to build documentation", file=sys.stderr ) return hhc_base = r"C:\Program Files%s\HTML Help Workshop\hhc.exe" for hhc_exe in (hhc_base % "", hhc_base % " (x86)"): if osp.isfile(hhc_exe): break else: print( "Warning: `HTML Help Workshop` is required to build CHM " "documentation file", file=sys.stderr, ) return doctmp_dir = "doctmp" subprocess.call("sphinx-build -b htmlhelp doc %s" % doctmp_dir, shell=True) atexit.register(shutil.rmtree, osp.abspath(doctmp_dir)) fname = osp.abspath(osp.join(doctmp_dir, "%s.chm" % libname)) subprocess.call('"%s" %s' % (hhc_exe, fname), shell=True) if osp.isfile(fname): return fname else: print("Warning: CHM building process failed", file=sys.stderr) CHM_DOC = build_chm_doc(LIBNAME) setup( name=LIBNAME, version=version, description=DESCRIPTION, long_description=LONG_DESCRIPTION, packages=get_subpackages(LIBNAME), package_data={LIBNAME: get_package_data(LIBNAME, (".png", ".svg", ".mo"))}, data_files=[(r"Doc", [CHM_DOC])] if CHM_DOC else [], install_requires=["QtPy>=1.3"], entry_points={ "gui_scripts": [ "guidata-tests = guidata.tests:run", ] }, extras_require={ "Doc": ["Sphinx>=1.1"], }, author="Pierre Raybaut", author_email="pierre.raybaut@gmail.com", url="https://github.com/PierreRaybaut/%s" % LIBNAME, license="CeCILL V2", classifiers=CLASSIFIERS + [ "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python :: 3", ], )