pyqttoolkit 0.8.0.dev1__tar.gz → 0.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/PKG-INFO +6 -5
  2. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/_version.py +3 -3
  3. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/application/application.py +1 -1
  4. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/config/environment.py +8 -1
  5. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/data/__init__.py +1 -0
  6. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/data/interval.py +8 -11
  7. pyqttoolkit-0.8.2/pyqttoolkit/data/message.py +29 -0
  8. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/datetime/qdatetime.py +1 -1
  9. pyqttoolkit-0.8.2/pyqttoolkit/exceptions/__init__.py +18 -0
  10. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/logs/__init__.py +29 -0
  11. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/models/code_text.py +7 -3
  12. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/models/dicttable.py +6 -3
  13. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/models/span.py +36 -31
  14. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/scripting/script_runner.py +28 -12
  15. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/event_registry.py +1 -0
  16. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/file_dialog.py +23 -5
  17. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/message_board.py +21 -19
  18. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/task_runner.py +3 -4
  19. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/tool_window_service.py +3 -2
  20. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/bulk_value_selector.py +3 -2
  21. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/code/code_edit.py +3 -2
  22. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/datetime.py +7 -5
  23. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/datetime_range_selector.py +55 -10
  24. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/plot/legend_control.py +8 -1
  25. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/plot/matplotlib/__init__.py +23 -23
  26. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/plot/matplotlib/base.py +169 -56
  27. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/plot/plot_options.py +6 -1
  28. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/plot/plot_toolbar_options.py +2 -2
  29. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/popup.py +3 -1
  30. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit.egg-info/PKG-INFO +6 -5
  31. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit.egg-info/SOURCES.txt +2 -0
  32. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit.egg-info/requires.txt +3 -1
  33. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/setup.py +5 -5
  34. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/MANIFEST.in +0 -0
  35. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/README.md +0 -0
  36. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/__init__.py +0 -0
  37. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/application/__init__.py +0 -0
  38. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/application/garbage_collector.py +0 -0
  39. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/colors/__init__.py +0 -0
  40. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/config/__init__.py +0 -0
  41. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/config/base.py +0 -0
  42. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/config/composite.py +0 -0
  43. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/config/default.py +0 -0
  44. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/config/multi_json_file.py +0 -0
  45. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/config/with_signal.py +0 -0
  46. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/config/with_value_changed.py +0 -0
  47. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/data/bits.py +0 -0
  48. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/data/cache.py +0 -0
  49. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/data/names.py +0 -0
  50. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/datetime/__init__.py +0 -0
  51. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/datetime/datetime.py +0 -0
  52. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/dependencies/__init__.py +0 -0
  53. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/errors/__init__.py +0 -0
  54. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/filesystem/__init__.py +0 -0
  55. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/metadata/__init__.py +0 -0
  56. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/metadata/dataframe_metadata_manager.py +0 -0
  57. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/metadata/dict_metadata_manager.py +0 -0
  58. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/metadata/id_generator.py +0 -0
  59. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/metadata/metadata_manager_base.py +0 -0
  60. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/models/__init__.py +0 -0
  61. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/models/dataframetable.py +0 -0
  62. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/models/dictpropertytree.py +0 -0
  63. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/models/json_text.py +0 -0
  64. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/models/list_model.py +0 -0
  65. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/models/module.py +0 -0
  66. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/models/roles.py +0 -0
  67. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/models/series_data.py +0 -0
  68. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/models/validation.py +0 -0
  69. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/modules/__init__.py +0 -0
  70. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/modules/events/__init__.py +0 -0
  71. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/modules/events/add_signal.py +0 -0
  72. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/modules/events/module_opening.py +0 -0
  73. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/modules/events/rejectable.py +0 -0
  74. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/modules/launcher_config.py +0 -0
  75. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/modules/module.py +0 -0
  76. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/properties/__init__.py +0 -0
  77. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/properties/auto.py +0 -0
  78. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/properties/throttled.py +0 -0
  79. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/scripting/__init__.py +0 -0
  80. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/scripting/script_context.py +0 -0
  81. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/scripting/script_template.py +0 -0
  82. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/__init__.py +0 -0
  83. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/autosave.py +0 -0
  84. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/email.py +0 -0
  85. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/hotkey_manager.py +0 -0
  86. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/link_manager.py +0 -0
  87. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/module_service.py +0 -0
  88. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/object_converter.py +0 -0
  89. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/project_updater.py +0 -0
  90. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/services/theme_manager.py +0 -0
  91. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/viewmanagers/__init__.py +0 -0
  92. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/viewmanagers/tool.py +0 -0
  93. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/__init__.py +0 -0
  94. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/check_box.py +0 -0
  95. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/code/__init__.py +0 -0
  96. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/code/python_syntax.py +0 -0
  97. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/combo_box.py +0 -0
  98. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/count_selector.py +0 -0
  99. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/default_text_line_edit.py +0 -0
  100. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/delegates.py +0 -0
  101. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/dtype_edit.py +0 -0
  102. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/editable_combo_box.py +0 -0
  103. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/file_selector.py +0 -0
  104. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/icon.py +0 -0
  105. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/icon_button.py +0 -0
  106. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/json_edit.py +0 -0
  107. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/labelled.py +0 -0
  108. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/layout/__init__.py +0 -0
  109. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/layout/collabsible.py +0 -0
  110. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/layout/group_box.py +0 -0
  111. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/layout/group_panel.py +0 -0
  112. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/layout/lines.py +0 -0
  113. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/layout/link.py +0 -0
  114. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/line_edit.py +0 -0
  115. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/linkable.py +0 -0
  116. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/list_view.py +0 -0
  117. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/main_window.py +0 -0
  118. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/metadata_selector.py +0 -0
  119. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/modulewindow.py +0 -0
  120. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/plot/__init__.py +0 -0
  121. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/plot/matplotlib/colorbar.py +0 -0
  122. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/plot/matplotlib/colormap.py +0 -0
  123. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/plot/matplotlib/font.py +0 -0
  124. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/plot/size.py +0 -0
  125. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/plot/tool_type.py +0 -0
  126. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/plot/toolbar.py +0 -0
  127. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/popout.py +0 -0
  128. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/property_tree.py +0 -0
  129. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/shrunk_push_button.py +0 -0
  130. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/steppable_combo_box.py +0 -0
  131. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/styleable.py +0 -0
  132. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/table.py +0 -0
  133. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/text_edit.py +0 -0
  134. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit/views/tool_window.py +0 -0
  135. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit.egg-info/dependency_links.txt +0 -0
  136. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/pyqttoolkit.egg-info/top_level.txt +0 -0
  137. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/setup.cfg +0 -0
  138. {pyqttoolkit-0.8.0.dev1 → pyqttoolkit-0.8.2}/versioneer.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyqttoolkit
