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.
- {karcytics_sdk-2.3.0/src/karcytics_sdk.egg-info → karcytics_sdk-2.4.0}/PKG-INFO +1 -1
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/pyproject.toml +1 -1
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/academy_driver.py +66 -5
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/question_panel.py +12 -0
- karcytics_sdk-2.4.0/src/karcytics_sdk/plugin/tutorial_highlight.py +84 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/tutorial_overlay.py +13 -1
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0/src/karcytics_sdk.egg-info}/PKG-INFO +1 -1
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk.egg-info/SOURCES.txt +1 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/LICENSE +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/README.md +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/setup.cfg +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/__init__.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/__init__.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/commands/__init__.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/commands/diagnostics.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/commands/migrate.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/commands/scaffold.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/commands/security.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/cli/main.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/contrib/__init__.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/contrib/image_utils.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/__init__.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/ai.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/core_services.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/docs.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/marketplace_cache.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/module_status_widget.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/qt_bridge.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/sign_plugin.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/trust_manager.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/trust_overrides.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/trust_path.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/host/trust_storage.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/interfaces/__init__.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/interfaces/i_crash_reporter.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/interfaces/i_event_bus.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/interfaces/i_logger.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/interfaces/i_task_scheduler.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/__init__.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/academy.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/academy_window.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/analysis.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/autosave.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/base.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/components.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/context.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/course_complete_overlay.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/cyto_character.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/cyto_costumes.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/daemon.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/dialogs.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/draggable.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/effects.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/events.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/galactic_loader.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/galactic_loader.qml +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/history.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/interfaces.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/io.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/logging.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/managed_task.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/manifest.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/manifest_parser.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/menu_builder.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/preferences.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/rendering/__init__.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/rendering/graphics_scene.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/rendering/lock.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/rendering/mpl_canvas.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/rendering/pipeline.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/ribbon.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/runtime_services.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/security_parser.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/signals.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/state.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/theme_fallback.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/toast.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/tutorial_models.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/ui_daemon_runtime.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/ui_preferences.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/update_check.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/validation.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/wizard.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/worker_thread.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/workflow.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/py.typed +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/sdk_cli.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/testing/__init__.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/testing/contract.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk.egg-info/dependency_links.txt +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk.egg-info/entry_points.txt +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk.egg-info/requires.txt +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk.egg-info/top_level.txt +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_host_ai.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_host_security.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_image_utils.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_marketplace_cache.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_namespaces.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_plugin_architecture.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_plugin_ribbon.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_plugin_state.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_plugin_ui.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_sdk_cli.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_sign_plugin.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_testing_contract.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_theme_fallback.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_ui_daemon_runtime.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_workflow_context.py +0 -0
- {karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/tests/test_workflow_dialogs.py +0 -0
|
@@ -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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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:
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/interfaces/i_crash_reporter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/interfaces/i_task_scheduler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/course_complete_overlay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/rendering/graphics_scene.py
RENAMED
|
File without changes
|
|
File without changes
|
{karcytics_sdk-2.3.0 → karcytics_sdk-2.4.0}/src/karcytics_sdk/plugin/rendering/mpl_canvas.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|