qcanvas 0.0.3.4a0__py3-none-any.whl → 0.0.4.1a0__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 (43) hide show
  1. qcanvas/QtVersionHelper/QtGui/__init__.py +1 -1
  2. qcanvas/QtVersionHelper/__init__.py +2 -1
  3. qcanvas/__main__.py +5 -9
  4. qcanvas/db/__init__.py +2 -1
  5. qcanvas/db/database.py +11 -4
  6. qcanvas/icons/__init__.py +1 -1
  7. qcanvas/icons/icons.qrc +5 -4
  8. qcanvas/icons/main_icon.svg +217 -204
  9. qcanvas/icons/rc_icons.py +3 -0
  10. qcanvas/net/canvas/__init__.py +1 -1
  11. qcanvas/net/canvas/canvas_client.py +4 -2
  12. qcanvas/net/canvas/legacy_canvas_types.py +2 -2
  13. qcanvas/net/custom_httpx_async_transport.py +2 -3
  14. qcanvas/net/self_authenticating.py +1 -0
  15. qcanvas/queries/all_courses.gql +3 -3
  16. qcanvas/queries/all_courses.py +2 -2
  17. qcanvas/queries/canvas_course_data.py +2 -2
  18. qcanvas/ui/container_item.py +0 -6
  19. qcanvas/ui/main_ui.py +34 -18
  20. qcanvas/ui/menu_bar/__init__.py +0 -0
  21. qcanvas/ui/menu_bar/grouping_preferences_menu.py +61 -0
  22. qcanvas/ui/menu_bar/theme_selection_menu.py +36 -0
  23. qcanvas/ui/setup_dialog.py +3 -3
  24. qcanvas/ui/viewer/course_list.py +1 -3
  25. qcanvas/ui/viewer/file_list.py +1 -3
  26. qcanvas/ui/viewer/file_view_tab.py +3 -64
  27. qcanvas/util/__init__.py +1 -1
  28. qcanvas/util/app_settings.py +28 -2
  29. qcanvas/util/constants.py +1 -1
  30. qcanvas/util/course_indexer/__init__.py +1 -1
  31. qcanvas/util/course_indexer/data_manager.py +8 -4
  32. qcanvas/util/course_indexer/resource_helpers.py +2 -1
  33. qcanvas/util/file_icon_helper.py +2 -1
  34. qcanvas/util/linkscanner/canvas_media_object_scanner.py +2 -2
  35. qcanvas/util/linkscanner/resource_scanner.py +1 -1
  36. qcanvas/util/self_updater.py +6 -6
  37. qcanvas/util/task_pool.py +1 -1
  38. qcanvas/util/tree_util/model_helpers.py +1 -1
  39. qcanvas/util/tree_util/tree_model.py +1 -0
  40. {qcanvas-0.0.3.4a0.dist-info → qcanvas-0.0.4.1a0.dist-info}/METADATA +1 -1
  41. qcanvas-0.0.4.1a0.dist-info/RECORD +60 -0
  42. qcanvas-0.0.3.4a0.dist-info/RECORD +0 -57
  43. {qcanvas-0.0.3.4a0.dist-info → qcanvas-0.0.4.1a0.dist-info}/WHEEL +0 -0
qcanvas/icons/rc_icons.py CHANGED
@@ -230,10 +230,13 @@ qt_resource_struct = b"\
230
230
  \x00\x00\x01\x8d\xcbE['\
231
231
  "
232
232
 
233
+
233
234
  def qInitResources():
234
235
  QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
235
236
 
237
+
236
238
  def qCleanupResources():
237
239
  QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
238
240
 
241
+
239
242
  qInitResources()
@@ -1,2 +1,2 @@
1
1
  from .canvas_client import CanvasClient
2
- from .legacy_canvas_types import LegacyFile, LegacyPage
2
+ from .legacy_canvas_types import LegacyFile, LegacyPage
@@ -99,7 +99,8 @@ class CanvasClient(SelfAuthenticating):
99
99
  async def get_page(self, page_id: str | int, course_id: str | int) -> LegacyPage:
100
100
  async with self._net_op_sem:
101
101
  response = detect_ratelimit_and_raise(
102
- await self.client.get(self.canvas_url.join(f"api/v1/courses/{course_id}/pages/{page_id}"), **self.get_headers()))
102
+ await self.client.get(self.canvas_url.join(f"api/v1/courses/{course_id}/pages/{page_id}"),
103
+ **self.get_headers()))
103
104
 
