qcanvas 0.0.5.7a0__py3-none-any.whl → 1.0.3.post0__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.

Potentially problematic release.


This version of qcanvas might be problematic. Click here for more details.

Files changed (114) hide show
  1. qcanvas/app_start/__init__.py +47 -0
  2. qcanvas/backend_connectors/__init__.py +2 -0
  3. qcanvas/backend_connectors/frontend_resource_manager.py +63 -0
  4. qcanvas/backend_connectors/qcanvas_task_master.py +28 -0
  5. qcanvas/icons/__init__.py +6 -0
  6. qcanvas/icons/file-download-failed.svg +6 -0
  7. qcanvas/icons/file-downloaded.svg +6 -0
  8. qcanvas/icons/file-not-downloaded.svg +6 -0
  9. qcanvas/icons/file-unknown.svg +6 -0
  10. qcanvas/icons/icons.qrc +4 -0
  11. qcanvas/icons/main_icon.svg +7 -7
  12. qcanvas/icons/rc_icons.py +580 -214
  13. qcanvas/icons/sync.svg +6 -6
  14. qcanvas/run.py +29 -0
  15. qcanvas/ui/course_viewer/__init__.py +2 -0
  16. qcanvas/ui/course_viewer/content_tree.py +123 -0
  17. qcanvas/ui/course_viewer/course_tree.py +93 -0
  18. qcanvas/ui/course_viewer/course_viewer.py +62 -0
  19. qcanvas/ui/course_viewer/tabs/__init__.py +3 -0
  20. qcanvas/ui/course_viewer/tabs/assignment_tab/__init__.py +1 -0
  21. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +168 -0
  22. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +104 -0
  23. qcanvas/ui/course_viewer/tabs/content_tab.py +96 -0
  24. qcanvas/ui/course_viewer/tabs/mail_tab/__init__.py +1 -0
  25. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +68 -0
  26. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +70 -0
  27. qcanvas/ui/course_viewer/tabs/page_tab/__init__.py +1 -0
  28. qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +36 -0
  29. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +74 -0
  30. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +176 -0
  31. qcanvas/ui/course_viewer/tabs/util.py +1 -0
  32. qcanvas/ui/main_ui/course_viewer_container.py +52 -0
  33. qcanvas/ui/main_ui/options/__init__.py +3 -0
  34. qcanvas/ui/main_ui/options/quick_sync_option.py +25 -0
  35. qcanvas/ui/main_ui/options/sync_on_start_option.py +25 -0
  36. qcanvas/ui/main_ui/qcanvas_window.py +192 -0
  37. qcanvas/ui/main_ui/status_bar_progress_display.py +153 -0
  38. qcanvas/ui/memory_tree/__init__.py +2 -0
  39. qcanvas/ui/memory_tree/_tree_memory.py +66 -0
  40. qcanvas/ui/memory_tree/memory_tree_widget.py +133 -0
  41. qcanvas/ui/memory_tree/memory_tree_widget_item.py +19 -0
  42. qcanvas/ui/setup/__init__.py +2 -0
  43. qcanvas/ui/setup/setup_checker.py +17 -0
  44. qcanvas/ui/setup/setup_dialog.py +212 -0
  45. qcanvas/util/__init__.py +2 -0
  46. qcanvas/util/basic_fonts.py +12 -0
  47. qcanvas/util/fe_resource_manager.py +23 -0
  48. qcanvas/util/html_cleaner.py +25 -0
  49. qcanvas/util/layouts.py +52 -0
  50. qcanvas/util/logs.py +6 -0
  51. qcanvas/util/paths.py +41 -0
  52. qcanvas/util/settings/__init__.py +9 -0
  53. qcanvas/util/settings/_client_settings.py +29 -0
  54. qcanvas/util/settings/_mapped_setting.py +63 -0
  55. qcanvas/util/settings/_ui_settings.py +34 -0
  56. qcanvas/util/ui_tools.py +41 -0
  57. qcanvas/util/url_checker.py +13 -0
  58. qcanvas-1.0.3.post0.dist-info/METADATA +61 -0
  59. qcanvas-1.0.3.post0.dist-info/RECORD +64 -0
  60. {qcanvas-0.0.5.7a0.dist-info → qcanvas-1.0.3.post0.dist-info}/WHEEL +1 -1
  61. qcanvas-1.0.3.post0.dist-info/entry_points.txt +3 -0
  62. qcanvas/__main__.py +0 -155
  63. qcanvas/db/__init__.py +0 -5
  64. qcanvas/db/database.py +0 -338
  65. qcanvas/db/db_converter_helper.py +0 -81
  66. qcanvas/net/canvas/__init__.py +0 -2
  67. qcanvas/net/canvas/canvas_client.py +0 -209
  68. qcanvas/net/canvas/legacy_canvas_types.py +0 -124
  69. qcanvas/net/custom_httpx_async_transport.py +0 -34
  70. qcanvas/net/self_authenticating.py +0 -108
  71. qcanvas/queries/__init__.py +0 -4
  72. qcanvas/queries/all_courses.gql +0 -7
  73. qcanvas/queries/all_courses.py +0 -108
  74. qcanvas/queries/canvas_course_data.gql +0 -51
  75. qcanvas/queries/canvas_course_data.py +0 -143
  76. qcanvas/ui/container_item.py +0 -11
  77. qcanvas/ui/main_ui.py +0 -251
  78. qcanvas/ui/menu_bar/__init__.py +0 -0
  79. qcanvas/ui/menu_bar/grouping_preferences_menu.py +0 -61
  80. qcanvas/ui/menu_bar/theme_selection_menu.py +0 -39
  81. qcanvas/ui/setup_dialog.py +0 -190
  82. qcanvas/ui/status_bar_reporter.py +0 -40
  83. qcanvas/ui/viewer/__init__.py +0 -0
  84. qcanvas/ui/viewer/course_list.py +0 -96
  85. qcanvas/ui/viewer/file_list.py +0 -195
  86. qcanvas/ui/viewer/file_view_tab.py +0 -62
  87. qcanvas/ui/viewer/page_list_viewer.py +0 -150
  88. qcanvas/util/app_settings.py +0 -98
  89. qcanvas/util/constants.py +0 -5
  90. qcanvas/util/course_indexer/__init__.py +0 -1
  91. qcanvas/util/course_indexer/conversion_helpers.py +0 -78
  92. qcanvas/util/course_indexer/data_manager.py +0 -447
  93. qcanvas/util/course_indexer/resource_helpers.py +0 -191
  94. qcanvas/util/download_pool.py +0 -58
  95. qcanvas/util/helpers/__init__.py +0 -0
  96. qcanvas/util/helpers/canvas_sanitiser.py +0 -47
  97. qcanvas/util/helpers/file_icon_helper.py +0 -34
  98. qcanvas/util/helpers/qaction_helper.py +0 -25
  99. qcanvas/util/helpers/theme_helper.py +0 -48
  100. qcanvas/util/link_scanner/__init__.py +0 -2
  101. qcanvas/util/link_scanner/canvas_link_scanner.py +0 -41
  102. qcanvas/util/link_scanner/canvas_media_object_scanner.py +0 -60
  103. qcanvas/util/link_scanner/dropbox_scanner.py +0 -68
  104. qcanvas/util/link_scanner/resource_scanner.py +0 -69
  105. qcanvas/util/progress_reporter.py +0 -101
  106. qcanvas/util/self_updater.py +0 -55
  107. qcanvas/util/task_pool.py +0 -253
  108. qcanvas/util/tree_util/__init__.py +0 -3
  109. qcanvas/util/tree_util/expanding_tree.py +0 -165
  110. qcanvas/util/tree_util/model_helpers.py +0 -36
  111. qcanvas/util/tree_util/tree_model.py +0 -85
  112. qcanvas-0.0.5.7a0.dist-info/METADATA +0 -21
  113. qcanvas-0.0.5.7a0.dist-info/RECORD +0 -62
  114. /qcanvas/{net → ui/main_ui}/__init__.py +0 -0
