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.
- qcanvas/app_start/__init__.py +47 -0
- qcanvas/backend_connectors/__init__.py +2 -0
- qcanvas/backend_connectors/frontend_resource_manager.py +63 -0
- qcanvas/backend_connectors/qcanvas_task_master.py +28 -0
- qcanvas/icons/__init__.py +6 -0
- qcanvas/icons/file-download-failed.svg +6 -0
- qcanvas/icons/file-downloaded.svg +6 -0
- qcanvas/icons/file-not-downloaded.svg +6 -0
- qcanvas/icons/file-unknown.svg +6 -0
- qcanvas/icons/icons.qrc +4 -0
- qcanvas/icons/main_icon.svg +7 -7
- qcanvas/icons/rc_icons.py +580 -214
- qcanvas/icons/sync.svg +6 -6
- qcanvas/run.py +29 -0
- qcanvas/ui/course_viewer/__init__.py +2 -0
- qcanvas/ui/course_viewer/content_tree.py +123 -0
- qcanvas/ui/course_viewer/course_tree.py +93 -0
- qcanvas/ui/course_viewer/course_viewer.py +62 -0
- qcanvas/ui/course_viewer/tabs/__init__.py +3 -0
- qcanvas/ui/course_viewer/tabs/assignment_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +168 -0
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +104 -0
- qcanvas/ui/course_viewer/tabs/content_tab.py +96 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +68 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +70 -0
- qcanvas/ui/course_viewer/tabs/page_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +36 -0
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +74 -0
- qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +176 -0
- qcanvas/ui/course_viewer/tabs/util.py +1 -0
- qcanvas/ui/main_ui/course_viewer_container.py +52 -0
- qcanvas/ui/main_ui/options/__init__.py +3 -0
- qcanvas/ui/main_ui/options/quick_sync_option.py +25 -0
- qcanvas/ui/main_ui/options/sync_on_start_option.py +25 -0
- qcanvas/ui/main_ui/qcanvas_window.py +192 -0
- qcanvas/ui/main_ui/status_bar_progress_display.py +153 -0
- qcanvas/ui/memory_tree/__init__.py +2 -0
- qcanvas/ui/memory_tree/_tree_memory.py +66 -0
- qcanvas/ui/memory_tree/memory_tree_widget.py +133 -0
- qcanvas/ui/memory_tree/memory_tree_widget_item.py +19 -0
- qcanvas/ui/setup/__init__.py +2 -0
- qcanvas/ui/setup/setup_checker.py +17 -0
- qcanvas/ui/setup/setup_dialog.py +212 -0
- qcanvas/util/__init__.py +2 -0
- qcanvas/util/basic_fonts.py +12 -0
- qcanvas/util/fe_resource_manager.py +23 -0
- qcanvas/util/html_cleaner.py +25 -0
- qcanvas/util/layouts.py +52 -0
- qcanvas/util/logs.py +6 -0
- qcanvas/util/paths.py +41 -0
- qcanvas/util/settings/__init__.py +9 -0
- qcanvas/util/settings/_client_settings.py +29 -0
- qcanvas/util/settings/_mapped_setting.py +63 -0
- qcanvas/util/settings/_ui_settings.py +34 -0
- qcanvas/util/ui_tools.py +41 -0
- qcanvas/util/url_checker.py +13 -0
- qcanvas-1.0.3.post0.dist-info/METADATA +61 -0
- qcanvas-1.0.3.post0.dist-info/RECORD +64 -0
- {qcanvas-0.0.5.7a0.dist-info → qcanvas-1.0.3.post0.dist-info}/WHEEL +1 -1
- qcanvas-1.0.3.post0.dist-info/entry_points.txt +3 -0
- qcanvas/__main__.py +0 -155
- qcanvas/db/__init__.py +0 -5
- qcanvas/db/database.py +0 -338
- qcanvas/db/db_converter_helper.py +0 -81
- qcanvas/net/canvas/__init__.py +0 -2
- qcanvas/net/canvas/canvas_client.py +0 -209
- qcanvas/net/canvas/legacy_canvas_types.py +0 -124
- qcanvas/net/custom_httpx_async_transport.py +0 -34
- qcanvas/net/self_authenticating.py +0 -108
- qcanvas/queries/__init__.py +0 -4
- qcanvas/queries/all_courses.gql +0 -7
- qcanvas/queries/all_courses.py +0 -108
- qcanvas/queries/canvas_course_data.gql +0 -51
- qcanvas/queries/canvas_course_data.py +0 -143
- qcanvas/ui/container_item.py +0 -11
- qcanvas/ui/main_ui.py +0 -251
- qcanvas/ui/menu_bar/__init__.py +0 -0
- qcanvas/ui/menu_bar/grouping_preferences_menu.py +0 -61
- qcanvas/ui/menu_bar/theme_selection_menu.py +0 -39
- qcanvas/ui/setup_dialog.py +0 -190
- qcanvas/ui/status_bar_reporter.py +0 -40
- qcanvas/ui/viewer/__init__.py +0 -0
- qcanvas/ui/viewer/course_list.py +0 -96
- qcanvas/ui/viewer/file_list.py +0 -195
- qcanvas/ui/viewer/file_view_tab.py +0 -62
- qcanvas/ui/viewer/page_list_viewer.py +0 -150
- qcanvas/util/app_settings.py +0 -98
- qcanvas/util/constants.py +0 -5
- qcanvas/util/course_indexer/__init__.py +0 -1
- qcanvas/util/course_indexer/conversion_helpers.py +0 -78
- qcanvas/util/course_indexer/data_manager.py +0 -447
- qcanvas/util/course_indexer/resource_helpers.py +0 -191
- qcanvas/util/download_pool.py +0 -58
- qcanvas/util/helpers/__init__.py +0 -0
- qcanvas/util/helpers/canvas_sanitiser.py +0 -47
- qcanvas/util/helpers/file_icon_helper.py +0 -34
- qcanvas/util/helpers/qaction_helper.py +0 -25
- qcanvas/util/helpers/theme_helper.py +0 -48
- qcanvas/util/link_scanner/__init__.py +0 -2
- qcanvas/util/link_scanner/canvas_link_scanner.py +0 -41
- qcanvas/util/link_scanner/canvas_media_object_scanner.py +0 -60
- qcanvas/util/link_scanner/dropbox_scanner.py +0 -68
- qcanvas/util/link_scanner/resource_scanner.py +0 -69
- qcanvas/util/progress_reporter.py +0 -101
- qcanvas/util/self_updater.py +0 -55
- qcanvas/util/task_pool.py +0 -253
- qcanvas/util/tree_util/__init__.py +0 -3
- qcanvas/util/tree_util/expanding_tree.py +0 -165
- qcanvas/util/tree_util/model_helpers.py +0 -36
- qcanvas/util/tree_util/tree_model.py +0 -85
- qcanvas-0.0.5.7a0.dist-info/METADATA +0 -21
- qcanvas-0.0.5.7a0.dist-info/RECORD +0 -62
- /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,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,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
|