104
105
  return LegacyPage.from_dict(json.loads(response.text))
105
106
 
@@ -155,7 +156,8 @@ class CanvasClient(SelfAuthenticating):
155
156
  retries = 0
156
157
 
157
158
  while retries < self.max_retries:
158
- async with self.client.stream(method='GET', url=resource.url, cookies=self.client.cookies, follow_redirects=True) as resp:
159
+ async with self.client.stream(method='GET', url=resource.url, cookies=self.client.cookies,
160
+ follow_redirects=True) as resp:
159
161
  if await self.reauthenticate_if_needed(resp):
160
162
  retries += 1
161
163
  self._logger.warning("Retrying download of %s", resource.url)
@@ -1,6 +1,5 @@
1
1
  from typing import Any, TypeVar, Type, cast
2
2
 
3
-
4
3
  T = TypeVar("T")
5
4
 
6
5
 
@@ -66,7 +65,8 @@ class LegacyFile:
66
65
  hidden_for_user: bool
67
66
  locked_for_user: bool
68
67
 
69
- def __init__(self, id: int, uuid: str, display_name: str, filename: str, url: str, size: int, locked: bool, hidden: bool, hidden_for_user: bool, locked_for_user: bool) -> None:
68
+ def __init__(self, id: int, uuid: str, display_name: str, filename: str, url: str, size: int, locked: bool,
69
+ hidden: bool, hidden_for_user: bool, locked_for_user: bool) -> None:
70
70
  self.id = id
71
71
  self.uuid = uuid
72
72
  self.display_name = display_name
@@ -12,7 +12,8 @@ class CustomHTTPXAsyncTransport(HTTPXAsyncTransport):
12
12
  The transport uses the httpx library with anyio.