3
- Version: 0.8.0.dev1
3
+ Version: 0.8.2
4
4
  Summary: A toolkit for PyQt 5
5
- Home-page: https://github.com/Simmovation/pyqttoolkit
6
- Author: Simmovation Ltd
7
- Author-email: info@simmovation.tech
5
+ Home-page: https://github.com/BitBloomTech/pyqttoolkit
6
+ Author: Bitbloom Ltd
7
+ Author-email: info@bitbloom.tech
8
8
  License: GPLv3
9
9
  Description: # PyQtToolkit
10
10
 
@@ -12,5 +12,6 @@ Description: # PyQtToolkit
12
12
  Platform: any
13
13
  Requires-Python: >=3.6
14
14
  Description-Content-Type: text/markdown
15
- Provides-Extra: publish
16
15
  Provides-Extra: test
16
+ Provides-Extra: ui
17
+ Provides-Extra: publish
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2022-11-24T10:18:56+0000",
11
+ "date": "2025-08-20T11:31:02+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "3235793eccec3f7d7b99aa7e3734985153e3242e",
15
- "version": "0.8.0.dev1"
14
+ "full-revisionid": "821c2bf119c1f87df7dabc550793fed84592812d",
15
+ "version": "0.8.2"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -30,7 +30,7 @@ class Application(QApplication):
30
30
  def __init__(self, argv):
