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.
Files changed (97) hide show
  1. qcanvas/__init__.py +60 -0
  2. qcanvas/app.py +72 -0
  3. qcanvas/backend_connectors/frontend_resource_manager.py +13 -5
  4. qcanvas/backend_connectors/qcanvas_task_master.py +2 -2
  5. qcanvas/icons/__init__.py +55 -6
  6. qcanvas/icons/_icon_type.py +42 -0
  7. qcanvas/icons/icons.qrc +48 -8
  8. qcanvas/icons/rc_icons.py +2477 -566
  9. qcanvas/settings/__init__.py +6 -0
  10. qcanvas/{util/settings → settings}/_client_settings.py +15 -6
  11. qcanvas/settings/_course_settings.py +54 -0
  12. qcanvas/{util/settings → settings}/_mapped_setting.py +8 -6
  13. qcanvas/{util/settings → settings}/_ui_settings.py +5 -5
  14. qcanvas/theme.py +101 -0
  15. qcanvas/ui/course_viewer/content_tree.py +37 -19
  16. qcanvas/ui/course_viewer/course_tree/__init__.py +1 -0
  17. qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +86 -0
  18. qcanvas/ui/course_viewer/{course_tree.py → course_tree/course_tree.py} +29 -14
  19. qcanvas/ui/course_viewer/course_viewer.py +79 -46
  20. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +107 -29
  21. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +19 -18
  22. qcanvas/ui/course_viewer/tabs/content_tab.py +33 -39
  23. qcanvas/ui/course_viewer/tabs/file_tab/__init__.py +1 -0
  24. qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +46 -0
  25. qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +96 -0
  26. qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +55 -0
  27. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +50 -27
  28. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +18 -19
  29. qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +3 -3
  30. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +18 -16
  31. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +61 -74
  32. qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
  33. qcanvas/ui/memory_tree/_tree_memory.py +45 -41
  34. qcanvas/ui/memory_tree/memory_tree_widget.py +22 -18
  35. qcanvas/ui/memory_tree/memory_tree_widget_item.py +3 -3
  36. qcanvas/ui/qcanvas_window/__init__.py +1 -0
  37. qcanvas/ui/qcanvas_window/course_viewer_container.py +95 -0
  38. qcanvas/ui/{main_ui → qcanvas_window}/options/auto_download_resources_option.py +8 -6
  39. qcanvas/ui/{main_ui → qcanvas_window}/options/quick_sync_option.py +7 -6
  40. qcanvas/ui/{main_ui → qcanvas_window}/options/sync_on_start_option.py +7 -6
  41. qcanvas/ui/{main_ui → qcanvas_window}/options/theme_selection_menu.py +12 -10
  42. qcanvas/ui/{main_ui → qcanvas_window}/qcanvas_window.py +74 -45
  43. qcanvas/ui/{main_ui → qcanvas_window}/status_bar_progress_display.py +20 -12
  44. qcanvas/ui/qml_components/__init__.py +4 -0
  45. qcanvas/ui/qml_components/attachments_pane.py +70 -0
  46. qcanvas/ui/qml_components/comments_pane.py +83 -0
  47. qcanvas/ui/qml_components/qml/AttachmentsList.ui.qml +15 -0
  48. qcanvas/ui/qml_components/qml/AttachmentsListDelegate.qml +77 -0
  49. qcanvas/ui/qml_components/qml/AttachmentsListModel.qml +19 -0
  50. qcanvas/ui/qml_components/qml/AttachmentsPane.qml +11 -0
  51. qcanvas/ui/qml_components/qml/CommentsList.ui.qml +15 -0
  52. qcanvas/ui/qml_components/qml/CommentsListDelegate.ui.qml +118 -0
  53. qcanvas/ui/qml_components/qml/CommentsListModel.qml +56 -0
  54. qcanvas/ui/qml_components/qml/CommentsPane.qml +11 -0
  55. qcanvas/ui/qml_components/qml/DecoratedText.ui.qml +44 -0
  56. qcanvas/ui/qml_components/qml/Spacer.ui.qml +7 -0
  57. qcanvas/ui/qml_components/qml/ThemedRectangle.qml +53 -0
  58. qcanvas/ui/qml_components/qml/__init__.py +3 -0
  59. qcanvas/ui/qml_components/qml/rc_qml.py +709 -0
  60. qcanvas/ui/qml_components/qml/rc_qml.qrc +16 -0
  61. qcanvas/ui/qml_components/qml_bridge_types.py +95 -0
  62. qcanvas/ui/qml_components/qml_pane.py +21 -0
  63. qcanvas/ui/setup/setup_checker.py +3 -3
  64. qcanvas/ui/setup/setup_dialog.py +173 -80
  65. qcanvas/util/__init__.py +0 -2
  66. qcanvas/util/auto_downloader.py +9 -8
  67. qcanvas/util/basic_fonts.py +2 -2
  68. qcanvas/util/context_dict.py +12 -0
  69. qcanvas/util/file_icons.py +46 -0
  70. qcanvas/util/html_cleaner.py +2 -0
  71. qcanvas/util/layouts.py +9 -8
  72. qcanvas/util/paths.py +26 -22
  73. qcanvas/util/qurl_util.py +1 -1
  74. qcanvas/util/runtime.py +20 -0
  75. qcanvas/util/ui_tools.py +121 -7
  76. qcanvas/util/url_checker.py +1 -1
  77. qcanvas-2026.1.19.dist-info/METADATA +95 -0
  78. qcanvas-2026.1.19.dist-info/RECORD +92 -0
  79. {qcanvas-1.0.11.dist-info → qcanvas-2026.1.19.dist-info}/WHEEL +1 -1
  80. qcanvas-2026.1.19.dist-info/entry_points.txt +3 -0
  81. qcanvas/app_start/__init__.py +0 -54
  82. qcanvas/icons/file-download-failed.svg +0 -6
  83. qcanvas/icons/file-downloaded.svg +0 -6
  84. qcanvas/icons/file-not-downloaded.svg +0 -6
  85. qcanvas/icons/file-unknown.svg +0 -6
  86. qcanvas/icons/main_icon.svg +0 -325
  87. qcanvas/icons/sync.svg +0 -7
  88. qcanvas/run.py +0 -30
  89. qcanvas/ui/main_ui/__init__.py +0 -0
  90. qcanvas/ui/main_ui/course_viewer_container.py +0 -52
  91. qcanvas/util/settings/__init__.py +0 -9
  92. qcanvas/util/themes.py +0 -24
  93. qcanvas-1.0.11.dist-info/METADATA +0 -61
  94. qcanvas-1.0.11.dist-info/RECORD +0 -68
  95. qcanvas-1.0.11.dist-info/entry_points.txt +0 -3
  96. /qcanvas/ui/course_viewer/tabs/{util.py → constants.py} +0 -0
  97. /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.util.settings as settings
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
@@ -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
- from qcanvas_api_clients.canvas import CanvasClient, CanvasClientConfig
6
- from qcanvas_api_clients.panopto import PanoptoClient, PanoptoClientConfig
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 is_url
15
- from qcanvas.util.layouts import grid_layout_widget, layout
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 SetupDialog(QDialog):
25
- closed = Signal()
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
- self._canvas_url_box = QLineEdit(settings.client.canvas_url)
37
- self._canvas_url_box.setPlaceholderText("https://instance.canvas.com")
38
- self._canvas_api_key_box = QLineEdit(settings.client.canvas_api_key)
39
- self._canvas_api_key_box.setEchoMode(QLineEdit.EchoMode.Password)
40
- self._panopto_url_box = QLineEdit(settings.client.panopto_url)
41
- self._panopto_url_box.setPlaceholderText("https://instance.panopto.com")
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
- [QLabel("Canvas URL"), self._canvas_url_box],
54
- [QLabel("Canvas API Key"), self._canvas_api_key_box],
55
- [QLabel("Panopto URL"), self._panopto_url_box],
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 key", QDialogButtonBox.ButtonRole.HelpRole)
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 _accepted(self) -> None:
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._all_inputs_valid():
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().strip(),
98
- canvas_url=self._get_url(self._canvas_url_box),
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 not await self._check_panopto_config(canvas_config):
105
- self._show_panopto_help()
106
- return
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
- for line_edit in [
121
- self._canvas_url_box,
122
- self._panopto_url_box,
123
- self._canvas_api_key_box,
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 is_url(self._get_url(self._canvas_url_box)):
222
+ if not self._canvas_url_box.is_valid_url:
133
223
  all_valid = False
134
- self._show_error(self._canvas_url_box, "Canvas URL is invalid")
135
- if len(self._canvas_api_key_box.text().strip()) == 0:
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._show_error(self._canvas_api_key_box, "Canvas API key is empty")
138
- if not is_url(self._get_url(self._panopto_url_box)):
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._show_error(self._panopto_url_box, "Panopto URL is invalid")
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._show_error(self._canvas_api_key_box, "Canvas API key is invalid")
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._get_url(self._panopto_url_box)),
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(self._open_panopto_login)
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._get_url(self._panopto_url_box))
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._get_url(self._canvas_url_box)
204
- settings.client.panopto_url = self._get_url(self._panopto_url_box)
205
- settings.client.canvas_api_key = self._canvas_api_key_box.text().strip()
206
- self.closed.emit()
207
- self.close()
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(self._open_tutorial)
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
@@ -1,2 +0,0 @@
1
- # todo remove this
2
- from .url_checker import is_url
@@ -1,13 +1,13 @@
1
1
  import asyncio
2
2
  import logging
3
3
 
4
- import qcanvas_backend.database.types as db
5
- from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
6
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
- from qtpy.QtWidgets import QMessageBox
8
- from qtpy.QtWidgets import QWidget
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
- from qcanvas.util import settings
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.updated_resources:
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)
@@ -1,5 +1,5 @@
1
- from qtpy.QtGui import QFont
2
- from qtpy.QtWidgets import QLabel
1
+ from PySide6.QtGui import QFont
2
+ from PySide6.QtWidgets import QLabel
3
3
 
4
4
  normal_font = QFont()
5
5
  bold_font = QFont()
@@ -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)
@@ -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 qtpy.QtCore import Qt
5
- from qtpy.QtWidgets import *
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: Type[T], *items: QWidget, **kwargs) -> QWidget:
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: Type[T], *items: QWidget, **kwargs) -> T:
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
- result_layout.addWidget(item)
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