13
13
  """
14
14
 
15
- def __init__(self, client: httpx.AsyncClient, url: Union[str, httpx.URL], headers: dict[str, Any] | None = None, **kwargs):
15
+ def __init__(self, client: httpx.AsyncClient, url: Union[str, httpx.URL], headers: dict[str, Any] | None = None,
16
+ **kwargs):
16
17
  super().__init__(url=url, **kwargs)
17
18
  self.client = client
18
19
  self.headers = headers or {}
@@ -31,5 +32,3 @@ class CustomHTTPXAsyncTransport(HTTPXAsyncTransport):
31
32
  result["headers"] = self.headers
32
33
 
33
34
  return result
34
-
35
-
@@ -5,6 +5,7 @@ from asyncio import Lock, Event
5
5
  class AuthenticationException(Exception):
6
6
  pass
7
7
 
8
+
8
9
  # httpx does have an authentication flow mechanism that allows you to also make other requests but I don't know if it
9
10
  # will behave the same way as this does. I also finished this before I found out that existed.
10
11
 
@@ -1,7 +1,7 @@
1
1
  # qenerate: plugin=pydantic_v1
2
2
 
3
3
  query AllCourses($detailed: Boolean!) {
4
- allCourses {
5
- ...CanvasCourseData
6
- }
4
+ allCourses {
5
+ ...CanvasCourseData
6
+ }
7
7
  }
@@ -81,8 +81,8 @@ query AllCourses($detailed: Boolean!) {
81
81
 
82
82
  class ConfiguredBaseModel(BaseModel):
83
83
  class Config:
84
- smart_union=True
85
- extra=Extra.forbid
84
+ smart_union = True
85
+ extra = Extra.forbid
86
86
 
87
87
 
88
88
  class AllCoursesQueryData(ConfiguredBaseModel):
@@ -72,8 +72,8 @@ fragment CanvasCourseData on Course {
72
72
 
73
73
  class ConfiguredBaseModel(BaseModel):
74
74
  class Config:
75
- smart_union=True
76
- extra=Extra.forbid
75
+ smart_union = True
76
+ extra = Extra.forbid
77
77
 
78
78
 
79
79
  class Term(ConfiguredBaseModel):
@@ -10,12 +10,6 @@ class ContainerItem(QStandardItem):
10
10
  self.content = data
11
11
  self.setEditable(False)
12
12
 
13
-
14
13
  def data(self, role=257):
15
14
  if role == Qt.ItemDataRole.DisplayRole:
16
15
  return self.content.text
17
-
18
-
19
-
20
-
21
-
qcanvas/ui/main_ui.py CHANGED
@@ -8,8 +8,10 @@ from qasync import asyncSlot
8
8
 
9
9
  import qcanvas.db.database as db
10
10
  from qcanvas.QtVersionHelper.QtCore import Slot, Signal, Qt, QUrl
11
- from qcanvas.QtVersionHelper.QtGui import QDesktopServices
11
+ from qcanvas.QtVersionHelper.QtGui import QDesktopServices, QAction
12
12
  from qcanvas.QtVersionHelper.QtWidgets import *
13
+ from qcanvas.ui.menu_bar.grouping_preferences_menu import GroupingPreferencesMenu
14
+ from qcanvas.ui.menu_bar.theme_selection_menu import ThemeSelectionMenu
13
15
  from qcanvas.ui.viewer.course_list import CourseList
14
16
  from qcanvas.ui.viewer.file_list import FileRow
15
17
  from qcanvas.ui.viewer.file_view_tab import FileViewTab
@@ -20,14 +22,19 @@ from qcanvas.util.course_indexer import DataManager
20
22
 
21
23
  _aux_settings = AppSettings.auxiliary
22
24
 
25
+
23
26
  class AppMainWindow(QMainWindow):
24
27
  logger = logging.getLogger()
25
28
  loaded = Signal()
29
+ files_grouping_preference_changed = Signal(db.GroupByPreference)
26
30
  operation_lock = Event()
27
31
 
28
32
  def __init__(self, data_manager: DataManager, parent: QWidget | None = None):
29
33
  super().__init__(parent)
30
34
 
35
+ self.group_by_pages_action: QAction | None = None
36
+ self.group_by_modules_action: QAction | None = None
37
+ self.file_grouping_menu: QMenu | None = None
31
38
  self.selected_course: db.Course | None = None
32
39
  self.courses: Sequence[db.Course] = []
33
40
  self.resources: dict[str, db.Resource] = {}
@@ -50,8 +57,6 @@ class AppMainWindow(QMainWindow):
50
57
  self.pages_viewer.viewer.anchorClicked.connect(self.viewer_link_clicked)
51
58
 
52
59
  self.file_viewer = FileViewTab(data_manager.download_pool)
53
- self.file_viewer.group_by_preference_changed.connect(self.course_file_group_by_preference_changed)
54
-
55
60
  self.file_viewer.files_column.tree.itemActivated.connect(self.download_file_from_file_pane)
56
61
  self.file_viewer.assignment_files_column.tree.itemActivated.connect(self.download_file_from_file_pane)
57
62
 
@@ -73,17 +78,36 @@ class AppMainWindow(QMainWindow):
73
78
  widget.setLayout(v_layout)
74
79
  self.setCentralWidget(widget)
75
80
 
81
+ self.setup_menu_bar()
82
+
83
+ self.files_grouping_preference_changed.connect(self.on_grouping_preference_changed)
84
+
76
85
  self.loaded.connect(self.load_course_list)
77
86
  self.loaded.connect(self.check_for_update)
78
87
  self.loaded.emit()
79
88
 
80
- self.read_settings()
89
+ self.restore_window_position()
90
+
91
+ def setup_menu_bar(self):
92
+ menu_bar = self.menuBar()
93
+
94
+ menu_bar.addMenu(ThemeSelectionMenu())
95
+ view_menu = menu_bar.addMenu("View")
96
+
97
+ view_menu.addMenu(self.setup_group_by_menu())
98
+
99
+ def setup_group_by_menu(self) -> QMenu:
100
+ file_grouping_menu = GroupingPreferencesMenu(self.data_manager)
101
+ self.course_list.course_selected.connect(file_grouping_menu.course_changed)
102
+ file_grouping_menu.preference_changed.connect(self.on_grouping_preference_changed)
103
+
104
+ return file_grouping_menu
81
105
 
82
106
  def closeEvent(self, event):
83
107
  _aux_settings.setValue("geometry", self.saveGeometry())
84
108
  _aux_settings.setValue("windowState", self.saveState())
85
109
 
86
- def read_settings(self):
110
+ def restore_window_position(self):
87
111
  self.restoreGeometry(_aux_settings.value("geometry"))
88
112
  self.restoreState(_aux_settings.value("windowState"))
89
113
 
@@ -97,14 +121,12 @@ class AppMainWindow(QMainWindow):
97
121
  await self.data_manager.download_resource(resource)
98
122
  QDesktopServices.openUrl(QUrl.fromLocalFile(resource.download_location.absolute()))
99
123
 
100
-
101
124
  @asyncSlot(QTreeWidgetItem, int)
102
- async def download_file_from_file_pane(self, item: QTreeWidgetItem, _ : int):
125
+ async def download_file_from_file_pane(self, item: QTreeWidgetItem, _: int):
103
126
  if isinstance(item, FileRow):
104
127
  await self.data_manager.download_resource(item.resource)
105
128
  QDesktopServices.openUrl(QUrl.fromLocalFile(item.resource.download_location.absolute()))
106
129
 
107
-
108
130
  @asyncSlot()
109
131
  async def sync_data(self):
110
132
  # # self.operation_lock.
@@ -118,8 +140,6 @@ class AppMainWindow(QMainWindow):
118
140
  self.sync_button.setEnabled(True)
119
141
  self.sync_button.setText("Synchronize")
120
142
 
121
-
122
-
123
143
  @asyncSlot()
124
144
  async def load_course_list(self):
125
145
  self.courses = (await self.data_manager.get_data())
@@ -134,13 +154,13 @@ class AppMainWindow(QMainWindow):
134
154
  @asyncSlot()
135
155
  async def check_for_update(self):
136
156
  try:
137
- newer_version = await self_updater.get_newer_version()
157
+ newer_version, installed_version = await self_updater.get_newer_version()
138
158
 
139
159
  if newer_version is not None and newer_version != AppSettings.last_ignored_update:
140
160
  msg_box = QMessageBox(
141
161
  QMessageBox.Icon.Question,
142
162
  "Update available",
143
- "There is an update available, do you want to update?\nThe program will close after the update is finished.",
163
+ f"There is an update available ({installed_version} -> {newer_version})\n do you want to update?\nThe program will close after the update is finished.",
144
164
  QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
145
165
  self
146
166
  )
@@ -180,6 +200,7 @@ class AppMainWindow(QMainWindow):
180
200
  def on_course_selected(self, course: Optional[db.Course]):
181
201
  if course is not None:
182
202
  self.selected_course = course
203
+ # todo these should really be slots connected to this signal...
183
204
  self.pages_viewer.fill_tree(course)
184
205
  self.assignment_viewer.fill_tree(course)
185
206
  self.file_viewer.load_course_files(course)
@@ -187,11 +208,6 @@ class AppMainWindow(QMainWindow):
187
208
  self.selected_course = None
188
209
  self.file_viewer.clear()
189
210
 
190
-
191
211
  @asyncSlot(db.CoursePreferences)
192
- async def course_file_group_by_preference_changed(self, preference: db.GroupByPreference):
193
- self.selected_course.preferences.files_group_by_preference = preference
194
- await self.data_manager.update_item(self.selected_course.preferences)
212
+ async def on_grouping_preference_changed(self):
195
213
  self.file_viewer.load_course_files(self.selected_course)
196
-
197
-
File without changes
@@ -0,0 +1,61 @@
1
+ from qasync import asyncSlot
2
+
3
+ import qcanvas.db as db
4
+ from qcanvas.QtVersionHelper.QtCore import Slot, Signal
5
+ from qcanvas.QtVersionHelper.QtGui import create_qaction
6
+ from qcanvas.QtVersionHelper.QtWidgets import QMenu, QWidget
7
+ from qcanvas.util.course_indexer import DataManager
8
+
9
+
10
+ class GroupingPreferencesMenu(QMenu):
11
+ _preference_changed_private = Signal(db.GroupByPreference)
12
+ preference_changed = Signal()
13
+
14
+ def __init__(self, data_manager: DataManager, parent: QWidget | None = None):
15
+ super().__init__("Group files by", parent)
16
+ self.setEnabled(False)
17
+
18
+ self.data_manager = data_manager
19
+ self._selected_course: db.Course | None = None
20
+
21
+ self.group_by_modules_action = self._make_action("Modules", db.GroupByPreference.GROUP_BY_MODULES)
22
+ self.group_by_pages_action = self._make_action("Pages", db.GroupByPreference.GROUP_BY_PAGES)
23
+
24
+ self.addActions([self.group_by_pages_action, self.group_by_modules_action])
25
+
26
+ self._preference_changed_private.connect(self._on_preference_changed)
27
+
28
+ def _make_action(self, text: str, preference_value: db.GroupByPreference):
29
+ return create_qaction(
30
+ name=text,
31
+ parent=self,
32
+ triggered=lambda: self._preference_changed_private.emit(preference_value),
33
+ checkable=True,
34
+ checked=False
35
+ )
36
+
37
+ @Slot()
38
+ def course_changed(self, course: db.Course | None):
39
+ if course is not None:
40
+ self._selected_course = course
41
+ self._update_actions()
42
+ self.setEnabled(True)
43
+ else:
44
+ self._selected_course = None
45
+ self.setEnabled(False)
46
+
47
+ @asyncSlot()
48
+ async def _on_preference_changed(self, preference: db.GroupByPreference):
49
+ if self._selected_course is not None:
50
+ self._selected_course.preferences.files_group_by_preference = preference
51
+ self._update_actions()
52
+ # todo not sure if this should go in main_ui or here...
53
+ await self.data_manager.update_item(self._selected_course.preferences)
54
+
55
+ self.preference_changed.emit()
56
+
57
+ def _update_actions(self):
58
+ preference = self._selected_course.preferences.files_group_by_preference
59
+
60
+ self.group_by_pages_action.setChecked(preference == db.GroupByPreference.GROUP_BY_PAGES)
61
+ self.group_by_modules_action.setChecked(preference == db.GroupByPreference.GROUP_BY_MODULES)
@@ -0,0 +1,36 @@
1
+ from qcanvas.QtVersionHelper.QtGui import QActionGroup
2
+ from qcanvas.QtVersionHelper.QtGui import create_qaction
3
+ from qcanvas.QtVersionHelper.QtWidgets import QMenu, QWidget
4
+ from qcanvas.util import AppSettings
5
+
6
+
7
+ def change_theme(theme_name: str):
8
+ AppSettings.theme = theme_name
9
+ AppSettings.apply_selected_theme()
10
+
11
+
12
+ class ThemeSelectionMenu(QMenu):
13
+ def __init__(self, parent: QWidget | None = None):
14
+ super().__init__("Theme", parent)
15
+
16
+ action_group = QActionGroup(self)
17
+
18
+ light_theme = self._create_action("Light", "light")
19
+ dark_theme = self._create_action("Dark", "dark")
20
+ native_theme = self._create_action("Native (requires restart)", "native")
21
+
22
+ actions = [light_theme, dark_theme, native_theme]
23
+
24
+ self.addActions(actions)
25
+
26
+ for theme in actions:
27
+ action_group.addAction(theme)
28
+
29
+ def _create_action(self, text: str, theme_name: str):
30
+ return create_qaction(
31
+ name=text,
32
+ parent=self,
33
+ triggered=lambda: change_theme(theme_name),
34
+ checkable=True,
35
+ checked=AppSettings.theme == theme_name
36
+ )
@@ -14,6 +14,7 @@ from qcanvas.util import AppSettings
14
14
 
15
15
  tutorial_url = "https://www.iorad.com/player/2053777/Canvas---How-to-generate-an-access-token-"
16
16
 
17
+
17
18
  def row(name: str) -> QWidget:
18
19
  widget = QWidget()
19
20
  layout = QHBoxLayout()
@@ -78,7 +79,7 @@ class SetupDialog(QDialog):
78
79
  "Error",
79
80
  f"{name} is invalid",
80
81
  parent=self
81
- )
82
+ )
82
83
  msg.show()
83
84
 
84
85
  def ensure_protocol(url: str):
@@ -104,7 +105,7 @@ class SetupDialog(QDialog):
104
105
  "Error",
105
106
  f"The canvas URL or API key is invalid.\nPlease check you entered them correctly.",
106
107
  parent=self
107
- )
108
+ )
108
109
  msg.show()
109
110
  else:
110
111
  AppSettings.canvas_url = canvas_url_text
@@ -144,7 +145,6 @@ Don't share this key. You can revoke it at any time.""",
144
145
  msg.accepted.connect(lambda: QDesktopServices.openUrl(tutorial_url))