31
31
  QApplication.__init__(self, argv)
32
32
  self._dependency_container = DependencyContainer()
33
- self._garbage_collector = GarbageCollector(self, None)
33
+ self._garbage_collector = GarbageCollector(self)
34
34
 
35
35
  self._module_service = None
36
36
  self._message_board = None
@@ -15,11 +15,18 @@
15
15
  # along with this program; if not, write to the Free Software Foundation,
16
16
  # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
  import os
18
+ import json
18
19
  from .base import BaseApplicationConfiguration
19
20
 
20
21
  class EnvironmentApplicationConfiguration(BaseApplicationConfiguration):
21
22
  def get_value(self, key):
22
- return os.getenv(self._format_key(key))
23
+ value = os.getenv(self._format_key(key))
24
+ if value is None:
25
+ return value
26
+ try:
27
+ return json.loads(value)
28
+ except json.decoder.JSONDecodeError:
29
+ return value
23
30
 
24
31
  def set_value(self, key, value):
25
32
  os.environ[self._format_key(key)] = str(value)
@@ -18,3 +18,4 @@ from .cache import Cache
18
18
  from .interval import calculate_interval
19
19
  from .bits import get_next_available_bit
20
20
  from .names import get_next_available_name
21
+ from .message import MessageType, MessageResponse
@@ -14,16 +14,13 @@
14
14
  # You should have received a copy of the GNU General Public License
15
15
  # along with this program; if not, write to the Free Software Foundation,
16
16
  # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ import pandas as pd
18
+
17
19
  def calculate_interval(values, sort=True):
18
- interval = None
19
- last_value = None
20
+ if len(values) < 2:
21
+ return None
22
+ values_series = pd.Series(values)
20
23
  if sort:
21
- values = sorted(values)
22
- for value in values:
23
- if last_value is not None:
24
- next_interval = value - last_value
25
- if next_interval == interval:
26
- return interval
27
- interval = next_interval
28
- last_value = value
29
- return interval
24
+ values_series = values_series.sort_values()
25
+ # We want to ensure that the calculated interval is accurate for 90% of values to avoid mistakenly losing values
26
+ return (values_series - values_series.shift(1)).quantile(0.1)
@@ -0,0 +1,29 @@
1
+ # pyqttoolkit
2
+ # Copyright (C) 2018-2019, Simmovation Ltd.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software Foundation,
16
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ from enum import Enum, IntEnum
18
+
19
+ class MessageType(Enum):
20
+ validation_error = 0
21
+ confirmation = 1
22
+ application_exception = 2
23
+
24
+ class MessageResponse(IntEnum):
25
+ ok = 1
26
+ cancel = 2
27
+ save = 4
28
+ discard = 8
29
+ ignore = 16
@@ -37,7 +37,7 @@ def pd_timestamp_to_q_datetime(pd_timestamp: datetime):
37
37
  return QDateTime()
