karcytics-sdk 2.3.0__tar.gz → 2.4.0__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 (109) hide show
  1. {karcytics_sdk-2.3.0/src/karcytics_sdk.egg-info → karcytics_sdk-2.4.0}/PKG-INFO +1 -1
  2. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/pyproject.toml +1 -1
  3. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/academy_driver.py +66 -5
  4. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/question_panel.py +12 -0
  5. karcytics_sdk-2.4.0/src/karcytics_sdk/plugin/tutorial_highlight.py +84 -0
  6. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/tutorial_overlay.py +13 -1
  7. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0/src/karcytics_sdk.egg-info}/PKG-INFO +1 -1
  8. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk.egg-info/SOURCES.txt +1 -0
  9. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/LICENSE +0 -0
  10. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/README.md +0 -0
  11. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/setup.cfg +0 -0
  12. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/__init__.py +0 -0
  13. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/__init__.py +0 -0
  14. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/commands/__init__.py +0 -0
  15. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/commands/diagnostics.py +0 -0
  16. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/commands/migrate.py +0 -0
  17. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/commands/scaffold.py +0 -0
  18. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/commands/security.py +0 -0
  19. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/main.py +0 -0
  20. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/contrib/__init__.py +0 -0
  21. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/contrib/image_utils.py +0 -0
  22. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/__init__.py +0 -0
  23. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/ai.py +0 -0
  24. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/core_services.py +0 -0
  25. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/docs.py +0 -0
  26. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/marketplace_cache.py +0 -0
  27. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/module_status_widget.py +0 -0
  28. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/qt_bridge.py +0 -0
  29. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/sign_plugin.py +0 -0
  30. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/trust_manager.py +0 -0
  31. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/trust_overrides.py +0 -0
  32. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/trust_path.py +0 -0
  33. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/trust_storage.py +0 -0
  34. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/interfaces/__init__.py +0 -0
  35. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/interfaces/i_crash_reporter.py +0 -0
  36. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/interfaces/i_event_bus.py +0 -0
  37. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/interfaces/i_logger.py +0 -0
  38. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/interfaces/i_task_scheduler.py +0 -0
  39. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/__init__.py +0 -0
  40. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/academy.py +0 -0
  41. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/academy_window.py +0 -0
  42. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/analysis.py +0 -0
  43. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/autosave.py +0 -0
  44. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/base.py +0 -0
  45. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/components.py +0 -0
  46. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/context.py +0 -0
  47. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/course_complete_overlay.py +0 -0
  48. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/cyto_character.py +0 -0
  49. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/cyto_costumes.py +0 -0
  50. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/daemon.py +0 -0
  51. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/dialogs.py +0 -0
  52. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/draggable.py +0 -0
  53. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/effects.py +0 -0
  54. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/events.py +0 -0
  55. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/galactic_loader.py +0 -0
  56. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/galactic_loader.qml +0 -0
  57. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/history.py +0 -0
  58. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/interfaces.py +0 -0
  59. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/io.py +0 -0
  60. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/logging.py +0 -0
  61. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/managed_task.py +0 -0
  62. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/manifest.py +0 -0
  63. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/manifest_parser.py +0 -0
  64. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/menu_builder.py +0 -0
  65. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/preferences.py +0 -0
  66. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/rendering/__init__.py +0 -0
  67. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/rendering/graphics_scene.py +0 -0
  68. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/rendering/lock.py +0 -0
  69. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/rendering/mpl_canvas.py +0 -0
  70. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/rendering/pipeline.py +0 -0
  71. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/ribbon.py +0 -0
  72. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/runtime_services.py +0 -0
  73. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/security_parser.py +0 -0
  74. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/signals.py +0 -0
  75. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/state.py +0 -0
  76. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/theme_fallback.py +0 -0
  77. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/toast.py +0 -0
  78. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/tutorial_models.py +0 -0
  79. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/ui_daemon_runtime.py +0 -0
  80. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/ui_preferences.py +0 -0
  81. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/update_check.py +0 -0
  82. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/validation.py +0 -0
  83. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/wizard.py +0 -0
  84. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/worker_thread.py +0 -0
  85. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/workflow.py +0 -0
  86. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/py.typed +0 -0
  87. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/sdk_cli.py +0 -0
  88. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/testing/__init__.py +0 -0
  89. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/testing/contract.py +0 -0
  90. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk.egg-info/dependency_links.txt +0 -0
  91. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk.egg-info/entry_points.txt +0 -0
  92. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk.egg-info/requires.txt +0 -0
  93. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk.egg-info/top_level.txt +0 -0
  94. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_host_ai.py +0 -0
  95. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_host_security.py +0 -0
  96. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_image_utils.py +0 -0
  97. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_marketplace_cache.py +0 -0
  98. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_namespaces.py +0 -0
  99. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_plugin_architecture.py +0 -0
  100. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_plugin_ribbon.py +0 -0
  101. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_plugin_state.py +0 -0
  102. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_plugin_ui.py +0 -0
  103. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_sdk_cli.py +0 -0
  104. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_sign_plugin.py +0 -0
  105. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_testing_contract.py +0 -0
  106. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_theme_fallback.py +0 -0
  107. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_ui_daemon_runtime.py +0 -0
  108. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_workflow_context.py +0 -0
  109. {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_workflow_dialogs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karcytics-sdk
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Software Development Kit and CLI for Karcytics.
5
5
  Author: Kalaimaran Balasothy
6
6
  License: # PolyForm Noncommercial License 1.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "karcytics-sdk"
7
- version = "2.3.0"
7
+ version = "2.4.0"
8
8
  description = "Software Development Kit and CLI for Karcytics."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -24,6 +24,7 @@ from PyQt6.QtCore import QObject, QRect, QTimer
24
24
  from PyQt6.QtWidgets import QApplication, QScrollArea, QWidget
25
25
 
26
26
  from .academy import AcademyManager
27
+ from .tutorial_highlight import TutorialHighlight
27
28
  from .tutorial_models import (
28
29
  ActionStep,
29
30
  BranchingStep,
@@ -86,6 +87,10 @@ class AcademyStepDriver(QObject):
86
87
  self._failure_hint_shown = False
87
88
  self._was_reviewing = False
88
89
  self._last_canvas_guide_active = False
90
+ # In-window spotlights for targets in other windows (see _spotlight_rects).
91
+ self._window_highlights: dict[QWidget, TutorialHighlight] = {}
92
+ # Step whose targets were last scrolled into view (see _update_targets).
93
+ self._revealed_step_id: str | None = None
89
94
 
90
95
  self._timer = QTimer(self)
91
96
  self._timer.timeout.connect(self._tick)
@@ -468,11 +473,16 @@ class AcademyStepDriver(QObject):
468
473
  if w and w.isVisible():
469
474
  targets.append(w)
470
475
 
471
- rects = []
472
- for w in targets:
473
- global_pos = w.mapToGlobal(w.rect().topLeft())
474
- local_pos = self._overlay.mapFromGlobal(global_pos)
475
- rects.append(QRect(local_pos, w.size()))
476
+ # A step's first tick with targets: scroll them into view. Otherwise one
477
+ # scrolled out of a sidebar is spotlit off-screen, Cyto and the bubble
478
+ # follow it there, and the learner sees only the dim. Once per step,
479
+ # so they can still scroll away afterwards. (Not before the targets
480
+ # exist: a popup the step points into may open a few ticks later.)
481
+ if targets and getattr(step, "id", None) != self._revealed_step_id:
482
+ self._revealed_step_id = getattr(step, "id", None)
483
+ _scroll_into_view(targets)
484
+
485
+ rects = self._spotlight_rects(targets)
476
486
  rects.extend(self._collect_canvas_target_rects(step))
477
487
  self._overlay.set_targets(rects)
478
488
 
@@ -488,6 +498,47 @@ class AcademyStepDriver(QObject):
488
498
  scroll_target = scroll_area.viewport()
489
499
  self._overlay.set_scroll_target(scroll_target)
490
500
 
501
+ def _spotlight_rects(self, targets: list[QWidget]) -> list[QRect]:
502
+ """Overlay rects for `targets`; targets in other windows are framed there.
503
+
504
+ A target in the overlay's own window gets a spotlight hole as usual.
505
+ One inside another window — a dialog or popup, even one parented to
506
+ the panel (so `findChildren()` finds it) — would otherwise be
507
+ spotlit on the main window *underneath* that window. Those are framed
508
+ inside their own window by a `TutorialHighlight` instead, and that
509
+ window's frame becomes the overlay rect, so Cyto and the bubble steer
510
+ clear of the whole window rather than sitting behind it.
511
+ """
512
+ from PyQt6 import sip
513
+
514
+ # Forget windows that have since been destroyed (a closed dialog).
515
+ self._window_highlights = {w: h for w, h in self._window_highlights.items() if not sip.isdeleted(w)}
516
+ own_window = self._overlay.window()
517
+ rects: list[QRect] = []
518
+ elsewhere: dict[QWidget, list[QWidget]] = {}
519
+ for w in targets:
520
+ window = w.window()
521
+ if window is None or window is own_window:
522
+ local_pos = self._overlay.mapFromGlobal(w.mapToGlobal(w.rect().topLeft()))
523
+ rects.append(QRect(local_pos, w.size()))
524
+ else:
525
+ inner = elsewhere.setdefault(window, [])
526
+ if w is not window and w not in inner:
527
+ inner.append(w)
528
+
529
+ for window, widgets in elsewhere.items():
530
+ highlight = self._window_highlights.get(window)
531
+ if highlight is None:
532
+ highlight = TutorialHighlight(window)
533
+ self._window_highlights[window] = highlight
534
+ highlight.show_on(widgets)
535
+ frame = window.frameGeometry()
536
+ rects.append(QRect(self._overlay.mapFromGlobal(frame.topLeft()), frame.size()))
537
+ for window, highlight in self._window_highlights.items():
538
+ if window not in elsewhere:
539
+ highlight.clear()
540
+ return rects
541
+
491
542
  def _find_in_other_windows(self, name: str) -> list[QWidget]:
492
543
  """Visible widgets named `name` in this process's *other* top-level
493
544
  windows — e.g. the SDK Preferences dialog, a separate window parented
@@ -549,6 +600,16 @@ class AcademyStepDriver(QObject):
549
600
  return None
550
601
 
551
602
 
603
+ def _scroll_into_view(widgets: list[QWidget]) -> None:
604
+ """Scrolls each widget's nearest `QScrollArea` so it shows — last
605
+ first, so when they don't all fit, the first-named one wins.
606
+ """
607
+ for w in reversed(widgets):
608
+ area = AcademyStepDriver._find_scroll_area_ancestor(w)
609
+ if area is not None:
610
+ area.ensureWidgetVisible(w, 0, 0)
611
+
612
+
552
613
  # ── Generic Academy launch — shared by any isolated plugin's own UI trigger ──
553
614
  #
554
615
  # Originally private to `ui_daemon_runtime.py`'s Help-menu wiring. Promoted
@@ -47,6 +47,16 @@ _REVEAL_NOTE = "The outlined answer is correct — select it to continue."
47
47
  _MULTI_MISSING = "Not quite — at least one correct answer is still unselected."
48
48
 
49
49
 
50
+ def _claim_wrapped_height(label: QLabel, width: int) -> None:
51
+ """Makes a word-wrapped label ask for all its lines at `width`.
52
+
53
+ A wrapped QLabel's sizeHint() assumes its own preferred width, not the
54
+ fixed one it's given here, and the bubble is sized from sizeHint() — so
55
+ the last line or two (a long explanation) would be clipped.
56
+ """
57
+ label.setMinimumHeight(label.heightForWidth(width))
58
+
59
+
50
60
  class _ChoiceRow(QFrame):
51
61
  """One answer: an indicator plus a word-wrapped label; the whole row clicks."""
52
62
 
@@ -65,6 +75,7 @@ class _ChoiceRow(QFrame):
65
75
  row.addWidget(self.label, 1)
66
76
  self.setFixedWidth(width)
67
77
  theme_manager.apply_style(self, _ROW_QSS)
78
+ _claim_wrapped_height(self.label, width - 20 - 8 - indicator.sizeHint().width())
68
79
 
69
80
  def mousePressEvent(self, event) -> None: # noqa: N802
70
81
  if self.indicator.isEnabled():
@@ -205,6 +216,7 @@ class QuestionPanel(QWidget):
205
216
 
206
217
  def _show_feedback(self, text: str, state: str) -> None:
207
218
  self.feedback.setText(text)
219
+ _claim_wrapped_height(self.feedback, self.feedback.width())
208
220
  self.feedback.setProperty("state", state)
209
221
  style = self.feedback.style()
210
222
  if style is not None:
@@ -0,0 +1,84 @@
1
+ """Academy spotlight drawn inside a window other than the overlay's own.
2
+
3
+ The `TutorialOverlay` is a child of the plugin's main window and can't paint
4
+ over a separate window. A step's target inside a dialog or popup (a Tool
5
+ window, a non-modal QDialog…) would otherwise be spotlit on the main window
6
+ *underneath* it — wherever the target happens to sit on screen. The
7
+ `AcademyStepDriver` instead gives each such window one of these, which draws
8
+ accent frames over the targets inside that window itself.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from PyQt6.QtCore import QPoint, QRect, Qt, QTimer
14
+ from PyQt6.QtWidgets import QFrame, QWidget
15
+
16
+ from .theme_fallback import theme_manager
17
+
18
+ # The driver refreshes every tick (100 ms); once it stops (step changed,
19
+ # window closed, Academy hidden) the frames disappear on their own.
20
+ _EXPIRE_MS = 400
21
+ _PAD = 3
22
+
23
+
24
+ def _visible_rect(widget: QWidget) -> QRect:
25
+ """`widget`'s own rect clipped by every ancestor (e.g. a scroll viewport)."""
26
+ rect = widget.rect()
27
+ child, parent = widget, widget.parentWidget()
28
+ while parent is not None and not child.isWindow():
29
+ offset = widget.mapTo(parent, QPoint(0, 0))
30
+ rect = rect.intersected(parent.rect().translated(-offset))
31
+ child, parent = parent, parent.parentWidget()
32
+ return rect
33
+
34
+
35
+ class TutorialHighlight:
36
+ """Accent frames over widgets inside `host` (a top-level window)."""
37
+
38
+ def __init__(self, host: QWidget) -> None:
39
+ self.host = host
40
+ self._frames: list[QFrame] = []
41
+ self._expiry = QTimer(host)
42
+ self._expiry.setSingleShot(True)
43
+ self._expiry.timeout.connect(self.clear)
44
+
45
+ @property
46
+ def visible_count(self) -> int:
47
+ return sum(1 for f in self._frames if f.isVisible())
48
+
49
+ def show_on(self, widgets: list[QWidget]) -> None:
50
+ """Frames exactly `widgets` (descendants of `host`); hides the rest.
51
+
52
+ Only the part of each widget that's actually on screen is framed —
53
+ one scrolled out of its scroll area gets no frame, rather than one
54
+ drawn over whatever sits where it would be (a dialog's buttons).
55
+ """
56
+ shown = [(w, r) for w in widgets if not (r := _visible_rect(w)).isEmpty()]
57
+ while len(self._frames) < len(shown):
58
+ self._frames.append(self._new_frame())
59
+ for frame, (widget, rect) in zip(self._frames, shown, strict=False):
60
+ top_left = widget.mapTo(self.host, rect.topLeft())
61
+ frame.setGeometry(QRect(top_left, rect.size()).adjusted(-_PAD, -_PAD, _PAD, _PAD))
62
+ frame.show()
63
+ frame.raise_()
64
+ for frame in self._frames[len(shown) :]:
65
+ frame.hide()
66
+ self._expiry.start(_EXPIRE_MS)
67
+
68
+ def clear(self) -> None:
69
+ for frame in self._frames:
70
+ frame.hide()
71
+
72
+ def _new_frame(self) -> QFrame:
73
+ frame = QFrame(self.host)
74
+ frame.setObjectName("TutorialHighlightFrame")
75
+ frame.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
76
+ theme_manager.apply_style(
77
+ frame,
78
+ "QFrame#TutorialHighlightFrame {"
79
+ " border: 2px solid {ACCENT_PRIMARY};"
80
+ " border-radius: 6px;"
81
+ " background: transparent;"
82
+ "}",
83
+ )
84
+ return frame
@@ -1271,7 +1271,16 @@ class TutorialOverlay(QWidget):
1271
1271
  panel.content_changed.connect(self._on_question_content_changed)
1272
1272
 
1273
1273
  def _on_question_content_changed(self) -> None:
1274
- """Feedback appeared or grew: re-fit the bubble and re-punch the mask."""
1274
+ """Feedback appeared or grew: re-fit the bubble and re-punch the mask.
1275
+
1276
+ Once now and once on the next event-loop turn: the panel's own layout
1277
+ only settles after its pending LayoutRequest, and fitting to the stale
1278
+ size hint squeezes the choices and clips the explanation's last lines.
1279
+ """
1280
+ self._refit_for_question()
1281
+ QTimer.singleShot(0, self._refit_for_question)
1282
+
1283
+ def _refit_for_question(self) -> None:
1275
1284
  if not self._is_alive():
1276
1285
  return
1277
1286
  self._force_resize()
@@ -1442,6 +1451,9 @@ class TutorialOverlay(QWidget):
1442
1451
  while self.dynamic_content.count():
1443
1452
  item = self.dynamic_content.takeAt(0)
1444
1453
  if item.widget():
1454
+ # Hidden now; deleteLater() waits for the event loop to come
1455
+ # back round, and until then it would still paint in the bubble.
1456
+ item.widget().hide()
1445
1457
  item.widget().deleteLater()
1446
1458
 
1447
1459
  def _force_resize(self) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karcytics-sdk
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Software Development Kit and CLI for Karcytics.
5
5
  Author: Kalaimaran Balasothy
6
6
  License: # PolyForm Noncommercial License 1.0.0
@@ -72,6 +72,7 @@ src/karcytics_sdk/plugin/signals.py
72
72
  src/karcytics_sdk/plugin/state.py
73
73
  src/karcytics_sdk/plugin/theme_fallback.py
74
74
  src/karcytics_sdk/plugin/toast.py
75
+ src/karcytics_sdk/plugin/tutorial_highlight.py
75
76
  src/karcytics_sdk/plugin/tutorial_models.py
76
77
  src/karcytics_sdk/plugin/tutorial_overlay.py
77
78
  src/karcytics_sdk/plugin/ui_daemon_runtime.py
File without changes
File without changes
File without changes