145
146
  msg.show()
146
147
 
147
-
148
148
  def _row(self, name: str, widget: QWidget):
149
149
  self.grid.addWidget(QLabel(name), self._row_counter, 0)
150
150
  self.grid.addWidget(widget, self._row_counter, 1)
@@ -18,7 +18,7 @@ class CourseNode(QStandardItem, QObject):
18
18
  QStandardItem.__init__(self, course.preferences.local_name or course.name)
19
19
  self.course = course
20
20
 
21
- def setData(self, value, role = ...):
21
+ def setData(self, value, role=...):
22
22
  if isinstance(value, str):
23
23
  value = value.strip()
24
24
 
@@ -93,5 +93,3 @@ class CourseList(QTreeView):
93
93
 
94
94
  if isinstance(item, CourseNode):
95
95
  self.course_selected.emit(item.course)
96
-
97
-
@@ -130,7 +130,6 @@ class FileList(QTreeWidget):
130
130
  # Update the download column
131
131
  self.model().dataChanged.emit(index, index, Qt.ItemDataRole.DisplayRole)
132
132
 
133
-
134
133
  def _setup_header(self):
135
134
  self.setColumnCount(4)
136
135
  self.setHeaderLabels(["Name", "Date Found", "Size", "Download"])
@@ -167,7 +166,7 @@ class FileList(QTreeWidget):
167
166
  # Create the group node for this item