38
38
  return QDateTime(
39
39
  QDate(pd_timestamp.year, pd_timestamp.month, pd_timestamp.day),
40
- QTime(pd_timestamp.hour, pd_timestamp.minute, pd_timestamp.second, pd_timestamp.microsecond / 1000)
40
+ QTime(pd_timestamp.hour, pd_timestamp.minute, pd_timestamp.second, pd_timestamp.microsecond // 1000)
41
41
  )
42
42
 
43
43
  def step_qdatetime(control, fraction, interval, range_start, range_end):
@@ -0,0 +1,18 @@
1
+ # pyqttoolkit
2
+ # Copyright (C) 2018-2019, Simmovation Ltd.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software Foundation,
16
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ class TaskCancelled(Exception):
18
+ pass
@@ -14,6 +14,8 @@
14
14
  # You should have received a copy of the GNU General Public License
15
15
  # along with this program; if not, write to the Free Software Foundation,
16
16
  # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ from typing import Callable
18
+ from io import StringIO
17
19
  import logging
18
20
  from logging import handlers
19
21
  from functools import wraps
@@ -109,3 +111,30 @@ def logf(level, log_args=True):
109
111
  return result
110
112
  return _
111
113
  return logf_decorator
114
+
115
+
116
+ class LogStream:
117
+ def __init__(self, notify_update: Callable = None):
118
+ self._buffer = StringIO()
119
+ self._notify_update = notify_update
120
+
121
+ def __iadd__(self, other):
122
+ self.write(other)
123
+ return self
124
+
125
+ def write(self, message):
126
+ self._buffer.write(message)
127
+ if self._notify_update:
128
+ self._notify_update()
129
+
130
+ def flush(self):
131
+ self._buffer.flush()
132
+
133
+ def getvalue(self):
134
+ return self._buffer.getvalue()
135
+
136
+ def clear(self):
137
+ self._buffer.close()
138
+ self._buffer = StringIO()
139
+ if self._notify_update:
140
+ self._notify_update()
@@ -17,6 +17,7 @@
17
17
  from PyQt5.QtCore import QObject, pyqtSignal
18
18
 
19
19
  from pyqttoolkit.properties import AutoProperty
20
+ from pyqttoolkit.logs import LogStream
20
21
 
21
22
  class CodeTextModel(QObject):
22
23
  def __init__(self, parent, validator):
@@ -24,16 +25,19 @@ class CodeTextModel(QObject):
24
25
  self.textChanged.connect(self._validate)
25
26
  self._validator = validator
26
27
  self.text = ''
27
- self.output = ''
28
+ self.output = LogStream(self._handle_output_buffer_changed)
28
29
 
29
30
  textChanged = pyqtSignal(str)
30
- outputChanged = pyqtSignal(str)
31
+ outputChanged = pyqtSignal(LogStream)
31
32
  validationMessageChanged = pyqtSignal(str)
32
33
 
33
34
  text = AutoProperty(str)
34
- output = AutoProperty(str)
35
+ output = AutoProperty(LogStream)
35
36
  validationMessage = AutoProperty(str)
36
37
 
37
38
  def _validate(self, value):
38
39
  validation_model = self._validator(value)
39
40
  self.validationMessage = validation_model.error if validation_model else ''
41
+
42
+ def _handle_output_buffer_changed(self):
43
+ self.outputChanged.emit(self.output)
@@ -61,11 +61,14 @@ class DictTableModel(QAbstractTableModel):
61
61
  value = self._value[self._ids[index.row()]].get(self.displayProperties[index.column()])
62
62
  if value is None:
63
63
  return None
64
- if role == Qt.DisplayRole and isinstance(value, float):
65
- return '{:0.2f}'.format(value)
66
- return value
64
+ return self._format_value(value, index)
67
65
  return None
68
66
 
67
+ def _format_value(self, value, index):
68
+ if isinstance(value, float):
69
+ return '{:0.2f}'.format(value)
70
+ return value
71
+
69
72
  def setData(self, index, value, role=None):
70
73
  role = role or Qt.EditRole
71
74
  if role == Qt.EditRole:
@@ -1,31 +1,36 @@
1
- # pyqttoolkit
2
- # Copyright (C) 2018-2019, Simmovation Ltd.
3
- #
4
- # This program is free software; you can redistribute it and/or modify
5
- # it under the terms of the GNU General Public License as published by
6
- # the Free Software Foundation; either version 3 of the License, or
7
- # (at your option) any later version.
8
- #
9
- # This program is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
- # GNU General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU General Public License
15
- # along with this program; if not, write to the Free Software Foundation,
16
- # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
- from PyQt5.QtCore import QObject
18
-
19
- class SpanModel(QObject):
20
- def __init__(self, parent, left, right):
21
- QObject.__init__(self, parent)
22
- self._left = left
23
- self._right = right
24
-
25
- @property
26
- def left(self):
27
- return self._left
28
-
29
- @property
30
- def right(self):
31
- return self._right
1
+ # pyqttoolkit
2
+ # Copyright (C) 2018-2019, Simmovation Ltd.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software Foundation,
16
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ from PyQt5.QtCore import QObject
18
+
19
+ class SpanModel(QObject):
20
+ def __init__(self, parent, left, right, radians=False):
21
+ QObject.__init__(self, parent)
22
+ self._left = left
23
+ self._right = right
24
+ self._radians = radians
25
+
26
+ @property
27
+ def left(self):
28
+ return self._left
29
+
30
+ @property
31
+ def right(self):
32
+ return self._right
33
+
34
+ @property
35
+ def radians(self):
36
+ return self._radians
@@ -15,6 +15,7 @@
15
15
  # along with this program; if not, write to the Free Software Foundation,
16
16
  # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
  from contextlib import contextmanager
18
+ from functools import partial
18
19
  import re
19
20
  import builtins
20
21
  import sys
@@ -80,21 +81,36 @@ class ScriptRunner:
80
81
  self._context = None
81
82
 
82
83
  def _globals(self, context):
83
- return {'__builtins__': {**__builtins__, '__import__': self._import, 'exit': self._exit, **{g: self._not_implemented for g in self._disallowed_globals}}}
84
+ return {'__builtins__': {**__builtins__, '__import__': self._import(builtins.__import__), 'exit': self._exit, **{g: self._not_implemented for g in self._disallowed_globals}}}
84
85
 
85
86
  def compile(self, script):
86
87
  return compile(script, 'code', 'exec')
87
-
88
- def _import(self, module_name, *args, **kwargs):
89
- if any(re.match(m, module_name) for m in self._disallowed_modules):
90
- raise ImportError('Module not found')
91
- try:
92
-
93
- with add_paths(self._additional_paths):
94
- module = builtins.__import__(module_name, *args, **kwargs)
95
- return module
96
- except KeyError:
97
- raise ImportError('Module not found')
88
+
89
+ def _patch_plt_show(self, module):
90
+ def plt_show_as_show_figure(plt, block=None):
91
+ # Block is not used in the context of the script runner
92
+ for no, label in zip(plt.get_fignums(), plt.get_figlabels()):
93
+ self._context.show_figure(plt.figure(no), str(label))
94
+ plt.close('all')
95
+ if module.__name__ == 'matplotlib.pyplot':
96
+ module.show = partial(plt_show_as_show_figure, module)
97
+ if module.__name__ == 'matplotlib':
98
+ module.pyplot.show = partial(plt_show_as_show_figure, module.pyplot)
99
+ return module
100
+
101
+ def _import(self, import_function):
102
+ def _(module_name, *args, **kwargs):
103
+ if any(re.match(m, module_name) for m in self._disallowed_modules):
104
+ raise ImportError('Module not found')
105
+ try:
106
+ with add_paths(self._additional_paths):
107
+ module = import_function(module_name, *args, **kwargs)
108
+ if module_name in 'importlib':
109
+ module.import_module = self._import(importlib.import_module)
110
+ return self._patch_plt_show(module)
111
+ except KeyError:
112
+ raise ImportError('Module not found')
113
+ return _
98
114
 
99
115
  def _additional_paths(self):
100
116
  return self._additional_paths
@@ -21,6 +21,7 @@ class EventTypes:
21
21
  save_requested = 'save_requested'
22
22
  close_project_requested = 'close_project_requested'
23
23
  project_closed = 'project_closed'
24
+ save_dialog_finished = 'save_dialog_finished'
24
25
 
25
26
  class FutureConnector:
26
27
  def __init__(self):
@@ -16,16 +16,32 @@
16
16
  # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
  from os import path
18
18
 
19
- from PyQt5.QtCore import QStandardPaths
19
+ from PyQt5.QtCore import QStandardPaths, QTimer
20
20
  from PyQt5.QtWidgets import QFileDialog
21
21
 
22
+ from .event_registry import EventTypes
23
+
22
24
  class FileDialogService:
23
- def __init__(self, project_manager, application_configuration):
25
+ def __init__(self, project_manager, application_configuration, event_registry):
24
26
  self._project_manager = project_manager
25
27
  self._application_configuration = application_configuration
26
-
28
+ self._event_registry = event_registry
29
+
30
+ def _get_dialog(self, parent, filter_, default_directory=None):
31
+ dialog = QFileDialog(
32
+ parent,
33
+ directory=default_directory or self._get_default_directory(),
34
+ filter=filter_
35
+ )
36
+ def _notify_save_dialog_finished(result):
37
+ self._event_registry.event(EventTypes.save_dialog_finished).emit(result)
38
+ dialog.finished.connect(_notify_save_dialog_finished)
39
+ return dialog
40
+
27
41
  def get_save_filename(self, parent, filter_, default_name=None):
28
- dialog = QFileDialog(parent, directory=self._get_default_directory(), filter=filter_)
42
+ default_directory = path.dirname(default_name) if default_name else None
43
+ dialog = self._get_dialog(parent, filter_, default_directory)
44
+
29
45
  if default_name:
30
46
  dialog.selectFile(default_name)
31
47
  dialog.setModal(True)
@@ -35,7 +51,7 @@ class FileDialogService:
35
51
  return dialog.selectedFiles()[0]
36
52
 
37
53
  def get_open_filename(self, parent, filter_, file_mode=None):
38
- dialog = QFileDialog(parent, directory=self._get_default_directory(), filter=filter_)
54
+ dialog = self._get_dialog(parent, filter_)
39
55
  if file_mode:
40
56
  dialog.setFileMode(file_mode)
41
57
  if dialog.exec_():
@@ -49,5 +65,7 @@ class FileDialogService:
49
65
  if default_location and path.isdir(default_location):
50
66
  return default_location
51
67
  return QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation)