@@ -0,0 +1,192 @@
1
+ import logging
2
+ from threading import Semaphore
3
+ from typing import *
4
+
5
+ import qcanvas_backend.database.types as db
6
+ from qasync import asyncSlot
7
+ from qcanvas_backend.database.data_monolith import DataMonolith
8
+ from qcanvas_backend.qcanvas import QCanvas
9
+ from qtpy.QtCore import QUrl, Signal, Slot
10
+ from qtpy.QtGui import QDesktopServices, QKeySequence, QPixmap
11
+ from qtpy.QtWidgets import *
12
+
13
+ from qcanvas import icons
14
+ from qcanvas.backend_connectors import FrontendResourceManager
15
+ from qcanvas.ui.course_viewer import CourseTree
16
+ from qcanvas.ui.main_ui.course_viewer_container import CourseViewerContainer
17
+ from qcanvas.ui.main_ui.options.quick_sync_option import QuickSyncOption
18
+ from qcanvas.ui.main_ui.options.sync_on_start_option import SyncOnStartOption
19
+ from qcanvas.ui.main_ui.status_bar_progress_display import StatusBarProgressDisplay
20
+ from qcanvas.util import paths, settings
21
+ from qcanvas.util.ui_tools import create_qaction
22
+
23
+ _logger = logging.getLogger(__name__)
24
+
25
+
26
+ class QCanvasWindow(QMainWindow):
27
+ _loaded = Signal()
28
+
29
+ def __init__(self):
30
+ super().__init__()
31
+
32
+ self.setWindowTitle("QCanvas")
33
+ self.setWindowIcon(QPixmap(icons.main_icon))
34
+
35
+ self._operation_semaphore = Semaphore()
36
+ self._data: Optional[DataMonolith] = None
37
+ self._qcanvas = QCanvas[FrontendResourceManager](
38
+ canvas_config=settings.client.canvas_config,
39
+ panopto_config=settings.client.panopto_config,
40
+ storage_path=paths.data_storage(),
41
+ resource_manager_class=FrontendResourceManager,
42
+ )
43
+
44
+ self._course_tree = CourseTree()
45
+ self._course_tree.item_selected.connect(self._on_course_selected)
46
+ self._course_tree.course_renamed.connect(self._on_course_renamed)
47
+ self._sync_button = QPushButton("Synchronise")
48
+ self._sync_button.clicked.connect(self._synchronise_requested)
49
+ self._course_viewer_container = CourseViewerContainer(
50
+ self._qcanvas.resource_manager
51
+ )
52
+
53
+ self.setCentralWidget(self._setup_main_layout())
54
+ self.setStatusBar(StatusBarProgressDisplay())
55
+ self._setup_menu_bar()
56
+ self._restore_window_position()
57
+
58
+ self._loaded.connect(self._on_app_loaded)
59
+ self._loaded.emit()
60
+
61
+ def _setup_menu_bar(self) -> None:
62
+ menu_bar = self.menuBar()
63
+ app_menu = menu_bar.addMenu("Actions")
64
+
65
+ create_qaction(
66
+ name="Synchronise",
67
+ shortcut=QKeySequence("Ctrl+S"),
68
+ triggered=self._synchronise_requested,
69
+ parent=app_menu,
70
+ )
71
+
72
+ create_qaction(
73
+ name="Open downloads folder",
74
+ shortcut=QKeySequence("Ctrl+D"),
75
+ triggered=self._open_downloads_folder,
76
+ parent=app_menu,
77
+ )
78
+
79
+ create_qaction(
80
+ name="Quick canvas login",
81
+ shortcut=QKeySequence("Ctrl+O"),
82
+ triggered=self._open_quick_auth_in_browser,
83
+ parent=app_menu,
84
+ )
85
+
86
+ create_qaction(
87
+ name="Quit",
88
+ shortcut=QKeySequence("Ctrl+Q"),
89
+ triggered=lambda: self.close(),
90
+ parent=app_menu,
91
+ )
92
+
93
+ options_menu = menu_bar.addMenu("Options")
94
+
95
+ options_menu.addAction(QuickSyncOption(options_menu))
96
+ options_menu.addAction(SyncOnStartOption(options_menu))
97
+
98
+ def _restore_window_position(self):
99
+ self.restoreGeometry(settings.ui.last_geometry)
100
+ self.restoreState(settings.ui.last_window_state)
101
+
102
+ def closeEvent(self, event):
103
+ settings.ui.last_geometry = self.saveGeometry()
104
+ settings.ui.last_window_state = self.saveState()
105
+
106
+ def _setup_main_layout(self) -> QWidget:
107
+ h_box = QHBoxLayout()
108
+
109
+ h_box.addLayout(self._setup_course_column(), 1)
110
+ h_box.addWidget(self._course_viewer_container, 5)
111
+
112
+ widget = QWidget()
113
+ widget.setLayout(h_box)
114
+ return widget
115
+
116
+ def _setup_course_column(self) -> QVBoxLayout:
117
+ course_list_column = QVBoxLayout()
118
+ course_list_column.addWidget(self._course_tree)
119
+ course_list_column.addWidget(self._sync_button)
120
+
121
+ return course_list_column
122
+
123
+ @asyncSlot()
124
+ async def _on_app_loaded(self) -> None:
125
+ await self._qcanvas.init()
126
+ self._course_tree.reload(await self._get_terms(), sync_receipt=None)
127
+
128
+ if settings.client.sync_on_start:
129
+ await self._synchronise()
130
+
131
+ @asyncSlot()
132
+ async def _synchronise_requested(self) -> None:
133
+ await self._synchronise()
134
+
135
+ async def _synchronise(self) -> None:
136
+ if not self._operation_semaphore.acquire(False):
137
+ _logger.debug("Sync operation already in progress")
138
+ return
139
+
140
+ try:
141
+ # todo handle exceptions and PROGRESS!! better
142
+ self._sync_button.setText("Sync in progress...")
143
+ receipt = await self._qcanvas.synchronise_canvas(
144
+ quick_sync=settings.client.quick_sync_enabled
145
+ )
146
+
147
+ self._course_tree.reload(await self._get_terms(), sync_receipt=receipt)
148
+ await self._course_viewer_container.reload_all(
149
+ await self._get_courses(), sync_receipt=receipt
150
+ )
151
+ self._sync_button.setText("Synchronise")
152
+
153
+ finally:
154
+ self._operation_semaphore.release()
155
+
156
+ async def _get_terms(self) -> Sequence[db.Term]:
157
+ return (await self._qcanvas.get_data()).terms
158
+
159
+ async def _get_courses(self) -> Sequence[db.Course]:
160
+ return (await self._qcanvas.get_data()).courses
161
+
162
+ @Slot()
163
+ def _on_course_selected(self, course: Optional[db.Course]) -> None:
164
+ if course is not None:
165
+ self._course_viewer_container.load_course(course)
166
+ else:
167
+ self._course_viewer_container.show_blank()
168
+
169
+ @asyncSlot()
170
+ async def _on_course_renamed(self, course: db.Course, new_name: str) -> None:
171
+ _logger.debug("Rename %s -> %s", course.name, new_name)
172
+
173
+ async with self._qcanvas.database.session() as session:
174
+ session.add(course)
175
+ course.configuration.nickname = new_name
176
+
177
+ @asyncSlot()
178
+ async def _open_quick_auth_in_browser(self):
179
+ opening_progress_dialog = QProgressDialog("Opening canvas", None, 0, 0, self)
180
+ opening_progress_dialog.setWindowTitle("Please wait")
181
+ opening_progress_dialog.show()
182
+ QDesktopServices.openUrl(
183
+ await self._qcanvas.canvas_client.get_temporary_session_url()
184
+ )
185
+ opening_progress_dialog.close()
186
+
187
+ @Slot()
188
+ def _open_downloads_folder(self) -> None:
189
+ # fixme hard coded path! >:(
190
+ QDesktopServices.openUrl(
191
+ QUrl(f"file://{(paths.data_storage() / 'downloads').absolute()}")
192
+ )
@@ -0,0 +1,153 @@
1
+ import logging
2
+ from asyncio import Lock
3
+ from dataclasses import dataclass
4
+ from typing import *
5
+
6
+ from qasync import asyncSlot
7
+ from qcanvas_backend.task_master import TaskID
8
+ from qtpy.QtWidgets import *
9
+
10
+ from qcanvas.backend_connectors import task_master
11
+
12
+ _logger = logging.getLogger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class _TaskProgress:
17
+ current: int
18
+ total: int
19
+
20
+
21
+ class StatusBarProgressDisplay(QStatusBar):
22
+ def __init__(self):
23
+ super().__init__()
24
+ self._lock = Lock()
25
+ self._tasks: dict[TaskID, _TaskProgress] = {}
26
+
27
+ self._progress_bar = self._setup_progress_bar()
28
+ self.addPermanentWidget(self._progress_bar)
29
+
30
+ task_master.task_progress.connect(self._on_task_progress)
31
+ task_master.task_failed.connect(self._on_task_failed)
32
+
33
+ self.showMessage("Ready", 5000)
34
+
35
+ def _setup_progress_bar(self) -> QProgressBar:
36
+ bar = QProgressBar()
37
+ bar.setTextVisible(True)
38
+ bar.hide()
39
+ return bar
40
+
41
+ @asyncSlot()
42
+ async def _on_task_progress(
43
+ self, task_id: TaskID, current: int, total: int
44
+ ) -> None:
45
+ _logger.debug("Progress %s: %i/%i", task_id, current, total)
46
+
47
+ async with self._lock:
48
+ if task_id not in self._tasks:
49
+ self._add_task(task_id, current, total)
50
+ if current == total and total != 0:
51
+ self._remove_task(task_id)
52
+ else:
53
+ self._update_task(task_id, current, total)
54
+
55
+ await self._update_task_status()
56
+
57
+ def _update_task(self, task_id: TaskID, current: int, total: int) -> None:
58
+ _logger.debug("Update %s", task_id)
59
+ task = self._tasks[task_id]
60
+ task.current = current
61
+ task.total = total
62
+
63
+ @asyncSlot()
64
+ async def _on_task_failed(self, task_id: TaskID, context: str | Exception) -> None:
65
+ _logger.debug("%s failed", task_id)
66
+
67
+ async with self._lock:
68
+ self._remove_task(task_id)
69
+
70
+ if self._has_no_tasks:
71
+ self._progress_bar.hide()
72
+
73
+ self.showMessage(f"Failed: {task_id.step_name}", 5000)
74
+
75
+ async def _update_task_status(self) -> None:
76
+ _logger.debug("Tasks: %s", self._tasks)
77
+ async with self._lock:
78
+ if self._has_no_tasks:
79
+ self._show_done()
80
+ elif self._has_single_task:
81
+ self._show_single_task_progress(list(self._tasks.items())[0])
82
+ else:
83
+ self._show_multiple_tasks_progress(list(self._tasks.values()))
84
+
85
+ def _show_done(self) -> None:
86
+ _logger.debug("Finished tasks. Tasks: %s", self._tasks)
87
+ self.showMessage("All tasks finished", 5000)
88
+ self._progress_bar.hide()
89
+
90
+ def _show_single_task_progress(self, task: Tuple[TaskID, _TaskProgress]) -> None:
91
+ _logger.debug("Single task %s", task)
92
+ id, progress = task
93
+
94
+ self._show_progress(progress)
95
+ self.showMessage(id.step_name)
96
+
97
+ def _show_multiple_tasks_progress(self, tasks: list[_TaskProgress]) -> None:
98
+ _logger.debug("Multiple tasks %s", tasks)
99
+ self.showMessage(f"{len(tasks)} tasks in progress")
100
+ self._show_progress(self._calculate_progress(tasks))
101
+
102
+ def _calculate_progress(self, tasks: list[_TaskProgress]) -> _TaskProgress:
103
+ # Used to represent 0..1 progress as 0..multiplier
104
+ multiplier = 1000
105
+ current_sum = 0
106
+ total_sum = 0
107
+
108
+ for task in tasks:
109
+ if task.total != 0:
110
+ current_sum += (task.current / task.total) * multiplier
111
+
112
+ total_sum += multiplier
113
+
114
+ _logger.debug(
115
+ "%s tasks, current=%i, total=%i", len(tasks), int(current_sum), total_sum
116
+ )
117
+
118
+ return _TaskProgress(int(current_sum), total_sum)
119
+
120
+ def _show_progress(self, progress: _TaskProgress) -> None:
121
+ self._progress_bar.setMaximum(progress.total)
122
+ self._progress_bar.setValue(progress.current)
123
+
124
+ if progress.total != 0:
125
+ self._progress_bar.setFormat(
126
+ f"{(progress.current / progress.total) * 100:.0f}%"
127
+ )
128
+ else:
129
+ self._progress_bar.setFormat("")
130
+
131
+ self._progress_bar.show()
132
+
133
+ def _add_task(self, task: TaskID, current: int, total: int) -> None:
134
+ self._tasks[task] = _TaskProgress(current, total)
135
+ _logger.debug("Added task %s", task)
136
+ _logger.debug("Tasks: %s", self._tasks)
137
+
138
+ def _remove_task(self, task: TaskID) -> None:
139
+ self._tasks.pop(task, None)
140
+ _logger.debug("Removed task %s", task)
141
+ _logger.debug("Tasks: %s", self._tasks)
142
+
143
+ @property
144
+ def _has_single_task(self) -> bool:
145
+ return len(self._tasks) == 1
146
+
147
+ @property
148
+ def _has_many_tasks(self) -> bool:
149
+ return len(self._tasks) > 1
150
+
151
+ @property
152
+ def _has_no_tasks(self) -> bool:
153
+ return len(self._tasks) == 0
@@ -0,0 +1,2 @@
1
+ from .memory_tree_widget import MemoryTreeWidget
2
+ from .memory_tree_widget_item import MemoryTreeWidgetItem
@@ -0,0 +1,66 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import *
4
+
5
+ from lightdb import LightDB, Model
6
+
7
+ from qcanvas.util import paths
8
+
9
+ _logger = logging.getLogger(__name__)
10
+
11
+
12
+ def _storage_path() -> Path:
13
+ path = paths.ui_storage() / "TREE.DB"
14
+ path.parent.mkdir(parents=True, exist_ok=True)
15
+ return path
16
+
17
+
18
+ _state_db = LightDB(str(_storage_path()))
19
+
20
+
21
+ class _TreeState(Model, table="trees", db=_state_db):
22
+ tree_name: str
23
+ collapsed_items: List[str] = []
24
+
25
+
26
+ def _get_or_create_state(name: str) -> _TreeState:
27
+ state = _TreeState.get(tree_name=name)
28
+
29
+ if state is None:
30
+ state = _TreeState.create(tree_name=name)
31
+ # Initialise the list here! Or else every instance has the same list object
32
+ state.collapsed_items = []
33
+ # Important or instances will get duplicated data in some cases
34
+ state.save()
35
+ return state
36
+ else:
37
+ return state
38
+
39
+
40
+ class TreeMemory:
41
+ def __init__(self, tree_name: str):
42
+ self._tree_name = tree_name
43
+ self._state = _get_or_create_state(tree_name)
44
+
45
+ def is_expanded(self, node_id: str) -> bool:
46
+ return node_id in self._state.expanded_items
47
+
48
+ def expanded(self, node_id: str) -> None:
49
+ self.set_expanded(node_id, True)
50
+
51
+ def collapsed(self, node_id: str) -> None:
52
+ self.set_expanded(node_id, False)
53
+
54
+ def set_expanded(self, node_id: str, expanded: bool) -> None:
55
+ contains = node_id in self._state.collapsed_items
56
+
57
+ if expanded and contains:
58
+ self._state.collapsed_items.remove(node_id)
59
+ self._state.save()
60
+ elif not expanded and not contains:
61
+ self._state.collapsed_items.append(node_id)
62
+ self._state.save()
63
+
64
+ @property
65
+ def collapsed_ids(self) -> List[str]:
66
+ return self._state.collapsed_items
@@ -0,0 +1,133 @@
1
+ import logging
2
+ from typing import *
3
+
4
+ from qtpy.QtCore import QItemSelectionModel, Slot
5
+ from qtpy.QtWidgets import QTreeWidget, QTreeWidgetItem, QWidget
6
+
7
+ from qcanvas.ui.memory_tree._tree_memory import TreeMemory
8
+ from qcanvas.ui.memory_tree.memory_tree_widget_item import MemoryTreeWidgetItem
9
+
10
+ _logger = logging.getLogger(__name__)
11
+
12
+
13
+ class MemoryTreeWidget(QTreeWidget):
14
+ def __init__(
15
+ self,
16
+ tree_name: str,
17
+ parent: Optional[QWidget] = None,
18
+ ):
19
+ super().__init__(parent)
20
+ self._id_map: dict[str, MemoryTreeWidgetItem] = {}
21
+ self._memory = TreeMemory(tree_name)
22
+ self._suppress_expansion_signals = False
23
+ self._suppress_selection_signal = False
24
+
25
+ self.itemExpanded.connect(self._expanded)
26
+ self.itemCollapsed.connect(self._collapsed)
27
+
28
+ def reexpand(self) -> None:
29
+ self.scheduleDelayedItemsLayout()
30
+
31
+ try:
32
+ self._suppress_expansion_signals = True
33
+
34
+ collapsed_ids = self._memory.collapsed_ids
35
+
36
+ for widget in self._id_map.values():
37
+ if widget.id not in collapsed_ids:
38
+ _logger.debug("Re-expand %s", widget.id)
39
+ self.expand(self.indexFromItem(widget, 0))
40
+ finally:
41
+ self._suppress_expansion_signals = False
42
+
43
+ def clear(self):
44
+ super().clear()
45
+ self._id_map.clear()
46
+
47
+ def select_ids(self, ids: List[str]) -> bool:
48
+ """
49
+ :returns: True if all ids were still found in the tree, False if one or more was missing
50
+ """
51
+ self._suppress_selection_signal = True
52
+
53
+ is_first = True
54
+ all_ids_in_tree = True
55
+
56
+ try:
57
+ for widget_id in ids:
58
+ if widget_id in self._id_map:
59
+ _logger.debug("Selected %s", widget_id)
60
+
61
+ flags = (
62
+ QItemSelectionModel.SelectionFlag.Rows
63
+ | QItemSelectionModel.SelectionFlag.Select
64
+ )
65
+
66
+ if is_first:
67
+ flags |= QItemSelectionModel.SelectionFlag.Clear
68
+
69
+ self.selectionModel().select(
70
+ self.indexFromItem(self._id_map[widget_id], 0), flags
71
+ )
72
+ else:
73
+ _logger.debug(
74
+ "Item %s is no longer in the tree, can't select it", widget_id
75
+ )
76
+ all_ids_in_tree = False
77
+ finally:
78
+ self._suppress_selection_signal = False
79
+
80
+ return all_ids_in_tree
81
+
82
+ def insertTopLevelItem(self, index: int, item: QTreeWidgetItem):
83
+ super().insertTopLevelItem(index, item)
84
+ self._add_widget_to_id_map(item)
85
+
86
+ def insertTopLevelItems(self, index: int, items: Sequence[QTreeWidgetItem]):
87
+ super().insertTopLevelItems(index, items)
88
+ self._add_widget_to_id_map(items)
89
+
90
+ def addTopLevelItems(self, items: Sequence[QTreeWidgetItem]):
91
+ super().addTopLevelItems(items)
92
+ self._add_widget_to_id_map(items)
93
+
94
+ def _add_widget_to_id_map(
95
+ self, widget: QTreeWidgetItem | Sequence[QTreeWidgetItem]
96
+ ):
97
+ map_updates = {}
98
+ widget_stack = widget if isinstance(widget, List) else [widget]
99
+
100
+ while len(widget_stack) > 0:
101
+ item = widget_stack.pop()
102
+
103
+ if isinstance(item, MemoryTreeWidgetItem):
104
+ if item.id in self._id_map or item.id in map_updates:
105
+ raise ValueError(f"Item with ID {item.id} is already in the tree")
106
+
107
+ map_updates[item.id] = item
108
+ _logger.debug("Add %s to map", item.id)
109
+
110
+ if item.childCount() > 0:
111
+ widget_stack.extend(
112
+ [item.child(index) for index in range(0, item.childCount())]
113
+ )
114
+
115
+ self._id_map.update(map_updates.items())
116
+
117
+ @Slot()
118
+ def _expanded(self, item: QTreeWidgetItem):
119
+ if self._suppress_expansion_signals:
120
+ return
121
+
122
+ if isinstance(item, MemoryTreeWidgetItem):
123
+ _logger.debug("Expanded %s", item.id)
124
+ self._memory.expanded(item.id)
125
+
126
+ @Slot()
127
+ def _collapsed(self, item: QTreeWidgetItem):
128
+ if self._suppress_expansion_signals:
129
+ return
130
+
131
+ if isinstance(item, MemoryTreeWidgetItem):
132
+ _logger.debug("Collapsed %s", item.id)
133
+ self._memory.collapsed(item.id)
@@ -0,0 +1,19 @@
1
+ import logging
2
+ from typing import *
3
+
4
+ from qtpy.QtWidgets import QTreeWidgetItem
5
+
6
+ _logger = logging.getLogger(__name__)
7
+
8
+
9
+ class MemoryTreeWidgetItem(QTreeWidgetItem):
10
+ def __init__(
11
+ self, id: str, data: Optional[object], strings: Optional[List[str]] = None
12
+ ):
13
+ super().__init__(strings)
14
+ self._id = id
15
+ self.extra_data = data
16
+
17
+ @property
18
+ def id(self) -> str:
19
+ return self._id
@@ -0,0 +1,2 @@
1
+ from .setup_checker import needs_setup
2
+ from .setup_dialog import SetupDialog
@@ -0,0 +1,17 @@
1
+ import logging
2
+
3
+ import qcanvas.util.settings as settings
4
+ from qcanvas.util import is_url
5
+
6
+ _logger = logging.getLogger(__name__)
7
+
8
+
9
+ def needs_setup() -> bool:
10
+ if not is_url(settings.client.panopto_url):
11
+ return True
12
+ elif not is_url(settings.client.canvas_url):
13
+ return True
14
+ elif len(settings.client.canvas_api_key) == 0:
15
+ return True
16
+ else:
17
+ return False