168
167
  group_node = QTreeWidgetItem([item.name])
169
168
 
170
- #fixme this does not remove duplicate files e.g. when on module groups
169
+ # fixme this does not remove duplicate files e.g. when on module groups
171
170
 
172
171
  for resource in resources:
173
172
  row = FileRow(resource)
@@ -193,4 +192,3 @@ class FileList(QTreeWidget):
193
192
  super().clear()
194
193
  self._setup_header()
195
194
  self.row_id_map.clear()
196
-
@@ -1,8 +1,6 @@
1
1
  from typing import Sequence
2
2
 
3
3
  import qcanvas.db as db
4
- from qcanvas.QtVersionHelper.QtCore import Signal, Qt, Slot, QPoint
5
- from qcanvas.QtVersionHelper.QtGui import QActionGroup, create_qaction
6
4
  from qcanvas.QtVersionHelper.QtWidgets import * # QWidget, QTreeView, QGroupBox, QBoxLayout, QHeaderView, QHBoxLayout, QComboBox, QLabel
7
5
  from qcanvas.ui.viewer.file_list import FileList
8
6
  from qcanvas.util.constants import default_assignments_module_names
@@ -25,7 +23,6 @@ class FileColumn(QGroupBox):
25
23
 
26
24
 
27
25
  class FileViewTab(QWidget):