68
+ elif self._project_manager.filename and not path.isdir(path.dirname(self._project_manager.filename)):
69
+ return QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation)
52
70
  else:
53
71
  return path.dirname(self._project_manager.filename)
@@ -14,32 +14,21 @@
14
14
  # You should have received a copy of the GNU General Public License
15
15
  # along with this program; if not, write to the Free Software Foundation,
16
16
  # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
- from enum import Enum, IntEnum
18
17
 
19
18
  #pylint: disable=no-name-in-module
20
- from PyQt5.QtCore import QObject, pyqtSignal
19
+ from PyQt5.QtCore import QObject, pyqtSignal, Qt
21
20
  #pylint: enable=no-name-in-module
22
21
 
23
22
  from ..properties import AutoProperty
24
-
25
- class MessageType(Enum):
26
- validation_error = 0
27
- confirmation = 1
28
- application_exception = 2
29
-
30
- class MessageResponse(IntEnum):
31
- ok = 1
32
- cancel = 2
33
- save = 4
34
- discard = 8
35
- ignore = 16
23
+ from ..data import MessageType, MessageResponse
36
24
 
37
25
  class MessageArgs:
38
- def __init__(self, message_type, message, checkbox_message, response_type):
26
+ def __init__(self, message_type, message, checkbox_message, response_type, text_format):
39
27
  self._message_type = message_type
