scitex 2.14.0__py3-none-any.whl → 2.15.1__py3-none-any.whl

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 (218) hide show
  1. scitex/__init__.py +47 -0
  2. scitex/_env_loader.py +156 -0
  3. scitex/_mcp_resources/__init__.py +37 -0
  4. scitex/_mcp_resources/_cheatsheet.py +135 -0
  5. scitex/_mcp_resources/_figrecipe.py +138 -0
  6. scitex/_mcp_resources/_formats.py +102 -0
  7. scitex/_mcp_resources/_modules.py +337 -0
  8. scitex/_mcp_resources/_session.py +149 -0
  9. scitex/_mcp_tools/__init__.py +4 -0
  10. scitex/_mcp_tools/audio.py +66 -0
  11. scitex/_mcp_tools/diagram.py +11 -95
  12. scitex/_mcp_tools/introspect.py +191 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +244 -0
  16. scitex/_mcp_tools/writer.py +21 -204
  17. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  18. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  19. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  20. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  21. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  22. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  23. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  24. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  25. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  26. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  27. scitex/audio/README.md +40 -36
  28. scitex/audio/__init__.py +127 -59
  29. scitex/audio/_branding.py +185 -0
  30. scitex/audio/_mcp/__init__.py +32 -0
  31. scitex/audio/_mcp/handlers.py +59 -6
  32. scitex/audio/_mcp/speak_handlers.py +238 -0
  33. scitex/audio/_relay.py +225 -0
  34. scitex/audio/engines/elevenlabs_engine.py +6 -1
  35. scitex/audio/mcp_server.py +228 -75
  36. scitex/canvas/README.md +1 -1
  37. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  38. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  39. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  40. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  41. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  42. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  43. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  44. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  45. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  46. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  47. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  48. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  49. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  50. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  51. scitex/cli/__init__.py +38 -43
  52. scitex/cli/audio.py +76 -27
  53. scitex/cli/capture.py +13 -20
  54. scitex/cli/introspect.py +443 -0
  55. scitex/cli/main.py +198 -109
  56. scitex/cli/mcp.py +60 -34
  57. scitex/cli/scholar/__init__.py +8 -0
  58. scitex/cli/scholar/_crossref_scitex.py +296 -0
  59. scitex/cli/scholar/_fetch.py +25 -3
  60. scitex/cli/social.py +314 -0
  61. scitex/cli/writer.py +117 -0
  62. scitex/config/README.md +1 -1
  63. scitex/config/__init__.py +16 -2
  64. scitex/config/_env_registry.py +191 -0
  65. scitex/diagram/__init__.py +42 -19
  66. scitex/diagram/mcp_server.py +13 -125
  67. scitex/introspect/__init__.py +75 -0
  68. scitex/introspect/_call_graph.py +303 -0
  69. scitex/introspect/_class_hierarchy.py +163 -0
  70. scitex/introspect/_core.py +42 -0
  71. scitex/introspect/_docstring.py +131 -0
  72. scitex/introspect/_examples.py +113 -0
  73. scitex/introspect/_imports.py +271 -0
  74. scitex/introspect/_mcp/__init__.py +37 -0
  75. scitex/introspect/_mcp/handlers.py +208 -0
  76. scitex/introspect/_members.py +151 -0
  77. scitex/introspect/_resolve.py +89 -0
  78. scitex/introspect/_signature.py +131 -0
  79. scitex/introspect/_source.py +80 -0
  80. scitex/introspect/_type_hints.py +172 -0
  81. scitex/io/bundle/README.md +1 -1
  82. scitex/mcp_server.py +98 -5
  83. scitex/plt/__init__.py +248 -550
  84. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  85. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  86. scitex/plt/gallery/README.md +1 -1
  87. scitex/plt/utils/_hitmap/__init__.py +82 -0
  88. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  89. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  90. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  91. scitex/plt/utils/_hitmap/_constants.py +40 -0
  92. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  93. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  94. scitex/plt/utils/_hitmap/_query.py +113 -0
  95. scitex/plt/utils/_hitmap.py +46 -1616
  96. scitex/plt/utils/_metadata/__init__.py +80 -0
  97. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  98. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  99. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  100. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  101. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  102. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  103. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  104. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  105. scitex/plt/utils/_metadata/_csv.py +416 -0
  106. scitex/plt/utils/_metadata/_detect.py +225 -0
  107. scitex/plt/utils/_metadata/_legend.py +127 -0
  108. scitex/plt/utils/_metadata/_rounding.py +117 -0
  109. scitex/plt/utils/_metadata/_verification.py +202 -0
  110. scitex/schema/README.md +1 -1
  111. scitex/scholar/__init__.py +8 -0
  112. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  113. scitex/scholar/core/Scholar.py +63 -1700
  114. scitex/scholar/core/_mixins/__init__.py +36 -0
  115. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  116. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  117. scitex/scholar/core/_mixins/_loaders.py +103 -0
  118. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  119. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  120. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  121. scitex/scholar/core/_mixins/_savers.py +69 -0
  122. scitex/scholar/core/_mixins/_search.py +103 -0
  123. scitex/scholar/core/_mixins/_services.py +88 -0
  124. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  125. scitex/scholar/crossref_scitex.py +367 -0
  126. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  127. scitex/scholar/examples/00_run_all.sh +120 -0
  128. scitex/scholar/jobs/_executors.py +27 -3
  129. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  130. scitex/scholar/pdf_download/_cli.py +154 -0
  131. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  132. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  133. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  134. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  135. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  136. scitex/scholar/pipelines/_single_steps.py +71 -36
  137. scitex/scholar/storage/_LibraryManager.py +97 -1695
  138. scitex/scholar/storage/_mixins/__init__.py +30 -0
  139. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  140. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  141. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  142. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  143. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  144. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  145. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  146. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  147. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  148. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  149. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  150. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  151. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  152. scitex/security/README.md +3 -3
  153. scitex/session/README.md +1 -1
  154. scitex/sh/README.md +1 -1
  155. scitex/social/__init__.py +153 -0
  156. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  157. scitex/template/README.md +1 -1
  158. scitex/template/clone_writer_directory.py +5 -5
  159. scitex/writer/README.md +1 -1
  160. scitex/writer/_mcp/handlers.py +11 -744
  161. scitex/writer/_mcp/tool_schemas.py +5 -335
  162. scitex-2.15.1.dist-info/METADATA +648 -0
  163. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
  164. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  165. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  166. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  167. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  168. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  169. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  170. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  171. scitex/diagram/_compile.py +0 -312
  172. scitex/diagram/_diagram.py +0 -355
  173. scitex/diagram/_mcp/__init__.py +0 -4
  174. scitex/diagram/_mcp/handlers.py +0 -400
  175. scitex/diagram/_mcp/tool_schemas.py +0 -157
  176. scitex/diagram/_presets.py +0 -173
  177. scitex/diagram/_schema.py +0 -182
  178. scitex/diagram/_split.py +0 -278
  179. scitex/plt/_mcp/__init__.py +0 -4
  180. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  181. scitex/plt/_mcp/_handlers_figure.py +0 -195
  182. scitex/plt/_mcp/_handlers_plot.py +0 -252
  183. scitex/plt/_mcp/_handlers_style.py +0 -219
  184. scitex/plt/_mcp/handlers.py +0 -74
  185. scitex/plt/_mcp/tool_schemas.py +0 -497
  186. scitex/plt/mcp_server.py +0 -231
  187. scitex/scholar/data/.gitkeep +0 -0
  188. scitex/scholar/data/README.md +0 -44
  189. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  190. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  191. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  192. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  193. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  194. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  195. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  196. scitex/scholar/data/bib_files/pac.bib +0 -698
  197. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  198. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  199. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  200. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  201. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  202. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  203. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  204. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  205. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  206. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  207. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  208. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  209. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  210. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  211. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  212. scitex/scholar/data/impact_factor.db +0 -0
  213. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  214. scitex/scholar/examples/dev.py +0 -38
  215. scitex-2.14.0.dist-info/METADATA +0 -1238
  216. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
  217. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
  218. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_editor.py