28
- group_by_preference_changed = Signal(db.GroupByPreference)
29
26
 
30
27
  def __init__(self, download_pool: DownloadPool):
31
28
  super().__init__()
@@ -33,37 +30,13 @@ class FileViewTab(QWidget):
33
30
  self.files_column = FileColumn("Files", download_pool)
34
31
  self.assignment_files_column = FileColumn("Assignment files", download_pool)
35
32
 
36
- self.group_by_combobox = QComboBox()
37
-
38
- self.group_by_preference: db.GroupByPreference | None = None
39
- self.group_preference_layout = QHBoxLayout()
40
- self.group_preference_layout.addWidget(QLabel("Group By:"))
41
- self.group_preference_layout.addWidget(self.group_by_combobox)
42
- self.group_preference_layout.setStretch(1, 1)
43
-
44
- widget = QWidget()
45
- widget.setLayout(self.group_preference_layout)
46
-
47
- # fixme this can't stay here
48
- self.files_column.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
49
- self.files_column.customContextMenuRequested.connect(self.files_column_context_menu)
50
-
51
- self.group_by_preference_changed.connect(self.update_group_by_preferences)
52
-
53
33
  layout = QHBoxLayout()
54
34
  layout.addWidget(self.files_column)
55
35
  layout.addWidget(self.assignment_files_column)
