qcanvas 1.0.11__py3-none-any.whl → 2026.1.19__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.
- qcanvas/__init__.py +60 -0
- qcanvas/app.py +72 -0
- qcanvas/backend_connectors/frontend_resource_manager.py +13 -5
- qcanvas/backend_connectors/qcanvas_task_master.py +2 -2
- qcanvas/icons/__init__.py +55 -6
- qcanvas/icons/_icon_type.py +42 -0
- qcanvas/icons/icons.qrc +48 -8
- qcanvas/icons/rc_icons.py +2477 -566
- qcanvas/settings/__init__.py +6 -0
- qcanvas/{util/settings → settings}/_client_settings.py +15 -6
- qcanvas/settings/_course_settings.py +54 -0
- qcanvas/{util/settings → settings}/_mapped_setting.py +8 -6
- qcanvas/{util/settings → settings}/_ui_settings.py +5 -5
- qcanvas/theme.py +101 -0
- qcanvas/ui/course_viewer/content_tree.py +37 -19
- qcanvas/ui/course_viewer/course_tree/__init__.py +1 -0
- qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +86 -0
- qcanvas/ui/course_viewer/{course_tree.py → course_tree/course_tree.py} +29 -14
- qcanvas/ui/course_viewer/course_viewer.py +79 -46
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +107 -29
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +19 -18
- qcanvas/ui/course_viewer/tabs/content_tab.py +33 -39
- qcanvas/ui/course_viewer/tabs/file_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +46 -0
- qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +96 -0
- qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +55 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +50 -27
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +18 -19
- qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +3 -3
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +18 -16
- qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +61 -74
- qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
- qcanvas/ui/memory_tree/_tree_memory.py +45 -41
- qcanvas/ui/memory_tree/memory_tree_widget.py +22 -18
- qcanvas/ui/memory_tree/memory_tree_widget_item.py +3 -3
- qcanvas/ui/qcanvas_window/__init__.py +1 -0
- qcanvas/ui/qcanvas_window/course_viewer_container.py +95 -0
- qcanvas/ui/{main_ui → qcanvas_window}/options/auto_download_resources_option.py +8 -6
- qcanvas/ui/{main_ui → qcanvas_window}/options/quick_sync_option.py +7 -6
- qcanvas/ui/{main_ui → qcanvas_window}/options/sync_on_start_option.py +7 -6
- qcanvas/ui/{main_ui → qcanvas_window}/options/theme_selection_menu.py +12 -10
- qcanvas/ui/{main_ui → qcanvas_window}/qcanvas_window.py +74 -45
- qcanvas/ui/{main_ui → qcanvas_window}/status_bar_progress_display.py +20 -12
- qcanvas/ui/qml_components/__init__.py +4 -0
- qcanvas/ui/qml_components/attachments_pane.py +70 -0
- qcanvas/ui/qml_components/comments_pane.py +83 -0
- qcanvas/ui/qml_components/qml/AttachmentsList.ui.qml +15 -0
- qcanvas/ui/qml_components/qml/AttachmentsListDelegate.qml +77 -0
- qcanvas/ui/qml_components/qml/AttachmentsListModel.qml +19 -0
- qcanvas/ui/qml_components/qml/AttachmentsPane.qml +11 -0
- qcanvas/ui/qml_components/qml/CommentsList.ui.qml +15 -0
- qcanvas/ui/qml_components/qml/CommentsListDelegate.ui.qml +118 -0
- qcanvas/ui/qml_components/qml/CommentsListModel.qml +56 -0
- qcanvas/ui/qml_components/qml/CommentsPane.qml +11 -0
- qcanvas/ui/qml_components/qml/DecoratedText.ui.qml +44 -0
- qcanvas/ui/qml_components/qml/Spacer.ui.qml +7 -0
- qcanvas/ui/qml_components/qml/ThemedRectangle.qml +53 -0
- qcanvas/ui/qml_components/qml/__init__.py +3 -0
- qcanvas/ui/qml_components/qml/rc_qml.py +709 -0
- qcanvas/ui/qml_components/qml/rc_qml.qrc +16 -0
- qcanvas/ui/qml_components/qml_bridge_types.py +95 -0
- qcanvas/ui/qml_components/qml_pane.py +21 -0
- qcanvas/ui/setup/setup_checker.py +3 -3
- qcanvas/ui/setup/setup_dialog.py +173 -80
- qcanvas/util/__init__.py +0 -2
- qcanvas/util/auto_downloader.py +9 -8
- qcanvas/util/basic_fonts.py +2 -2
- qcanvas/util/context_dict.py +12 -0
- qcanvas/util/file_icons.py +46 -0
- qcanvas/util/html_cleaner.py +2 -0
- qcanvas/util/layouts.py +9 -8
- qcanvas/util/paths.py +26 -22
- qcanvas/util/qurl_util.py +1 -1
- qcanvas/util/runtime.py +20 -0
- qcanvas/util/ui_tools.py +121 -7
- qcanvas/util/url_checker.py +1 -1
- qcanvas-2026.1.19.dist-info/METADATA +95 -0
- qcanvas-2026.1.19.dist-info/RECORD +92 -0
- {qcanvas-1.0.11.dist-info → qcanvas-2026.1.19.dist-info}/WHEEL +1 -1
- qcanvas-2026.1.19.dist-info/entry_points.txt +3 -0
- qcanvas/app_start/__init__.py +0 -54
- qcanvas/icons/file-download-failed.svg +0 -6
- qcanvas/icons/file-downloaded.svg +0 -6
- qcanvas/icons/file-not-downloaded.svg +0 -6
- qcanvas/icons/file-unknown.svg +0 -6
- qcanvas/icons/main_icon.svg +0 -325
- qcanvas/icons/sync.svg +0 -7
- qcanvas/run.py +0 -30
- qcanvas/ui/main_ui/__init__.py +0 -0
- qcanvas/ui/main_ui/course_viewer_container.py +0 -52
- qcanvas/util/settings/__init__.py +0 -9
- qcanvas/util/themes.py +0 -24
- qcanvas-1.0.11.dist-info/METADATA +0 -61
- qcanvas-1.0.11.dist-info/RECORD +0 -68
- qcanvas-1.0.11.dist-info/entry_points.txt +0 -3
- /qcanvas/ui/course_viewer/tabs/{util.py → constants.py} +0 -0
- /qcanvas/ui/{main_ui → qcanvas_window}/options/__init__.py +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from PySide6.QtCore import Property, QObject, Signal
|
|
2
|
+
from PySide6.QtQml import ListProperty, QmlElement
|
|
3
|
+
from libqcanvas.database.tables import ResourceDownloadState
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
QML_IMPORT_NAME = "QCanvas"
|
|
7
|
+
QML_IMPORT_MAJOR_VERSION = 1
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@QmlElement
|
|
11
|
+
class Attachment(QObject):
|
|
12
|
+
file_name_changed = Signal()
|
|
13
|
+
resource_id_changed = Signal()
|
|
14
|
+
download_state_changed = Signal()
|
|
15
|
+
|
|
16
|
+
# Emitted by AttachmentsListDelegate when the user clicks on an attachment
|
|
17
|
+
opened = Signal(str)
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
file_name: str,
|
|
22
|
+
resource_id: str,
|
|
23
|
+
download_state: ResourceDownloadState,
|
|
24
|
+
parent: QObject | None = None,
|
|
25
|
+
):
|
|
26
|
+
super().__init__(parent)
|
|
27
|
+
self._file_name = file_name
|
|
28
|
+
self._resource_id = resource_id
|
|
29
|
+
self._download_state = download_state.name
|
|
30
|
+
|
|
31
|
+
@Property(str, notify=file_name_changed)
|
|
32
|
+
def file_name(self) -> str:
|
|
33
|
+
return self._file_name
|
|
34
|
+
|
|
35
|
+
@Property(str, notify=resource_id_changed)
|
|
36
|
+
def resource_id(self) -> str:
|
|
37
|
+
return self._resource_id
|
|
38
|
+
|
|
39
|
+
@Property(str, notify=download_state_changed)
|
|
40
|
+
def download_state(self) -> str:
|
|
41
|
+
return self._download_state
|
|
42
|
+
|
|
43
|
+
@download_state.setter
|
|
44
|
+
def download_state(self, value: ResourceDownloadState):
|
|
45
|
+
if value.name != self._download_state:
|
|
46
|
+
self._download_state = value.name
|
|
47
|
+
self.download_state_changed.emit()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@QmlElement
|
|
51
|
+
class Comment(QObject):
|
|
52
|
+
body_changed = Signal()
|
|
53
|
+
author_changed = Signal()
|
|
54
|
+
date_changed = Signal()
|
|
55
|
+
attachments_changed = Signal()
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
body: str,
|
|
60
|
+
author: str,
|
|
61
|
+
date: str,
|
|
62
|
+
attachments: list[Attachment],
|
|
63
|
+
parent: QObject | None = None,
|
|
64
|
+
):
|
|
65
|
+
super().__init__(parent)
|
|
66
|
+
|
|
67
|
+
self._body = body
|
|
68
|
+
self._author = author
|
|
69
|
+
self._date = date
|
|
70
|
+
self._attachments = attachments
|
|
71
|
+
|
|
72
|
+
@Property(str, notify=body_changed)
|
|
73
|
+
def body(self) -> str:
|
|
74
|
+
return self._body
|
|
75
|
+
|
|
76
|
+
@Property(str, notify=date_changed)
|
|
77
|
+
def date(self) -> str:
|
|
78
|
+
return self._date
|
|
79
|
+
|
|
80
|
+
@Property(str, notify=author_changed)
|
|
81
|
+
def author(self) -> str:
|
|
82
|
+
return self._author
|
|
83
|
+
|
|
84
|
+
def attachment(self, n) -> Attachment:
|
|
85
|
+
return self._attachments[n]
|
|
86
|
+
|
|
87
|
+
def attachment_count(self) -> int:
|
|
88
|
+
return len(self._attachments)
|
|
89
|
+
|
|
90
|
+
# You must set `count`, `at` and `notify` EXPLICTLY (even if you name them according to the examples, which examples are all inconsistent and wrong).
|
|
91
|
+
# Qt?? Are you ok???
|
|
92
|
+
# Oh and you have to use `.length` instead of `.count` on the QML side because javascript.
|
|
93
|
+
attachments = ListProperty(
|
|
94
|
+
Attachment, count=attachment_count, at=attachment, notify=attachments_changed
|
|
95
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from PySide6.QtCore import QUrl
|
|
2
|
+
from PySide6.QtQuick import QQuickView
|
|
3
|
+
from PySide6.QtWidgets import QGroupBox, QWidget
|
|
4
|
+
|
|
5
|
+
from qcanvas.theme import app_theme
|
|
6
|
+
from qcanvas.util.context_dict import ContextDict
|
|
7
|
+
import qcanvas.util.ui_tools as ui
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QmlPane(QGroupBox):
|
|
11
|
+
def __init__(self, qml_path: QUrl, parent: QWidget | None = None):
|
|
12
|
+
super().__init__(parent)
|
|
13
|
+
self.qview = QQuickView(parent)
|
|
14
|
+
self._qml_path = qml_path
|
|
15
|
+
self.ctx = ContextDict(self.qview.rootContext())
|
|
16
|
+
self.ctx["appTheme"] = app_theme
|
|
17
|
+
|
|
18
|
+
self.setLayout(ui.hbox(QWidget.createWindowContainer(self.qview, self)))
|
|
19
|
+
|
|
20
|
+
def load_view(self):
|
|
21
|
+
self.qview.setSource(self._qml_path)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
import qcanvas.
|
|
4
|
-
from qcanvas.util import is_url
|
|
3
|
+
import qcanvas.settings as settings
|
|
4
|
+
from qcanvas.util.url_checker import is_url
|
|
5
5
|
|
|
6
6
|
_logger = logging.getLogger(__name__)
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def needs_setup() -> bool:
|
|
10
|
-
if not is_url(settings.client.panopto_url):
|
|
10
|
+
if not settings.client.panopto_disabled and not is_url(settings.client.panopto_url):
|
|
11
11
|
return True
|
|
12
12
|
elif not is_url(settings.client.canvas_url):
|
|
13
13
|
return True
|
qcanvas/ui/setup/setup_dialog.py
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from threading import Semaphore
|
|
3
|
-
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from libqcanvas_clients.canvas import CanvasClient, CanvasClientConfig
|
|
6
|
+
from libqcanvas_clients.panopto import PanoptoClient, PanoptoClientConfig
|
|
7
|
+
from libqcanvas_clients.util.request_exceptions import ConfigInvalidError
|
|
8
|
+
from PySide6.QtCore import Qt, QUrl, Slot
|
|
9
|
+
from PySide6.QtGui import QDesktopServices, QIcon
|
|
10
|
+
from PySide6.QtWidgets import (
|
|
11
|
+
QCheckBox,
|
|
12
|
+
QDialog,
|
|
13
|
+
QDialogButtonBox,
|
|
14
|
+
QErrorMessage,
|
|
15
|
+
QLabel,
|
|
16
|
+
QLineEdit,
|
|
17
|
+
QMessageBox,
|
|
18
|
+
QProgressBar,
|
|
19
|
+
QVBoxLayout,
|
|
20
|
+
QWidget,
|
|
21
|
+
)
|
|
4
22
|
from qasync import asyncSlot
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
from qcanvas_api_clients.util.request_exceptions import ConfigInvalidError
|
|
8
|
-
from qtpy.QtCore import QUrl, Signal, Slot
|
|
9
|
-
from qtpy.QtGui import QDesktopServices, QIcon
|
|
10
|
-
from qtpy.QtWidgets import *
|
|
11
|
-
|
|
12
|
-
import qcanvas.util.settings as settings
|
|
23
|
+
|
|
24
|
+
import qcanvas.settings as settings
|
|
13
25
|
from qcanvas import icons
|
|
14
|
-
from qcanvas.util import
|
|
15
|
-
from qcanvas.util.
|
|
26
|
+
from qcanvas.util.layouts import GridItem, grid_layout_widget, layout
|
|
27
|
+
from qcanvas.util.url_checker import is_url
|
|
16
28
|
|
|
17
29
|
_logger = logging.getLogger(__name__)
|
|
18
30
|
|
|
@@ -21,50 +33,132 @@ _tutorial_url = (
|
|
|
21
33
|
)
|
|
22
34
|
|
|
23
35
|
|
|
24
|
-
class
|
|
25
|
-
|
|
36
|
+
class _InputRow:
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
label: str,
|
|
41
|
+
initial_value: str,
|
|
42
|
+
placeholder_text: Optional[str] = None,
|
|
43
|
+
is_password: bool = False,
|
|
44
|
+
):
|
|
45
|
+
self._label = QLabel(label)
|
|
46
|
+
self._input = QLineEdit(initial_value)
|
|
47
|
+
|
|
48
|
+
if placeholder_text is not None:
|
|
49
|
+
self._input.setPlaceholderText(placeholder_text)
|
|
50
|
+
|
|
51
|
+
if is_password:
|
|
52
|
+
self._input.setEchoMode(QLineEdit.EchoMode.Password)
|
|
53
|
+
|
|
54
|
+
def set_error(self, message: Optional[str]) -> None:
|
|
55
|
+
self._input.setStyleSheet("QLineEdit { border: 1px solid red }")
|
|
56
|
+
self._input.setToolTip(message)
|
|
57
|
+
|
|
58
|
+
def clear_error(self) -> None:
|
|
59
|
+
self._input.setStyleSheet(None)
|
|
60
|
+
self._input.setToolTip(None)
|
|
61
|
+
|
|
62
|
+
def grid_row(self) -> list[QWidget]:
|
|
63
|
+
return [self._label, self._input]
|
|
64
|
+
|
|
65
|
+
def disable(self) -> None:
|
|
66
|
+
self._input.setEnabled(False)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def enabled(self) -> bool:
|
|
70
|
+
return self._input.isEnabled()
|
|
71
|
+
|
|
72
|
+
@enabled.setter
|
|
73
|
+
def enabled(self, value: bool) -> None:
|
|
74
|
+
self._input.setEnabled(value)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def text(self) -> str:
|
|
78
|
+
return self._input.text().strip()
|
|
26
79
|
|
|
80
|
+
@property
|
|
81
|
+
def url_text(self) -> str:
|
|
82
|
+
url = self.text
|
|
83
|
+
|
|
84
|
+
if not url.startswith("http"):
|
|
85
|
+
return "https://" + url
|
|
86
|
+
else:
|
|
87
|
+
return url
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def is_valid_url(self) -> bool:
|
|
91
|
+
return is_url(self.url_text)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def is_empty(self) -> bool:
|
|
95
|
+
return len(self.text) == 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class SetupDialog(QDialog):
|
|
27
99
|
def __init__(self):
|
|
28
100
|
super().__init__()
|
|
29
101
|
|
|
30
102
|
self.setWindowTitle("Configure QCanvas")
|
|
31
103
|
self.setMinimumSize(550, 200)
|
|
32
104
|
self.resize(550, 200)
|
|
33
|
-
self.setWindowIcon(QIcon(icons.main_icon))
|
|
105
|
+
self.setWindowIcon(QIcon(icons.branding.main_icon))
|
|
34
106
|
|
|
35
107
|
self._semaphore = Semaphore()
|
|
36
|
-
|
|
37
|
-
self._canvas_url_box
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
108
|
+
|
|
109
|
+
self._canvas_url_box = _InputRow(
|
|
110
|
+
label="Canvas URL",
|
|
111
|
+
initial_value=settings.client.canvas_url,
|
|
112
|
+
placeholder_text="https://instance.canvas.com",
|
|
113
|
+
)
|
|
114
|
+
self._panopto_url_box = _InputRow(
|
|
115
|
+
label="Panopto URL",
|
|
116
|
+
initial_value=settings.client.panopto_url,
|
|
117
|
+
placeholder_text="https://instance.panopto.com",
|
|
118
|
+
)
|
|
119
|
+
self._canvas_api_key_box = _InputRow(
|
|
120
|
+
label="Canvas API Key",
|
|
121
|
+
initial_value=settings.client.canvas_api_key,
|
|
122
|
+
is_password=True,
|
|
123
|
+
)
|
|
124
|
+
self._disable_panopto_checkbox = QCheckBox("Continue without Panopto")
|
|
125
|
+
self._disable_panopto_checkbox.setChecked(settings.client.panopto_disabled)
|
|
126
|
+
self._disable_panopto_checkbox.checkStateChanged.connect(
|
|
127
|
+
self._disable_panopto_check_changed
|
|
128
|
+
)
|
|
129
|
+
self._panopto_url_box.enabled = not settings.client.panopto_disabled
|
|
42
130
|
self._button_box = self._setup_button_box()
|
|
43
|
-
self._button_box.accepted.connect(self._accepted)
|
|
44
|
-
self._button_box.helpRequested.connect(self._help_requested)
|
|
45
131
|
self._waiting_indicator = self._setup_progress_bar()
|
|
46
|
-
self._status_bar = QStatusBar()
|
|
47
132
|
|
|
48
133
|
self.setLayout(
|
|
49
134
|
layout(
|
|
50
135
|
QVBoxLayout,
|
|
51
136
|
grid_layout_widget(
|
|
52
137
|
[
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
138
|
+
self._canvas_url_box.grid_row(),
|
|
139
|
+
self._canvas_api_key_box.grid_row(),
|
|
140
|
+
self._panopto_url_box.grid_row(),
|
|
141
|
+
[
|
|
142
|
+
GridItem(
|
|
143
|
+
self._disable_panopto_checkbox,
|
|
144
|
+
col_span=2,
|
|
145
|
+
alignment=Qt.AlignmentFlag.AlignRight,
|
|
146
|
+
)
|
|
147
|
+
],
|
|
56
148
|
]
|
|
57
149
|
),
|
|
58
150
|
self._waiting_indicator,
|
|
59
151
|
self._button_box,
|
|
60
|
-
self._status_bar,
|
|
61
152
|
)
|
|
62
153
|
)
|
|
63
154
|
|
|
64
155
|
def _setup_button_box(self) -> QDialogButtonBox:
|
|
65
156
|
box = QDialogButtonBox()
|
|
66
157
|
box.addButton(QDialogButtonBox.StandardButton.Ok)
|
|
67
|
-
box.addButton("Get a Canvas API
|
|
158
|
+
box.addButton("Get a Canvas API Key", QDialogButtonBox.ButtonRole.HelpRole)
|
|
159
|
+
|
|
160
|
+
box.accepted.connect(self._verify_settings)
|
|
161
|
+
box.helpRequested.connect(self._help_requested)
|
|
68
162
|
return box
|
|
69
163
|
|
|
70
164
|
def _setup_progress_bar(self) -> QProgressBar:
|
|
@@ -81,32 +175,33 @@ class SetupDialog(QDialog):
|
|
|
81
175
|
widget.setSizePolicy(size_policy)
|
|
82
176
|
|
|
83
177
|
@asyncSlot()
|
|
84
|
-
async def
|
|
178
|
+
async def _verify_settings(self) -> None:
|
|
85
179
|
if self._semaphore.acquire(False):
|
|
86
180
|
try:
|
|
87
181
|
self._clear_errors()
|
|
88
182
|
|
|
89
|
-
if not self.
|
|
90
|
-
self._status_bar.showMessage("Invalid input!", 5000)
|
|
183
|
+
if not self._check_all_inputs():
|
|
91
184
|
return
|
|
92
185
|
|
|
93
186
|
self._waiting_indicator.setVisible(True)
|
|
94
|
-
self._status_bar.showMessage("Checking configuration...")
|
|
95
187
|
|
|
96
188
|
canvas_config = CanvasClientConfig(
|
|
97
|
-
api_token=self._canvas_api_key_box.text
|
|
98
|
-
canvas_url=self.
|
|
189
|
+
api_token=self._canvas_api_key_box.text,
|
|
190
|
+
canvas_url=self._canvas_url_box.url_text,
|
|
99
191
|
)
|
|
100
192
|
|
|
101
193
|
if not await self._check_canvas_config(canvas_config):
|
|
102
194
|
return
|
|
103
195
|
|
|
104
|
-
if
|
|
105
|
-
self.
|
|
106
|
-
|
|
196
|
+
if self._panopto_enabled:
|
|
197
|
+
if not await self._check_panopto_config(canvas_config):
|
|
198
|
+
self._show_panopto_help()
|
|
199
|
+
return
|
|
107
200
|
except Exception as e:
|
|
108
|
-
self._status_bar.showMessage(f"An error occurred: {e}", 5000)
|
|
109
201
|
_logger.warning("Checking config failed", exc_info=e)
|
|
202
|
+
|
|
203
|
+
error_box = QErrorMessage(self)
|
|
204
|
+
error_box.showMessage(f"Checking config failed: {e}")
|
|
110
205
|
finally:
|
|
111
206
|
self._waiting_indicator.setVisible(False)
|
|
112
207
|
self._semaphore.release()
|
|
@@ -117,59 +212,40 @@ class SetupDialog(QDialog):
|
|
|
117
212
|
_logger.debug("Validation already in progress")
|
|
118
213
|
|
|
119
214
|
def _clear_errors(self) -> None:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
self._status_bar.clearMessage()
|
|
126
|
-
line_edit.setStyleSheet(None)
|
|
127
|
-
line_edit.setToolTip(None)
|
|
128
|
-
|
|
129
|
-
def _all_inputs_valid(self) -> bool:
|
|
215
|
+
self._canvas_url_box.clear_error()
|
|
216
|
+
self._panopto_url_box.clear_error()
|
|
217
|
+
self._canvas_api_key_box.clear_error()
|
|
218
|
+
|
|
219
|
+
def _check_all_inputs(self) -> bool:
|
|
130
220
|
all_valid = True
|
|
131
221
|
|
|
132
|
-
if not
|
|
222
|
+
if not self._canvas_url_box.is_valid_url:
|
|
133
223
|
all_valid = False
|
|
134
|
-
self.
|
|
135
|
-
|
|
224
|
+
self._canvas_url_box.set_error("Canvas URL is invalid")
|
|
225
|
+
|
|
226
|
+
if self._canvas_api_key_box.is_empty:
|
|
136
227
|
all_valid = False
|
|
137
|
-
self.
|
|
138
|
-
|
|
228
|
+
self._canvas_api_key_box.set_error("Canvas API key is empty")
|
|
229
|
+
|
|
230
|
+
if self._panopto_enabled and not self._panopto_url_box.is_valid_url:
|
|
139
231
|
all_valid = False
|
|
140
|
-
self.
|
|
232
|
+
self._panopto_url_box.set_error("Panopto URL is invalid")
|
|
141
233
|
|
|
142
234
|
return all_valid
|
|
143
235
|
|
|
144
|
-
def _get_url(self, line_edit: QLineEdit) -> str:
|
|
145
|
-
url = line_edit.text().strip()
|
|
146
|
-
|
|
147
|
-
if not url.startswith("http"):
|
|
148
|
-
return "https://" + url
|
|
149
|
-
else:
|
|
150
|
-
return url
|
|
151
|
-
|
|
152
236
|
async def _check_canvas_config(self, canvas_config: CanvasClientConfig) -> bool:
|
|
153
237
|
try:
|
|
154
238
|
await CanvasClient.verify_config(canvas_config)
|
|
155
239
|
return True
|
|
156
240
|
except ConfigInvalidError:
|
|
157
|
-
self.
|
|
241
|
+
self._canvas_api_key_box.set_error("Canvas API key is invalid")
|
|
158
242
|
return False
|
|
159
243
|
|
|
160
|
-
def _show_error(self, line_edit: QLineEdit, text: str) -> None:
|
|
161
|
-
line_edit.setToolTip(text)
|
|
162
|
-
self._waiting_indicator.hide()
|
|
163
|
-
self._highlight_line_edit(line_edit)
|
|
164
|
-
|
|
165
|
-
def _highlight_line_edit(self, line_edit: QLineEdit) -> None:
|
|
166
|
-
line_edit.setStyleSheet("QLineEdit { border: 1px solid red }")
|
|
167
|
-
|
|
168
244
|
async def _check_panopto_config(self, canvas_config: CanvasClientConfig) -> bool:
|
|
169
245
|
client = CanvasClient(canvas_config)
|
|
170
246
|
try:
|
|
171
247
|
await PanoptoClient.verify_config(
|
|
172
|
-
PanoptoClientConfig(panopto_url=self.
|
|
248
|
+
PanoptoClientConfig(panopto_url=self._panopto_url_box.url_text),
|
|
173
249
|
client,
|
|
174
250
|
)
|
|
175
251
|
return True
|
|
@@ -189,22 +265,29 @@ class SetupDialog(QDialog):
|
|
|
189
265
|
QMessageBox.StandardButton.Ok,
|
|
190
266
|
self,
|
|
191
267
|
)
|
|
192
|
-
msg.accepted.connect(
|
|
268
|
+
msg.accepted.connect(
|
|
269
|
+
self._open_panopto_login, Qt.ConnectionType.SingleShotConnection
|
|
270
|
+
)
|
|
193
271
|
msg.show()
|
|
194
272
|
|
|
195
273
|
@Slot()
|
|
196
274
|
def _open_panopto_login(self) -> None:
|
|
197
|
-
url = QUrl(self.
|
|
275
|
+
url = QUrl(self._panopto_url_box.url_text)
|
|
198
276
|
url.setPath("/Panopto/Pages/Auth/Login.aspx")
|
|
199
277
|
url.setQuery("instance=Canvas&AllowBounce=true")
|
|
200
278
|
QDesktopServices.openUrl(url)
|
|
201
279
|
|
|
202
280
|
def _save_and_close(self) -> None:
|
|
203
|
-
settings.client.canvas_url = self.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
281
|
+
settings.client.canvas_url = self._canvas_url_box.url_text
|
|
282
|
+
|
|
283
|
+
if self._panopto_enabled:
|
|
284
|
+
settings.client.panopto_url = self._panopto_url_box.url_text
|
|
285
|
+
else:
|
|
286
|
+
settings.client.panopto_disabled = True
|
|
287
|
+
|
|
288
|
+
settings.client.canvas_api_key = self._canvas_api_key_box.text
|
|
289
|
+
|
|
290
|
+
self.accept()
|
|
208
291
|
|
|
209
292
|
@Slot()
|
|
210
293
|
def _help_requested(self) -> None:
|
|
@@ -217,9 +300,19 @@ class SetupDialog(QDialog):
|
|
|
217
300
|
"Don't share this key. You can revoke it at any time.",
|
|
218
301
|
parent=self,
|
|
219
302
|
)
|
|
220
|
-
msg.accepted.connect(
|
|
303
|
+
msg.accepted.connect(
|
|
304
|
+
self._open_tutorial, Qt.ConnectionType.SingleShotConnection
|
|
305
|
+
)
|
|
221
306
|
msg.show()
|
|
222
307
|
|
|
223
308
|
@Slot()
|
|
224
309
|
def _open_tutorial(self) -> None:
|
|
225
310
|
QDesktopServices.openUrl(QUrl(_tutorial_url))
|
|
311
|
+
|
|
312
|
+
@Slot(Qt.CheckState)
|
|
313
|
+
def _disable_panopto_check_changed(self, state: Qt.CheckState) -> None:
|
|
314
|
+
self._panopto_url_box.enabled = state == Qt.CheckState.Unchecked
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def _panopto_enabled(self) -> bool:
|
|
318
|
+
return self._disable_panopto_checkbox.checkState() == Qt.CheckState.Unchecked
|
qcanvas/util/__init__.py
CHANGED
qcanvas/util/auto_downloader.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
from
|
|
4
|
+
from PySide6.QtCore import Qt
|
|
5
|
+
from libqcanvas import db
|
|
6
|
+
from libqcanvas.net.resources.download.resource_manager import ResourceManager
|
|
7
|
+
from libqcanvas.net.sync.sync_receipt import SyncReceipt
|
|
8
|
+
from PySide6.QtWidgets import QMessageBox, QWidget
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
import qcanvas.settings as settings
|
|
11
11
|
|
|
12
12
|
_logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
@@ -21,7 +21,7 @@ async def download_new_resources(
|
|
|
21
21
|
) -> None:
|
|
22
22
|
resources_to_download = []
|
|
23
23
|
|
|
24
|
-
for file_id in receipt.
|
|
24
|
+
for file_id in receipt.updates[db.Resource]:
|
|
25
25
|
resource = all_resources[file_id]
|
|
26
26
|
|
|
27
27
|
if _should_auto_download_resource(resource, resource_manager=downloader):
|
|
@@ -37,7 +37,8 @@ async def download_new_resources(
|
|
|
37
37
|
msg.accepted.connect(
|
|
38
38
|
lambda: asyncio.get_running_loop().create_task(
|
|
39
39
|
downloader.batch_download(resources_to_download),
|
|
40
|
-
)
|
|
40
|
+
),
|
|
41
|
+
Qt.ConnectionType.SingleShotConnection,
|
|
41
42
|
)
|
|
42
43
|
else:
|
|
43
44
|
await downloader.batch_download(resources_to_download)
|
qcanvas/util/basic_fonts.py
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from PySide6.QtQml import QQmlContext
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ContextDict:
|
|
5
|
+
def __init__(self, context: QQmlContext):
|
|
6
|
+
self._context = context
|
|
7
|
+
|
|
8
|
+
def __getitem__(self, item):
|
|
9
|
+
return self._context.contextProperty(item)
|
|
10
|
+
|
|
11
|
+
def __setitem__(self, key, value):
|
|
12
|
+
self._context.setContextProperty(key, value)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os.path
|
|
3
|
+
|
|
4
|
+
import cachetools
|
|
5
|
+
from PySide6.QtCore import QFileInfo, QMimeDatabase
|
|
6
|
+
from PySide6.QtGui import QIcon
|
|
7
|
+
from PySide6.QtWidgets import QApplication, QFileIconProvider, QStyle
|
|
8
|
+
|
|
9
|
+
import qcanvas.util.runtime as runtime
|
|
10
|
+
|
|
11
|
+
_logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Windows and linux have different ways of doing this
|
|
14
|
+
if runtime.is_running_on_windows:
|
|
15
|
+
_icon_provider = QFileIconProvider()
|
|
16
|
+
|
|
17
|
+
def icon_for_filename(file_name: str) -> QIcon:
|
|
18
|
+
return _icon_provider.icon(QFileInfo(file_name))
|
|
19
|
+
|
|
20
|
+
else:
|
|
21
|
+
_mime_database = QMimeDatabase()
|
|
22
|
+
_icon_for_suffix: dict[str, QIcon] = {}
|
|
23
|
+
|
|
24
|
+
def icon_for_filename(file_name: str) -> QIcon:
|
|
25
|
+
file_suffix = os.path.splitext(file_name)[1]
|
|
26
|
+
|
|
27
|
+
# Check if we already know what icon this file type has
|
|
28
|
+
if file_suffix in _icon_for_suffix:
|
|
29
|
+
return _icon_for_suffix[file_suffix]
|
|
30
|
+
|
|
31
|
+
# Try to find an icon for this file type
|
|
32
|
+
for mime_type in _mime_database.mimeTypesForFileName(file_name):
|
|
33
|
+
icon = QIcon.fromTheme(mime_type.iconName())
|
|
34
|
+
|
|
35
|
+
if not icon.isNull():
|
|
36
|
+
_icon_for_suffix[file_suffix] = icon
|
|
37
|
+
return icon
|
|
38
|
+
|
|
39
|
+
# No icon for this type of file was found, use default icon
|
|
40
|
+
icon = _default_icon()
|
|
41
|
+
_icon_for_suffix[file_suffix] = icon
|
|
42
|
+
return icon
|
|
43
|
+
|
|
44
|
+
@cachetools.cached(cachetools.LRUCache(maxsize=1))
|
|
45
|
+
def _default_icon() -> QIcon:
|
|
46
|
+
return QApplication.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon)
|
qcanvas/util/html_cleaner.py
CHANGED
|
@@ -16,6 +16,8 @@ def clean_up_html(html: str) -> str:
|
|
|
16
16
|
_remove_tags(doc.find_all(["link", "script"]))
|
|
17
17
|
# Remove font awesome icons (which don't load anyway)
|
|
18
18
|
_remove_tags(doc.find_all(["span"], class_=["dp-icon-content"]))
|
|
19
|
+
# Remove screen reader elements
|
|
20
|
+
_remove_tags(doc.find_all(class_="screenreader-only"))
|
|
19
21
|
|
|
20
22
|
return str(doc)
|
|
21
23
|
|
qcanvas/util/layouts.py
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Iterable, NamedTuple
|
|
3
3
|
|
|
4
|
-
from
|
|
5
|
-
from
|
|
4
|
+
from PySide6.QtCore import Qt
|
|
5
|
+
from PySide6.QtWidgets import QGridLayout, QLayout, QWidget
|
|
6
6
|
|
|
7
7
|
_logger = logging.getLogger(__name__)
|
|
8
8
|
|
|
9
|
-
T = TypeVar("T")
|
|
10
|
-
|
|
11
9
|
|
|
12
10
|
class GridItem(NamedTuple):
|
|
13
11
|
widget: QWidget
|
|
@@ -16,17 +14,20 @@ class GridItem(NamedTuple):
|
|
|
16
14
|
alignment: Qt.AlignmentFlag = Qt.AlignmentFlag.AlignLeft
|
|
17
15
|
|
|
18
16
|
|
|
19
|
-
def layout_widget(layout_type:
|
|
17
|
+
def layout_widget[T](layout_type: type[T], *items: QWidget, **kwargs) -> QWidget:
|
|
20
18
|
widget = QWidget()
|
|
21
19
|
widget.setLayout(layout(layout_type, *items, **kwargs))
|
|
22
20
|
return widget
|
|
23
21
|
|
|
24
22
|
|
|
25
|
-
def layout(layout_type:
|
|
23
|
+
def layout[T](layout_type: type[T], *items: QWidget | QLayout, **kwargs) -> T:
|
|
26
24
|
result_layout: QLayout = layout_type(**kwargs)
|
|
27
25
|
|
|
28
26
|
for item in items:
|
|
29
|
-
|
|
27
|
+
if isinstance(item, QLayout):
|
|
28
|
+
result_layout.addItem(item)
|
|
29
|
+
else:
|
|
30
|
+
result_layout.addWidget(item)
|
|
30
31
|
|
|
31
32
|
return result_layout
|
|
32
33
|
|