4
+
5
+ """
6
+ DearPyGui-based figure editor with GPU-accelerated rendering.
7
+
8
+ Thin orchestrator class that delegates to modular components.
9
+ """
10
+
11
+ from pathlib import Path
12
+ from typing import Any, Dict, Optional
13
+
14
+ from ._state import EditorState
15
+
16
+
17
+ class DearPyGuiEditor:
18
+ """
19
+ GPU-accelerated figure editor using DearPyGui.
20
+
21
+ Features:
22
+ - Modern immediate-mode GUI with GPU acceleration
23
+ - Real-time figure preview
24
+ - Property editors with sliders, color pickers, and input fields
25
+ - Click-to-select traces on preview
26
+ - Save to .manual.json
27
+ - SciTeX style defaults pre-filled
28
+ - Dark/light theme support
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ json_path: Path,
34
+ metadata: Dict[str, Any],
35
+ csv_data: Optional[Any] = None,
36
+ png_path: Optional[Path] = None,
37
+ manual_overrides: Optional[Dict[str, Any]] = None,
38
+ ):
39
+ """Initialize the DearPyGui editor.
40
+
41
+ Parameters
42
+ ----------
43
+ json_path : Path
44
+ Path to the JSON metadata file
45
+ metadata : dict
46
+ Figure metadata dictionary
47
+ csv_data : pd.DataFrame, optional
48
+ CSV data for plotting
49
+ png_path : Path, optional
50
+ Path to the PNG file
51
+ manual_overrides : dict, optional
52
+ Manual override settings
53
+ """
54
+ self.state = EditorState.create(
55
+ json_path=json_path,
56
+ metadata=metadata,
57
+ csv_data=csv_data,
58
+ png_path=png_path,
59
+ manual_overrides=manual_overrides,
60
+ )
61
+
62
+ # Backward compatibility properties
63
+ self._texture_id = None
64
+
65
+ @property
66
+ def json_path(self) -> Path:
67
+ """Get JSON path."""
68
+ return self.state.json_path
69
+
70
+ @property
71
+ def metadata(self) -> Dict[str, Any]:
72
+ """Get metadata."""
73
+ return self.state.metadata
74
+
75
+ @property
76
+ def csv_data(self) -> Optional[Any]:
77
+ """Get CSV data."""
78
+ return self.state.csv_data
79
+
80
+ @property
81
+ def current_overrides(self) -> Dict[str, Any]:
82
+ """Get current overrides."""
83
+ return self.state.current_overrides
84
+
85
+ @current_overrides.setter
86
+ def current_overrides(self, value: Dict[str, Any]) -> None:
87
+ """Set current overrides."""
88
+ self.state.current_overrides = value
89
+
90
+ def run(self):
91
+ """Launch the DearPyGui editor."""
92
+ try:
93
+ import dearpygui.dearpygui as dpg
94
+ except ImportError:
95
+ raise ImportError(
96
+ "DearPyGui is required for this editor. "
97
+ "Install with: pip install dearpygui"
98
+ )
99
+
100
+ from ._panels import create_control_panel, create_preview_panel
101
+ from ._rendering import update_preview
102
+
103
+ dpg.create_context()
104
+
105
+ # Configure viewport
106
+ dpg.create_viewport(
107
+ title=f"SciTeX Editor ({self.state.backend_name}) - {self.state.json_path.name}",
108
+ width=1400,
109
+ height=900,
110
+ )
111
+
112
+ # Create texture registry for image preview
113
+ with dpg.texture_registry(show=False):
114
+ # Create initial texture with placeholder
115
+ width, height = 800, 600
116
+ texture_data = [0.2, 0.2, 0.2, 1.0] * (width * height)
117
+ self._texture_id = dpg.add_dynamic_texture(
118
+ width=width,
119
+ height=height,
120
+ default_value=texture_data,
121
+ tag="preview_texture",
122
+ )
123
+ self.state.texture_id = self._texture_id
124
+
125
+ # Create main window
126
+ with dpg.window(label="SciTeX Figure Editor", tag="main_window"):
127
+ with dpg.group(horizontal=True):
128
+ # Left panel: Preview
129
+ create_preview_panel(self.state, dpg)
130
+
131
+ # Right panel: Controls
132
+ create_control_panel(self.state, dpg)
133
+
134
+ # Set main window as primary
135
+ dpg.set_primary_window("main_window", True)
136
+
137
+ # Initial render
138
+ update_preview(self.state, dpg)
139
+
140
+ # Setup and show
141
+ dpg.setup_dearpygui()
142
+ dpg.show_viewport()
143
+ dpg.start_dearpygui()
144
+ dpg.destroy_context()
145
+
146
+
147
+ # EOF
@@ -0,0 +1,476 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_handlers.py
4
+
5
+ """
6
+ Event handlers for DearPyGui editor.
7
+
8
+ Handles all callbacks and user interactions.
9
+ """
10
+
11
+ import copy
12
+ import time
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from ._state import EditorState
17
+
18
+
19
+ def on_value_change(state: "EditorState", dpg) -> None:
20
+ """Handle value changes from widgets."""
21
+ from ._rendering import update_preview
22
+
23
+ state.user_modified = True
24
+ collect_overrides(state, dpg)
25
+ update_preview(state, dpg)
26
+
27
+
28
+ def collect_overrides(state: "EditorState", dpg) -> None:
29
+ """Collect current values from all widgets."""
30
+ o = state.current_overrides
31
+
32
+ # Labels
33
+ o["title"] = dpg.get_value("title_input")
34
+ o["xlabel"] = dpg.get_value("xlabel_input")
35
+ o["ylabel"] = dpg.get_value("ylabel_input")
36
+
37
+ # Line style
38
+ o["linewidth"] = dpg.get_value("linewidth_slider")
39
+
40
+ # Font settings
41
+ o["title_fontsize"] = dpg.get_value("title_fontsize_slider")
42
+ o["axis_fontsize"] = dpg.get_value("axis_fontsize_slider")
43
+ o["tick_fontsize"] = dpg.get_value("tick_fontsize_slider")
44
+ o["legend_fontsize"] = dpg.get_value("legend_fontsize_slider")
45
+
46
+ # Tick settings
47
+ o["n_ticks"] = dpg.get_value("n_ticks_slider")
48
+ o["tick_length"] = dpg.get_value("tick_length_slider")
49
+ o["tick_width"] = dpg.get_value("tick_width_slider")
50
+ o["tick_direction"] = dpg.get_value("tick_direction_combo")
51
+
52
+ # Style
53
+ o["grid"] = dpg.get_value("grid_checkbox")
54
+ o["hide_top_spine"] = dpg.get_value("hide_top_spine_checkbox")
55
+ o["hide_right_spine"] = dpg.get_value("hide_right_spine_checkbox")
56
+ o["transparent"] = dpg.get_value("transparent_checkbox")
57
+ o["axis_width"] = dpg.get_value("axis_width_slider")
58
+
59
+ # Legend
60
+ o["legend_visible"] = dpg.get_value("legend_visible_checkbox")
61
+ o["legend_frameon"] = dpg.get_value("legend_frameon_checkbox")
62
+ o["legend_loc"] = dpg.get_value("legend_loc_combo")
63
+
64
+ # Dimensions
65
+ o["fig_size"] = [
66
+ dpg.get_value("fig_width_input"),
67
+ dpg.get_value("fig_height_input"),
68
+ ]
69
+ o["dpi"] = dpg.get_value("dpi_slider")
70
+
71
+
72
+ def on_apply_limits(state: "EditorState", dpg) -> None:
73
+ """Apply axis limits."""
74
+ from ._rendering import update_preview
75
+
76
+ xmin = dpg.get_value("xmin_input")
77
+ xmax = dpg.get_value("xmax_input")
78
+ ymin = dpg.get_value("ymin_input")
79
+ ymax = dpg.get_value("ymax_input")
80
+
81
+ if xmin < xmax:
82
+ state.current_overrides["xlim"] = [xmin, xmax]
83
+ if ymin < ymax:
84
+ state.current_overrides["ylim"] = [ymin, ymax]
85
+
86
+ state.user_modified = True
87
+ update_preview(state, dpg)
88
+
89
+
90
+ def on_add_annotation(state: "EditorState", dpg) -> None:
91
+ """Add text annotation."""
92
+ from ._rendering import update_preview
93
+
94
+ text = dpg.get_value("annot_text_input")
95
+ if not text:
96
+ return
97
+
98
+ x = dpg.get_value("annot_x_input")
99
+ y = dpg.get_value("annot_y_input")
100
+
101
+ if "annotations" not in state.current_overrides:
102
+ state.current_overrides["annotations"] = []
103
+
104
+ state.current_overrides["annotations"].append(
105
+ {
106
+ "type": "text",
107
+ "text": text,
108
+ "x": x,
109
+ "y": y,
110
+ "fontsize": state.current_overrides.get("axis_fontsize", 7),
111
+ }
112
+ )
113
+
114
+ dpg.set_value("annot_text_input", "")
115
+ update_annotations_list(state, dpg)
116
+ state.user_modified = True
117
+ update_preview(state, dpg)
118
+
119
+
120
+ def on_remove_annotation(state: "EditorState", dpg) -> None:
121
+ """Remove selected annotation."""
122
+ from ._rendering import update_preview
123
+
124
+ selected = dpg.get_value("annotations_listbox")
125
+ annotations = state.current_overrides.get("annotations", [])
126
+
127
+ if selected and annotations:
128
+ # Find index by text
129
+ for i, ann in enumerate(annotations):
130
+ label = (
131
+ f"{ann.get('text', '')[:20]} "
132
+ f"({ann.get('x', 0):.2f}, {ann.get('y', 0):.2f})"
133
+ )
134
+ if label == selected:
135
+ del annotations[i]
136
+ break
137
+
138
+ update_annotations_list(state, dpg)
139
+ state.user_modified = True
140
+ update_preview(state, dpg)
141
+
142
+
143
+ def update_annotations_list(state: "EditorState", dpg) -> None:
144
+ """Update the annotations listbox."""
145
+ annotations = state.current_overrides.get("annotations", [])
146
+ items = []
147
+ for ann in annotations:
148
+ if ann.get("type") == "text":
149
+ label = (
150
+ f"{ann.get('text', '')[:20]} "
151
+ f"({ann.get('x', 0):.2f}, {ann.get('y', 0):.2f})"
152
+ )
153
+ items.append(label)
154
+
155
+ dpg.configure_item("annotations_listbox", items=items)
156
+
157
+
158
+ def on_preview_click(state: "EditorState", dpg, app_data) -> None:
159
+ """Handle click on preview image to select element."""
160
+ from ._selection import find_clicked_element, find_nearest_trace, select_element
161
+
162
+ # Only handle left clicks
163
+ if app_data != 0: # 0 = left button
164
+ return
165
+
166
+ # Get mouse position relative to viewport
167
+ mouse_pos = dpg.get_mouse_pos(local=False)
168
+
169
+ # Get preview image position and size
170
+ if not dpg.does_item_exist("preview_image"):
171
+ return
172
+
173
+ # Get the image item's position in the window
174
+ img_pos = dpg.get_item_pos("preview_image")
175
+ panel_pos = dpg.get_item_pos("preview_panel")
176
+
177
+ # Calculate click position relative to image
178
+ click_x = mouse_pos[0] - panel_pos[0] - img_pos[0]
179
+ click_y = mouse_pos[1] - panel_pos[1] - img_pos[1]
180
+
181
+ # Check if click is within image bounds (800x600)
182
+ max_width, max_height = 800, 600
183
+ if not (0 <= click_x <= max_width and 0 <= click_y <= max_height):
184
+ return
185
+
186
+ # First check if click is on a fixed element (title, labels, axes, legend)
187
+ element = find_clicked_element(state, click_x, click_y)
188
+
189
+ if element:
190
+ select_element(state, element, dpg)
191
+ else:
192
+ # Fall back to trace selection
193
+ trace_idx = find_nearest_trace(state, click_x, click_y, max_width, max_height)
194
+ if trace_idx is not None:
195
+ select_element(state, {"type": "trace", "index": trace_idx}, dpg)
196
+
197
+
198
+ def on_preview_hover(state: "EditorState", dpg, app_data) -> None:
199
+ """Handle mouse move for hover effects on preview (optimized with caching)."""
200
+ from ._rendering import update_hover_overlay
201
+ from ._selection import find_clicked_element, find_nearest_trace
202
+
203
+ # Throttle hover updates - reduced to 16ms (~60fps) since we use fast overlay
204
+ current_time = time.time()
205
+ if current_time - state.last_hover_check < 0.016:
206
+ return
207
+ state.last_hover_check = current_time
208
+
209
+ # Get mouse position relative to viewport
210
+ mouse_pos = dpg.get_mouse_pos(local=False)
211
+
212
+ # Get preview image position
213
+ if not dpg.does_item_exist("preview_image"):
214
+ return
215
+
216
+ img_pos = dpg.get_item_pos("preview_image")
217
+ panel_pos = dpg.get_item_pos("preview_panel")
218
+
219
+ # Calculate hover position relative to image
220
+ hover_x = mouse_pos[0] - panel_pos[0] - img_pos[0]
221
+ hover_y = mouse_pos[1] - panel_pos[1] - img_pos[1]
222
+
223
+ # Check if within image bounds
224
+ max_width, max_height = 800, 600
225
+ if not (0 <= hover_x <= max_width and 0 <= hover_y <= max_height):
226
+ if state.hovered_element is not None:
227
+ state.hovered_element = None
228
+ dpg.set_value("hover_text", "")
229
+ # Use fast overlay update instead of full redraw
230
+ update_hover_overlay(state, dpg)
231
+ return
232
+
233
+ # Find element under cursor
234
+ element = find_clicked_element(state, hover_x, hover_y)
235
+
236
+ if element is None:
237
+ # Check for trace hover
238
+ trace_idx = find_nearest_trace(state, hover_x, hover_y, max_width, max_height)
239
+ if trace_idx is not None:
240
+ element = {"type": "trace", "index": trace_idx}
241
+
242
+ # Check if hover changed
243
+ old_hover = state.hovered_element
244
+ if element != old_hover:
245
+ state.hovered_element = element
246
+ if element:
247
+ elem_type = element.get("type", "")
248
+ elem_idx = element.get("index")
249
+ if elem_type == "trace" and elem_idx is not None:
250
+ traces = state.current_overrides.get("traces", [])
251
+ if elem_idx < len(traces):
252
+ label = traces[elem_idx].get("label", f"Trace {elem_idx}")
253
+ dpg.set_value("hover_text", f"Hover: {label} (click to select)")
254
+ else:
255
+ label = elem_type.replace("x", "X ").replace("y", "Y ").title()
256
+ dpg.set_value("hover_text", f"Hover: {label} (click to select)")
257
+ else:
258
+ dpg.set_value("hover_text", "")
259
+
260
+ # Use fast overlay update for hover (no matplotlib re-render)
261
+ update_hover_overlay(state, dpg)
262
+
263
+
264
+ def on_element_selected(state: "EditorState", dpg, app_data) -> None:
265
+ """Handle element selection from combo box."""
266
+ from ._selection import select_element
267
+
268
+ if app_data == "Title":
269
+ select_element(state, {"type": "title", "index": None}, dpg)
270
+ elif app_data == "X Label":
271
+ select_element(state, {"type": "xlabel", "index": None}, dpg)
272
+ elif app_data == "Y Label":
273
+ select_element(state, {"type": "ylabel", "index": None}, dpg)
274
+ elif app_data == "X Axis":
275
+ select_element(state, {"type": "xaxis", "index": None}, dpg)
276
+ elif app_data == "Y Axis":
277
+ select_element(state, {"type": "yaxis", "index": None}, dpg)
278
+ elif app_data == "Legend":
279
+ select_element(state, {"type": "legend", "index": None}, dpg)
280
+ elif app_data.startswith("Trace: "):
281
+ # Find trace index
282
+ trace_label = app_data[7:] # Remove "Trace: " prefix
283
+ traces = state.current_overrides.get("traces", [])
284
+ for i, t in enumerate(traces):
285
+ if t.get("label", t.get("id", f"Trace {i}")) == trace_label:
286
+ select_element(state, {"type": "trace", "index": i}, dpg)
287
+ break
288
+
289
+
290
+ def on_text_element_change(state: "EditorState", dpg) -> None:
291
+ """Handle changes to text element properties."""
292
+ from ._rendering import update_preview
293
+
294
+ if state.selected_element is None:
295
+ return
296
+
297
+ elem_type = state.selected_element.get("type")
298
+ if elem_type not in ("title", "xlabel", "ylabel"):
299
+ return
300
+
301
+ text = dpg.get_value("element_text_input")
302
+ fontsize = dpg.get_value("element_fontsize_slider")
303
+
304
+ if elem_type == "title":
305
+ state.current_overrides["title"] = text
306
+ state.current_overrides["title_fontsize"] = fontsize
307
+ elif elem_type == "xlabel":
308
+ state.current_overrides["xlabel"] = text
309
+ state.current_overrides["axis_fontsize"] = fontsize
310
+ elif elem_type == "ylabel":
311
+ state.current_overrides["ylabel"] = text
312
+ state.current_overrides["axis_fontsize"] = fontsize
313
+
314
+ state.user_modified = True
315
+ update_preview(state, dpg)
316
+
317
+
318
+ def on_axis_element_change(state: "EditorState", dpg) -> None:
319
+ """Handle changes to axis element properties."""
320
+ from ._rendering import update_preview
321
+
322
+ if state.selected_element is None:
323
+ return
324
+
325
+ elem_type = state.selected_element.get("type")
326
+ if elem_type not in ("xaxis", "yaxis"):
327
+ return
328
+
329
+ state.current_overrides["axis_width"] = dpg.get_value("axis_linewidth_slider")
330
+ state.current_overrides["tick_length"] = dpg.get_value("axis_tick_length_slider")
331
+ state.current_overrides["tick_fontsize"] = dpg.get_value(
332
+ "axis_tick_fontsize_slider"
333
+ )
334
+
335
+ show_spine = dpg.get_value("axis_show_spine_checkbox")
336
+ if elem_type == "xaxis":
337
+ state.current_overrides["hide_bottom_spine"] = not show_spine
338
+ else:
339
+ state.current_overrides["hide_left_spine"] = not show_spine
340
+
341
+ state.user_modified = True
342
+ update_preview(state, dpg)
343
+
344
+
345
+ def on_legend_element_change(state: "EditorState", dpg) -> None:
346
+ """Handle changes to legend element properties."""
347
+ from ._rendering import update_preview
348
+
349
+ if state.selected_element is None:
350
+ return
351
+
352
+ elem_type = state.selected_element.get("type")
353
+ if elem_type != "legend":
354
+ return
355
+
356
+ state.current_overrides["legend_visible"] = dpg.get_value("legend_visible_edit")
357
+ state.current_overrides["legend_frameon"] = dpg.get_value("legend_frameon_edit")
358
+ state.current_overrides["legend_loc"] = dpg.get_value("legend_loc_edit")
359
+ state.current_overrides["legend_fontsize"] = dpg.get_value("legend_fontsize_edit")
360
+
361
+ state.user_modified = True
362
+ update_preview(state, dpg)
363
+
364
+
365
+ def on_trace_property_change(state: "EditorState", dpg) -> None:
366
+ """Handle changes to selected trace properties."""
367
+ from ._rendering import update_preview
368
+
369
+ if state.selected_trace_index is None:
370
+ return
371
+
372
+ traces = state.current_overrides.get("traces", [])
373
+ if state.selected_trace_index >= len(traces):
374
+ return
375
+
376
+ trace = traces[state.selected_trace_index]
377
+
378
+ # Update trace properties from widgets
379
+ trace["label"] = dpg.get_value("trace_label_input")
380
+
381
+ # Convert RGB to hex
382
+ color_rgb = dpg.get_value("trace_color_picker")
383
+ if color_rgb and len(color_rgb) >= 3:
384
+ r, g, b = int(color_rgb[0]), int(color_rgb[1]), int(color_rgb[2])
385
+ trace["color"] = f"#{r:02x}{g:02x}{b:02x}"
386
+
387
+ trace["linewidth"] = dpg.get_value("trace_linewidth_slider")
388
+ trace["linestyle"] = dpg.get_value("trace_linestyle_combo")
389
+
390
+ marker = dpg.get_value("trace_marker_combo")
391
+ trace["marker"] = marker if marker else None
392
+
393
+ trace["markersize"] = dpg.get_value("trace_markersize_slider")
394
+
395
+ state.user_modified = True
396
+ update_preview(state, dpg)
397
+
398
+
399
+ def on_save_manual(state: "EditorState", dpg) -> None:
400
+ """Save current overrides to .manual.json."""
401
+ from ..edit import save_manual_overrides
402
+
403
+ try:
404
+ collect_overrides(state, dpg)
405
+ manual_path = save_manual_overrides(state.json_path, state.current_overrides)
406
+ dpg.set_value("status_text", f"Saved: {manual_path.name}")
407
+ except Exception as e:
408
+ dpg.set_value("status_text", f"Error: {str(e)}")
409
+
410
+
411
+ def on_reset_overrides(state: "EditorState", dpg) -> None:
412
+ """Reset to initial overrides."""
413
+ from ._rendering import update_preview
414
+
415
+ state.current_overrides = copy.deepcopy(state.initial_overrides)
416
+ state.user_modified = False
417
+
418
+ # Update all widgets
419
+ dpg.set_value("title_input", state.current_overrides.get("title", ""))
420
+ dpg.set_value("xlabel_input", state.current_overrides.get("xlabel", ""))
421
+ dpg.set_value("ylabel_input", state.current_overrides.get("ylabel", ""))
422
+ dpg.set_value("linewidth_slider", state.current_overrides.get("linewidth", 1.0))
423
+ dpg.set_value("grid_checkbox", state.current_overrides.get("grid", False))
424
+ dpg.set_value(
425
+ "transparent_checkbox", state.current_overrides.get("transparent", True)
426
+ )
427
+
428
+ update_preview(state, dpg)
429
+ dpg.set_value("status_text", "Reset to original")
430
+
431
+
432
+ def on_export_png(state: "EditorState", dpg) -> None:
433
+ """Export current view to PNG."""
434
+ import matplotlib
435
+
436
+ matplotlib.use("Agg")
437
+ import matplotlib.pyplot as plt
438
+
439
+ from ._plotting import plot_from_csv
440
+
441
+ try:
442
+ collect_overrides(state, dpg)
443
+ output_path = state.json_path.with_suffix(".edited.png")
444
+
445
+ # Full resolution render
446
+ o = state.current_overrides
447
+ fig_size = o.get("fig_size", [3.15, 2.68])
448
+ dpi = o.get("dpi", 300)
449
+
450
+ fig, ax = plt.subplots(figsize=fig_size, dpi=dpi)
451
+
452
+ if state.csv_data is not None:
453
+ plot_from_csv(ax, o, state.csv_data)
454
+
455
+ if o.get("title"):
456
+ ax.set_title(o["title"], fontsize=o.get("title_fontsize", 8))
457
+ if o.get("xlabel"):
458
+ ax.set_xlabel(o["xlabel"], fontsize=o.get("axis_fontsize", 7))
459
+ if o.get("ylabel"):
460
+ ax.set_ylabel(o["ylabel"], fontsize=o.get("axis_fontsize", 7))
461
+
462
+ fig.tight_layout()
463
+ fig.savefig(
464
+ output_path,
465
+ dpi=dpi,
466
+ bbox_inches="tight",
467
+ transparent=o.get("transparent", True),
468
+ )
469
+ plt.close(fig)
470
+
471
+ dpg.set_value("status_text", f"Exported: {output_path.name}")
472
+ except Exception as e:
473
+ dpg.set_value("status_text", f"Error: {str(e)}")
474
+
475
+
476
+ # EOF
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_panels/__init__.py
4
+
5
+ """
6
+ UI panel creation for DearPyGui editor.
7
+
8
+ Subpackage for organizing control panel sections.
9
+ """
10
+
11
+ from ._control import create_control_panel
12
+ from ._preview import create_preview_panel
13
+
14
+ __all__ = ["create_preview_panel", "create_control_panel"]
15
+
16
+
17
+ # EOF