56
36
 
57
37
  self.setLayout(layout)
58
38
 
59
- @Slot(db.GroupByPreference)
60
- def update_group_by_preferences(self, preference : db.GroupByPreference):
61
- if self.group_by_preference is not None:
62
- self.group_by_preference = preference
63
-
64
39
  def load_course_files(self, course: db.Course):
65
- self.group_by_preference = course.preferences.files_group_by_preference
66
-
67
40
  module_items: list[db.ModuleItem] = []
68
41
  assignment_items: list[db.ModuleItem] = []
69
42
 
@@ -73,8 +46,9 @@ class FileViewTab(QWidget):
73
46
  else:
74
47
  module_items.append(module_item)
75
48
 
76
- if self.group_by_preference == db.GroupByPreference.GROUP_BY_MODULES:
77
- exclude_assignments_module = list(filter(lambda x: x.name.lower() not in default_assignments_module_names, course.modules))
49
+ if course.preferences.files_group_by_preference == db.GroupByPreference.GROUP_BY_MODULES:
50
+ exclude_assignments_module = list(
51
+ filter(lambda x: x.name.lower() not in default_assignments_module_names, course.modules))
78
52
 
79
53
  self.files_column.load_items(exclude_assignments_module)
80
54
  else:
@@ -83,40 +57,5 @@ class FileViewTab(QWidget):
83
57
  self.assignment_files_column.load_items(assignment_items + course.assignments)
84
58
 
85
59
  def clear(self):
86
- self.group_by_preference = None
87
60
  self.files_column.clear()
88
61
  self.assignment_files_column.clear()
89
-
90
- @Slot(QPoint)
91
- def files_column_context_menu(self, pos: QPoint):
92
- if self.group_by_preference is None:
93
- return
94
-
95
- menu = QMenu(self.files_column)
96
-
97
- group_by_menu = menu.addMenu("Group by")
98
-
99
- select_group_preference_modules = create_qaction(
100
- name="Modules",
101
- checkable=True,
102
- checked=self.group_by_preference == db.GroupByPreference.GROUP_BY_MODULES,
103
- triggered=lambda: self.group_by_preference_changed.emit(db.GroupByPreference.GROUP_BY_MODULES)
104
- )
105
-
106
- select_group_preference_pages = create_qaction(
107
- name="Pages",
108
- checkable=True,
109
- checked=self.group_by_preference == db.GroupByPreference.GROUP_BY_PAGES,
110
- triggered=lambda: self.group_by_preference_changed.emit(db.GroupByPreference.GROUP_BY_PAGES)
111
- )
112
-
113
- action_group = QActionGroup(menu)
114
- action_group.addAction(select_group_preference_modules)
115
- action_group.addAction(select_group_preference_pages)
116
-
117
- group_by_menu.addAction(select_group_preference_pages)
118
- group_by_menu.addAction(select_group_preference_modules)
119
-
120
- menu.exec(self.files_column.mapToGlobal(pos))
121
-
122
-
qcanvas/util/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from .app_settings import _AppSettings
2
2
 
3
- AppSettings = _AppSettings()
3
+ AppSettings = _AppSettings()
@@ -1,6 +1,15 @@
1
+ import qdarktheme
2
+
1
3
  from qcanvas.QtVersionHelper.QtCore import QSettings, QUrl
2
4
 
3
5
 
6
+ def ensure_theme_is_valid(theme: str) -> str:
7
+ if theme not in ["auto", "light", "dark", "native"]:
8
+ return "light"
9
+ else:
10
+ return theme
11
+
12
+
4
13
  class _AppSettings:
5
14
  def __init__(self):
6
15
  self.settings = QSettings("QCanvas", "client")
@@ -8,6 +17,7 @@ class _AppSettings:
8
17
  self._canvas_url = self.settings.value("canvas_url", None)
9
18
  self._api_key = self.settings.value("api_key", defaultValue=None)
10
19
  self._ignored_update = self.settings.value("ignored_update", defaultValue=None)
20
+ self._theme = ensure_theme_is_valid(self.settings.value("theme", defaultValue="light"))
11
21
 
12
22
  @property