40
28
  self._message = message
41
29
  self._checkbox_message = checkbox_message
42
30
  self._response_type = response_type
31
+ self._text_format = text_format
43
32
  self._response = None
44
33
  self._checkbox_value = None
45
34
 
@@ -58,7 +47,11 @@ class MessageArgs:
58
47
  @property
59
48
  def response_type(self):
60
49
  return self._response_type
61
-
50
+
51
+ @property
52
+ def text_format(self):
53
+ return self._text_format
54
+
62
55
  @property
63
56
  def response(self):
64
57
  return self._response
@@ -78,9 +71,18 @@ class MessageArgs:
78
71
  class MessageBoard(QObject):
79
72
  def __init__(self, parent):
80
73
  QObject.__init__(self, parent)
81
-
82
- def post(self, message_type, message, response_type=MessageResponse.ok, checkbox_message=None):
83
- self.message = MessageArgs(message_type, message, checkbox_message, response_type)
74
+ self._message_enabled = True
75
+
76
+ def messageEnabled(self):
77
+ return self._message_enabled
78
+
79
+ def setMessageEnabled(self, value):
80
+ self._message_enabled = value
81
+
82
+ def post(self, message_type, message, response_type=MessageResponse.ok, checkbox_message=None, text_format=Qt.AutoText):
83
+ if not self._message_enabled:
84
+ return
85
+ self.message = MessageArgs(message_type, message, checkbox_message, response_type, text_format)
84
86
  return self.message