13
23
  def canvas_url(self) -> str | None:
@@ -37,7 +47,23 @@ class _AppSettings:
37
47
  self.settings.setValue("ignored_update", value)
38
48
 
39
49
  @property
40
- def is_set(self):
41
- return self.canvas_url is not None and QUrl(self.canvas_url).isValid() and self.canvas_url is not None
50
+ def theme(self) -> str | None:
51
+ return self._theme
42
52
 
53
+ @theme.setter
54
+ def theme(self, value: str):
55
+ value = ensure_theme_is_valid(value)
56
+ self._theme = value
57
+ self.settings.setValue("theme", value)
43
58
 
59
+ # fixme should this really be here
60
+ def apply_selected_theme(self):
61
+ if self.theme != "native":
62
+ qdarktheme.setup_theme(
63
+ self.theme,
64
+ custom_colors={"primary": "FF804F"}
65
+ )
66
+
67
+ @property
68
+ def is_set(self):
69
+ return self.canvas_url is not None and QUrl(self.canvas_url).isValid() and self.canvas_url is not None
qcanvas/util/constants.py CHANGED
@@ -1,3 +1,3 @@
1
1
  default_assignments_module_names = ["assessments", "assessment"]
2
2
  app_name = "QCanvas"
3
- package_name = "qcanvas"
3
+ package_name = "qcanvas"
@@ -1 +1 @@
1
- from .data_manager import DataManager
1
+ from .data_manager import DataManager
@@ -33,7 +33,8 @@ class TransientModulePage:
33
33
  position: int
34
34
 
35
35
 
36
- def _prepare_out_of_date_pages_for_loading(g_courses: Sequence[queries.Course], pages: Sequence[db.ModuleItem]) -> list[TransientModulePage]:
36
+ def _prepare_out_of_date_pages_for_loading(g_courses: Sequence[queries.Course], pages: Sequence[db.ModuleItem]) -> list[
37
+ TransientModulePage]:
37
38
  """
38
39
  Removes pages that are up-to-date from the pages list by comparing the last update time of the pages from the query
39
40
  to the last update time of the pages in the database.
@@ -70,6 +71,7 @@ def _prepare_out_of_date_pages_for_loading(g_courses: Sequence[queries.Course],
70
71
 
71
72
  return result
72
73
 
74
+
73
75
  # todo make this reusable and add some way of refreshing only a list of pages or one page or one course or something
74
76
  # todo use logger instead of print and put some signals around the place for useful things, e.g. indexing progress
75
77
  class DataManager:
@@ -111,7 +113,7 @@ class DataManager:
111
113
  self._add_resources_and_pages_to_taskpool(existing_pages=existing_pages,
112
114
  existing_resources=existing_resources)
113
115
 
114
- async def _download_resource_helper(self, link_handler : ResourceScanner, resource: db.Resource):
116
+ async def _download_resource_helper(self, link_handler: ResourceScanner, resource: db.Resource):
115
117
  try:
116
118
  async for progress in link_handler.download(resource):
117
119
  yield progress
@@ -263,7 +265,8 @@ class DataManager:
263
265
  self._moduleitem_pool.add_values({page.id: page for page in existing_pages})
264
266
  self._resource_pool.add_values({resource.id: resource for resource in existing_resources})
265
267
  # Add downloaded resources to the resource pool so we don't download them again
266
- self.download_pool.add_values({resource.id: None for resource in existing_resources if resource.state == db.ResourceState.DOWNLOADED})
268
+ self.download_pool.add_values(
269
+ {resource.id: None for resource in existing_resources if resource.state == db.ResourceState.DOWNLOADED})
267
270
 
268
271
  async def _load_page_content(self, pages: Sequence[TransientModulePage]) -> list[db.ModuleItem]:
269
272
  """
@@ -282,7 +285,8 @@ class DataManager:
282
285
  content = page.page
283
286
 
284
287
  if isinstance(content, queries.File):
285
- task = asyncio.create_task(self._load_module_file(content, page.course_id, page.module_id, page.position))
288
+ task = asyncio.create_task(
289
+ self._load_module_file(content, page.course_id, page.module_id, page.position))
286
290
  tasks.append(task)
287
291
  elif isinstance(content, queries.Page):
288
292
  tasks.append(