85
87
 
86
88
  messageChanged = pyqtSignal(MessageArgs)
@@ -26,6 +26,7 @@ from PyQt5.QtWidgets import QApplication
26
26
  from concurrent.futures import ThreadPoolExecutor
27
27
 
28
28
  from ..properties import AutoProperty
29
+ from ..exceptions import TaskCancelled
29
30
 
30
31
  LOGGER = logging.getLogger(__name__)
31
32
 
@@ -71,8 +72,6 @@ class BusyArgs(QObject):
71
72
  if self._cancellable:
72
73
  self.cancelled.emit()
73
74
 
74
- class TaskCancelled(Exception):
75
- pass
76
75
 
77
76
  class Worker:
78
77
  def __init__(self, task_runner, f, args, busy_args, on_completed, on_cancelled, on_error, error_description):
@@ -178,7 +177,7 @@ class TaskRunner(QObject):
178
177
  self._is_cancelled = False
179
178
  self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix='TaskRunner_Thread')
180
179
 
181
- def run_task(self, task_function, task_args=None, on_completed=None, on_cancelled=None, on_error=None, description=None, error_description=None, show_progress=True, cancellable=False, **kwargs):
180
+ def run_task(self, task_function, task_args=None, on_completed=None, on_cancelled=None, on_error=None, description=None, error_description=None, show_progress=True, cancellable=False, force_indeterminate_start=False, **kwargs):
182
181
  """function::runTask(self, task_function, task_args, on_completed, on_error)
183
182
  :param task_function: The function to execute
184
183
  :param task_args: The arguments to pass to the function
@@ -194,7 +193,7 @@ class TaskRunner(QObject):
194
193
 
195
194
  if 'update_progress' in parameters:
196
195
  kwargs['update_progress'] = self.update_progress
197
- indeterminate = False
196
+ indeterminate = force_indeterminate_start
198
197
  else:
199
198
  indeterminate = True
200
199
 
@@ -25,8 +25,9 @@ class ToolWindowService:
25
25
  self._dependency_container = dependency_contianer
26
26
  self._active_tools = []
27
27
 
28
- def open(self, parent, model, name):
29
- view = self._dependency_container.resolve_for(type(model), QWidget, {'parent': None})
28
+ def open(self, parent, model, name, *args, extras=None):
29
+ extras = {'parent': None, **(extras or {})}
30
+ view = self._dependency_container.resolve_for(type(model), QWidget, extras, *args)
30
31
  if hasattr(model, 'showButtons'):
31
32
  show_buttons = model.showButtons
32
33
  else:
@@ -69,8 +69,9 @@ class BulkValueSelectorWidget(QWidget):
69
69
  @auto_property(list)
70
70
  def selectedValues(self):
71
71
  if isinstance(self._value_selector.model(), QStringListModel):
72
+ string_list = self._value_selector.model().stringList()
72
73
  return [
73
- self._value_selector.model().stringList()[i.row()]\
74
+ string_list[i.row()]\
74
75
  for i in self._value_selector.selectedIndexes() if i.row() < self._value_selector.model().rowCount()
75
76
  ]
76
77
  return [self._value_selector.model().data(i, DataRole) for i in self._value_selector.selectedIndexes() if i.row() < self._value_selector.model().rowCount()]
@@ -82,7 +83,7 @@ class BulkValueSelectorWidget(QWidget):
82
83
  else self._select_values if self._value_selector.model()
83
84
  else None
84
85
  )
85
- if select_values:
86
+ if select_values and values != self.selectedValues:
86
87
  signals_blocked = self.blockSignals(True)
87
88
  self._value_selector.clearSelection()
88
89
  select_values(values)
@@ -115,7 +115,7 @@ class CodeTextEdit(QPlainTextEdit):
115
115
  if block.isVisible() and bottom >= event.rect().top():
116
116
  number = str(block_number + 1)
117
117
  painter.setPen(Qt.black)
118
- painter.drawText(0, top, self._line_number_area.width(), self.fontMetrics().height(), Qt.AlignRight, number)
118
+ painter.drawText(0, int(top), self._line_number_area.width(), self.fontMetrics().height(), Qt.AlignRight, number)
119
119
  block = block.next()
120
120
  top = bottom
121
121
  bottom = top + self.blockBoundingRect(block).height()
@@ -194,7 +194,7 @@ class CodeEdit(QWidget):
194
194
  self._text_edit.setPlainText(model.text)
195
195
  model.textChanged.connect(self._set_text(self._text_edit))
196
196
  if hasattr(model, 'output'):
197
- self._output_pane.setPlainText(model.output)
197
+ self._output_pane.setPlainText(model.output.getvalue())
198
198
  model.outputChanged.connect(self._set_text(self._output_pane, True))
199
199
  self._output_pane.setVisible(True)
200
200
  else:
@@ -207,6 +207,7 @@ class CodeEdit(QWidget):
207
207
  def _(text):
208
208
  widget_inst = widget_ref()
209
209
  if widget_inst:
210
+ text = text if isinstance(text, str) else text.getvalue()
210
211
  if widget_inst.toPlainText() != text:
211
212
  widget_inst.setPlainText(text)
212
213
  if scroll_to_end:
@@ -40,7 +40,7 @@ class DateTimeEdit(QDateTimeEdit):
40
40
  self.isLimit = False
41
41
  self._default = default
42
42
  self.isLimitChanged.connect(self._handle_is_limit_changed)
43
- self.dateTimeChanged.connect(self._handle_date_time_changed)
43
+ self.editingFinished.connect(self._handle_date_time_editing_finished)
44
44
  self.setCalendarPopup(calendar_popup)
45
45
  if calendar_popup:
46
46
  self.calendarWidget().clicked.connect(self._handle_date_changed_from_calendar)
@@ -50,7 +50,7 @@ class DateTimeEdit(QDateTimeEdit):
50
50
 
51
51
  isLimit = AutoProperty(bool)
52
52
 
53
- def focusOutEvent(self, event):
53
+ def focusOutEvent(self, event=None):
54
54
  if self._start_date and self.dateTime() < self._start_date:
55
55
  self.setDateTime(self._start_date)
56
56
  elif self._min_date and self.dateTime() < self._min_date:
@@ -59,7 +59,7 @@ class DateTimeEdit(QDateTimeEdit):
59
59
  self.setDateTime(self._end_date)
60
60
  elif self._max_date and self.dateTime() > self._max_date:
61
61
  self.setDateTime(self._max_date)
62
- return QDateTimeEdit.focusOutEvent(self, event)
62
+ return event and QDateTimeEdit.focusOutEvent(self, event)
63
63
 
64
64
  def setDateTimeRange(self, start_date, end_date):
65
65
  self._start_date = start_date
@@ -95,13 +95,15 @@ class DateTimeEdit(QDateTimeEdit):
95
95
  self.value = QDateTime()
96
96
 
97
97
  def _handle_is_limit_changed(self, is_limit):
98
- stylesheet = 'DateTimeEdit {font: italic}' if is_limit else ''
98
+ stylesheet = 'DateTimeEdit {font: italic}' if is_limit else 'DateTimeEdit {font: normal}'
99
99
  self.setStyleSheet(stylesheet)
100
100
 
101
- def _handle_date_time_changed(self, value):
101
+ def _handle_date_time_editing_finished(self):
102
+ value = self.dateTime()
102
103
  if value is not None or (isinstance(value, QDateTime) and value.isValid()):
103
104
  self.isLimit = False
104
105
  self.valueChanged.emit(self.value)
106
+ self.focusOutEvent()
105
107
 
106
108
  def _handle_date_changed_from_calendar(self, value):
107
109
  self.value = QDateTime(value, QTime(0, 0))