qcanvas 1.2.0a1__py3-none-any.whl → 1.2.1__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 (77) hide show
  1. qcanvas/icons/__init__.py +56 -9
  2. qcanvas/icons/_icon_type.py +42 -0
  3. qcanvas/icons/_update_icons.py +89 -0
  4. qcanvas/icons/dark/actions/exit.svg +3 -0
  5. qcanvas/icons/dark/actions/mark_all_read.svg +3 -0
  6. qcanvas/icons/dark/actions/open_downloads.svg +3 -0
  7. qcanvas/icons/dark/actions/quick_login.svg +3 -0
  8. qcanvas/icons/dark/actions/sync.svg +3 -0
  9. qcanvas/icons/dark/options/auto_download.svg +3 -0
  10. qcanvas/icons/dark/options/theme.svg +3 -0
  11. qcanvas/icons/dark/tabs/assignments.svg +3 -0
  12. qcanvas/icons/dark/tabs/mail.svg +3 -0
  13. qcanvas/icons/dark/tabs/pages.svg +3 -0
  14. qcanvas/icons/dark/tree_items/assignment.svg +3 -0
  15. qcanvas/icons/dark/tree_items/mail.svg +3 -0
  16. qcanvas/icons/dark/tree_items/module.svg +3 -0
  17. qcanvas/icons/dark/tree_items/page.svg +3 -0
  18. qcanvas/icons/icons.qrc +43 -9
  19. qcanvas/icons/light/actions/exit.svg +3 -0
  20. qcanvas/icons/light/actions/mark_all_read.svg +3 -0
  21. qcanvas/icons/light/actions/open_downloads.svg +3 -0
  22. qcanvas/icons/light/actions/quick_login.svg +3 -0
  23. qcanvas/icons/light/actions/sync.svg +3 -0
  24. qcanvas/icons/light/options/auto_download.svg +3 -0
  25. qcanvas/icons/light/options/ignore_old.svg +3 -0
  26. qcanvas/icons/light/options/include_videos.svg +3 -0
  27. qcanvas/icons/light/options/theme.svg +3 -0
  28. qcanvas/icons/light/tabs/assignments.svg +3 -0
  29. qcanvas/icons/light/tabs/mail.svg +3 -0
  30. qcanvas/icons/light/tabs/pages.svg +3 -0
  31. qcanvas/icons/light/tree_items/assignment.svg +3 -0
  32. qcanvas/icons/light/tree_items/mail.svg +3 -0
  33. qcanvas/icons/light/tree_items/module.svg +3 -0
  34. qcanvas/icons/light/tree_items/page.svg +3 -0
  35. qcanvas/icons/rc_icons.py +1891 -629
  36. qcanvas/icons/{file-downloaded.svg → universal/downloads/downloaded.svg} +1 -1
  37. qcanvas/icons/universal/tabs/assignments_new_content.svg +3 -0
  38. qcanvas/icons/universal/tabs/mail_new_content.svg +3 -0
  39. qcanvas/icons/universal/tabs/pages_new_content.svg +3 -0
  40. qcanvas/icons/universal/tree_items/semester.svg +108 -0
  41. qcanvas/ui/course_viewer/content_tree.py +7 -3
  42. qcanvas/ui/course_viewer/course_tree/__init__.py +1 -0
  43. qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +86 -0
  44. qcanvas/ui/course_viewer/{course_tree.py → course_tree/course_tree.py} +20 -6
  45. qcanvas/ui/course_viewer/course_viewer.py +72 -30
  46. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +6 -2
  47. qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +17 -13
  48. qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +15 -9
  49. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +7 -4
  50. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +6 -2
  51. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +36 -52
  52. qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
  53. qcanvas/ui/main_ui/course_viewer_container.py +11 -10
  54. qcanvas/ui/main_ui/options/auto_download_resources_option.py +3 -1
  55. qcanvas/ui/main_ui/options/theme_selection_menu.py +2 -0
  56. qcanvas/ui/main_ui/qcanvas_window.py +17 -4
  57. qcanvas/ui/memory_tree/_tree_memory.py +1 -0
  58. qcanvas/ui/memory_tree/memory_tree_widget.py +2 -2
  59. qcanvas/ui/setup/setup_dialog.py +1 -1
  60. qcanvas/util/file_icons.py +21 -3
  61. qcanvas/util/html_cleaner.py +2 -0
  62. qcanvas/util/layouts.py +5 -2
  63. qcanvas/util/settings/_mapped_setting.py +6 -1
  64. qcanvas/util/themes/_theme_changer.py +13 -1
  65. qcanvas/util/ui_tools.py +5 -1
  66. {qcanvas-1.2.0a1.dist-info → qcanvas-1.2.1.dist-info}/METADATA +11 -5
  67. qcanvas-1.2.1.dist-info/RECORD +118 -0
  68. qcanvas/icons/sync.svg +0 -7
  69. qcanvas-1.2.0a1.dist-info/RECORD +0 -80
  70. /qcanvas/icons/{logo-transparent-dark.svg → dark/branding/logo_transparent.svg} +0 -0
  71. /qcanvas/icons/{logo-transparent-light.svg → light/branding/logo_transparent.svg} +0 -0
  72. /qcanvas/icons/{main_icon.svg → universal/branding/main_icon.svg} +0 -0
  73. /qcanvas/icons/{file-download-failed.svg → universal/downloads/download_failed.svg} +0 -0
  74. /qcanvas/icons/{file-not-downloaded.svg → universal/downloads/not_downloaded.svg} +0 -0
  75. /qcanvas/icons/{file-unknown.svg → universal/downloads/unknown.svg} +0 -0
  76. {qcanvas-1.2.0a1.dist-info → qcanvas-1.2.1.dist-info}/WHEEL +0 -0
  77. {qcanvas-1.2.0a1.dist-info → qcanvas-1.2.1.dist-info}/entry_points.txt +0 -0
@@ -5,7 +5,9 @@ import qcanvas_backend.database.types as db
5
5
  from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
6
  from qtpy.QtCore import Qt
7
7
 
8
+ from qcanvas import icons
8
9
  from qcanvas.ui.course_viewer.content_tree import ContentTree
10
+ from qcanvas.ui.course_viewer.tree_widget_data_item import TreeWidgetDataItem
9
11
  from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
10
12
 
11
13
  _logger = logging.getLogger(__name__)
@@ -45,14 +47,16 @@ class PageTree(ContentTree[db.Course]):
45
47
  id=module.id, data=module, strings=[module.name]
46
48
  )
47
49
  module_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
50
+ module_widget.setIcon(0, icons.tree_items.module)
48
51
 
49
52
  return module_widget
50
53
 
51
54
  def _create_page_widget(
52
55
  self, page: db.ModulePage, sync_receipt: SyncReceipt
53
- ) -> MemoryTreeWidgetItem:
54
- page_widget = MemoryTreeWidgetItem(id=page.id, data=page, strings=[page.name])
56
+ ) -> TreeWidgetDataItem:
57
+ page_widget = TreeWidgetDataItem(id=page.id, data=page, strings=[page.name])
55
58
  page_widget.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
59
+ page_widget.setIcon(0, icons.tree_items.page)
56
60
 
57
61
  if sync_receipt.was_updated(page):
58
62
  self.mark_as_unseen(page_widget)
@@ -1,3 +1,4 @@
1
+ import html
1
2
  import logging
2
3
  from typing import Optional
3
4
 
@@ -6,12 +7,11 @@ from bs4 import BeautifulSoup, Tag
6
7
  from qasync import asyncSlot
7
8
  from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
8
9
  from qcanvas_backend.net.resources.extracting.no_extractor_error import NoExtractorError
9
- from qcanvas_backend.net.resources.scanning.resource_scanner import ResourceScanner
10
+ from qcanvas_backend.util import is_link_invisible
10
11
  from qtpy.QtCore import QUrl, Slot
11
12
  from qtpy.QtGui import QDesktopServices
12
13
  from qtpy.QtWidgets import QTextBrowser
13
14
 
14
- from qcanvas import icons
15
15
  from qcanvas.backend_connectors import FrontendResourceManager
16
16
  from qcanvas.util.html_cleaner import clean_up_html
17
17
  from qcanvas.util.qurl_util import file_url
@@ -19,22 +19,6 @@ from qcanvas.util.qurl_util import file_url
19
19
  _logger = logging.getLogger(__name__)
20
20
 
21
21
 
22
- # class _DarkListener(QObject):
23
- # theme_changed = Signal(str)
24
- #
25
- # def __init__(self):
26
- # super().__init__()
27
- #
28
- # self._thread = threading.Thread(target=darkdetect.listener, args=(self._emit,))
29
- # self._thread.daemon = True
30
- # self._thread.start()
31
- #
32
- # def _emit(self, theme: str) -> None:
33
- # self.theme_changed.emit(theme)
34
- #
35
- # _dark_listener = _DarkListener()
36
-
37
-
38
22
  class ResourceRichBrowser(QTextBrowser):
39
23
  def __init__(self, downloader: ResourceManager):
40
24
  super().__init__()
@@ -85,8 +69,7 @@ class ResourceRichBrowser(QTextBrowser):
85
69
  extractor = self._extractors.extractor_for_tag(resource_link)
86
70
  resource_id = extractor.resource_id_from_tag(resource_link)
87
71
 
88
- # FIXME private method
89
- if ResourceScanner._is_link_invisible(resource_link):
72
+ if is_link_invisible(resource_link):
90
73
  _logger.debug("Found dead link for %s, removing", resource_id)
91
74
  resource_link.decompose()
92
75
  continue
@@ -97,7 +80,7 @@ class ResourceRichBrowser(QTextBrowser):
97
80
  continue
98
81
 
99
82
  file_link_tag = self._create_resource_link_tag(
100
- doc, resource_id, resource_link.name == "img"
83
+ resource_id, resource_link.name == "img"
101
84
  )
102
85
  resource_link.replace_with(file_link_tag)
103
86
  except NoExtractorError:
@@ -105,9 +88,7 @@ class ResourceRichBrowser(QTextBrowser):
105
88
 
106
89
  return str(doc)
107
90
 
108
- def _create_resource_link_tag(
109
- self, doc: BeautifulSoup, resource_id: str, is_image: bool
110
- ) -> Tag:
91
+ def _create_resource_link_tag(self, resource_id: str, is_image: bool) -> Tag:
111
92
  resource = self._current_content_resources[resource_id]
112
93
 
113
94
  # todo not sure if this is a good idea or not
@@ -121,40 +102,26 @@ class ResourceRichBrowser(QTextBrowser):
121
102
  # },
122
103
  # )
123
104
  # else:
124
- file_link_tag = doc.new_tag(
125
- "a",
126
- attrs={
127
- "href": f"data:{resource_id}",
128
- },
129
- )
130
-
131
- file_link_tag.append(self._file_icon_tag(doc, resource.download_state))
132
- file_link_tag.append("\N{NO-BREAK SPACE}" + resource.file_name)
133
-
134
- _logger.debug(str(file_link_tag))
135
-
136
- return file_link_tag
137
-
138
- def _file_icon_tag(
139
- self, document: BeautifulSoup, download_state: db.ResourceDownloadState
140
- ) -> Tag:
141
- return document.new_tag(
142
- "img",
143
- attrs={
144
- "src": self._download_state_icon(download_state),
145
- "style": "vertical-align:middle",
146
- "width": 18,
147
- },
148
- )
105
+
106
+ return BeautifulSoup(
107
+ markup=f"""
108
+ <a href="data:{html.escape(resource_id)}" style="font-weight: normal;">
109
+ <img height="18" src="{html.escape(self._download_state_icon(resource.download_state))}"/>
110
+ {html.escape(resource.file_name)}
111
+ </a>
112
+ """,
113
+ features="html.parser",
114
+ ).a
149
115
 
150
116
  def _download_state_icon(self, download_state: db.ResourceDownloadState) -> str:
117
+ icon_path = ":icons/universal/downloads"
151
118
  match download_state:
152
119
  case db.ResourceDownloadState.DOWNLOADED:
153
- return icons.file_downloaded
120
+ return f"{icon_path}/downloaded.svg"
154
121
  case db.ResourceDownloadState.NOT_DOWNLOADED:
155
- return icons.file_not_downloaded
122
+ return f"{icon_path}/not_downloaded.svg"
156
123
  case db.ResourceDownloadState.FAILED:
157
- return icons.file_download_failed
124
+ return f"{icon_path}/download_failed.svg"
158
125
  case _:
159
126
  raise ValueError()
160
127
 
@@ -183,4 +150,21 @@ class ResourceRichBrowser(QTextBrowser):
183
150
  @Slot(db.Resource)
184
151
  def _download_updated(self, resource: db.Resource) -> None:
185
152
  if self._content is not None and resource.id in self._current_content_resources:
153
+ # BANDAID FIX: In the following situation:
154
+ # - Download is started
155
+ # - Synchronisation is started
156
+ # - Download finishes AFTER the sync
157
+ # --> `resource` is NOT `self._current_content_resources[resource.id]`, because the sync will reload the
158
+ # resource from the DB, but the downloader will still only know about the old resource object.
159
+ # This causes resources not update their download state in the viewer. This line "fixes" that, but does NOT
160
+ # address the root cause. I think reloading the resource from the DB somewhere is the only true fix for this
161
+
162
+ if self._current_content_resources[resource.id] is not resource:
163
+ _logger.warning(
164
+ "Resource has diverged from current loaded data, applying bandaid fix"
165
+ )
166
+ self._current_content_resources[resource.id].download_state = (
167
+ resource.download_state
168
+ )
169
+
186
170
  self._show_page_content(self._content)
@@ -0,0 +1,22 @@
1
+ from typing import List, Optional
2
+
3
+ from qtpy.QtWidgets import QTreeWidgetItem
4
+
5
+ from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
6
+
7
+
8
+ class TreeWidgetDataItem(QTreeWidgetItem):
9
+ def __init__(
10
+ self, id: str, data: Optional[object], strings: Optional[List[str]] = None
11
+ ):
12
+ super().__init__(strings)
13
+ # Still needs ID because it is used to reselect the item
14
+ self._id = id
15
+ self.extra_data = data
16
+
17
+ @property
18
+ def id(self) -> str:
19
+ return self._id
20
+
21
+
22
+ AnyTreeDataItem = TreeWidgetDataItem | MemoryTreeWidgetItem
@@ -6,7 +6,6 @@ import qcanvas_backend.database.types as db
6
6
  from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
7
7
  from qcanvas_backend.net.sync.sync_receipt import SyncReceipt, empty_receipt
8
8
  from qtpy.QtCore import Qt, Slot
9
- from qtpy.QtGui import QIcon
10
9
  from qtpy.QtWidgets import *
11
10
 
12
11
  from qcanvas import icons
@@ -16,7 +15,6 @@ from qcanvas.util import themes
16
15
  _logger = logging.getLogger(__name__)
17
16
 
18
17
 
19
- # todo needs to handle dark mode
20
18
  class _PlaceholderLogo(QLabel):
21
19
  """
22
20
  Automatically resizing logo icon for when no course is selected
@@ -24,12 +22,13 @@ class _PlaceholderLogo(QLabel):
24
22
 
25
23
  def __init__(self):
26
24
  super().__init__()
27
- self._light_icon = QIcon(icons.logo_transparent_light)
28
- self._dark_icon = QIcon(icons.logo_transparent_dark)
25
+ self._icon = icons.branding.logo_transparent
29
26
  self._old_width = -1
30
27
  self._old_height = -1
31
28
  self.setAlignment(Qt.AlignmentFlag.AlignCenter)
32
29
  self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
30
+ # Because we are using a pixmap for the icon, it will not get updated like a normal QIcon when the theme changes,
31
+ # So we need to update it ourselves
33
32
  themes.theme_changed().connect(self._theme_changed)
34
33
 
35
34
  def resizeEvent(self, event) -> None:
@@ -47,12 +46,7 @@ class _PlaceholderLogo(QLabel):
47
46
  if force or (width != self._old_width and height != self._old_height):
48
47
  self._old_width = width
49
48
  self._old_height = height
50
- else:
51
- return
52
-
53
- icon = self._dark_icon if themes.is_dark_mode() else self._light_icon
54
-
55
- self.setPixmap(icon.pixmap(width, height))
49
+ self.setPixmap(self._icon.pixmap(width, height))
56
50
 
57
51
 
58
52
  class CourseViewerContainer(QStackedWidget):
@@ -61,12 +55,14 @@ class CourseViewerContainer(QStackedWidget):
61
55
  self._course_viewers: dict[str, CourseViewer] = {}
62
56
  self._downloader = downloader
63
57
  self._last_course_id: Optional[str] = None
58
+ self._selected_course: Optional[db.Course] = None
64
59
  self._last_sync_receipt: SyncReceipt = empty_receipt()
65
60
  self._placeholder = _PlaceholderLogo()
66
61
  self.addWidget(self._placeholder)
67
62
 
68
63
  def show_blank(self) -> None:
69
64
  self._last_course_id = None
65
+ self._selected_course = None
70
66
  self.setCurrentWidget(self._placeholder)
71
67
 
72
68
  def load_course(self, course: db.Course) -> None:
@@ -82,6 +78,7 @@ class CourseViewerContainer(QStackedWidget):
82
78
  viewer = self._course_viewers[course.id]
83
79
 
84
80
  self.setCurrentWidget(viewer)
81
+ self._selected_course = course
85
82
  self._last_course_id = course.id
86
83
 
87
84
  async def reload_all(
@@ -92,3 +89,7 @@ class CourseViewerContainer(QStackedWidget):
92
89
  if course.id in self._course_viewers:
93
90
  viewer = self._course_viewers[course.id]
94
91
  viewer.reload(course, sync_receipt=sync_receipt)
92
+
93
+ @property
94
+ def selected_course(self) -> Optional[db.Course]:
95
+ return self._selected_course
@@ -5,6 +5,7 @@ from qtpy.QtCore import Slot
5
5
  from qtpy.QtGui import QAction
6
6
  from qtpy.QtWidgets import QMenu
7
7
 
8
+ from qcanvas import icons
8
9
  from qcanvas.util import settings
9
10
 
10
11
  _logger = logging.getLogger(__name__)
@@ -36,6 +37,7 @@ class _EnableVideoDownloadOption(QAction):
36
37
 
37
38
  class AutoDownloadResourcesMenu(QMenu):
38
39
  def __init__(self, parent: Optional[QMenu] = None):
39
- super().__init__("Download new resources", parent)
40
+ super().__init__("Auto download resources", parent)
40
41
  self.addAction(_EnableAutoDownloadOption(self))
41
42
  self.addAction(_EnableVideoDownloadOption(self))
43
+ self.setIcon(icons.options.auto_download)
@@ -4,6 +4,7 @@ from qtpy.QtCore import Slot
4
4
  from qtpy.QtGui import QAction, QActionGroup
5
5
  from qtpy.QtWidgets import QMenu
6
6
 
7
+ from qcanvas import icons
7
8
  from qcanvas.util import settings, themes
8
9
 
9
10
  _logger = logging.getLogger(__name__)
@@ -38,6 +39,7 @@ class ThemeSelectionMenu(QMenu):
38
39
  actions = [auto_theme, light_theme, dark_theme, native_theme]
39
40
 
40
41
  self.addActions(actions)
42
+ self.setIcon(icons.options.theme)
41
43
 
42
44
  for theme in actions:
43
45
  action_group.addAction(theme)
@@ -9,7 +9,7 @@ from qcanvas_backend.database.data_monolith import DataMonolith
9
9
  from qcanvas_backend.net.sync.sync_receipt import SyncReceipt, empty_receipt
10
10
  from qcanvas_backend.qcanvas import QCanvas
11
11
  from qtpy.QtCore import QUrl, Signal, Slot
12
- from qtpy.QtGui import QDesktopServices, QIcon, QKeySequence
12
+ from qtpy.QtGui import QDesktopServices, QKeySequence
13
13
  from qtpy.QtWidgets import *
14
14
 
15
15
  from qcanvas import icons
@@ -37,7 +37,7 @@ class QCanvasWindow(QMainWindow):
37
37
  super().__init__()
38
38
 
39
39
  self.setWindowTitle("QCanvas")
40
- self.setWindowIcon(QIcon(icons.main_icon))
40
+ self.setWindowIcon(icons.branding.main_icon)
41
41
 
42
42
  self._operation_semaphore = BoundedSemaphore()
43
43
  self._data: Optional[DataMonolith] = None
@@ -74,6 +74,7 @@ class QCanvasWindow(QMainWindow):
74
74
  shortcut=QKeySequence("Ctrl+S"),
75
75
  triggered=self._synchronise_requested,
76
76
  parent=app_menu,
77
+ icon=icons.actions.sync,
77
78
  )
78
79
 
79
80
  create_qaction(
@@ -81,17 +82,22 @@ class QCanvasWindow(QMainWindow):
81
82
  shortcut=QKeySequence("Ctrl+D"),
82
83
  triggered=self._open_downloads_folder,
83
84
  parent=app_menu,
85
+ icon=icons.actions.open_downloads,
84
86
  )
85
87
 
86
88
  create_qaction(
87
- name="Quick canvas login",
89
+ name="Open Canvas in browser",
88
90
  shortcut=QKeySequence("Ctrl+O"),
89
91
  triggered=self._open_quick_auth_in_browser,
90
92
  parent=app_menu,
93
+ icon=icons.actions.quick_login,
91
94
  )
92
95
 
93
96
  create_qaction(
94
- name="Mark all as seen", triggered=self._clear_new_items, parent=app_menu
97
+ name="Mark all as seen",
98
+ triggered=self._clear_new_items,
99
+ parent=app_menu,
100
+ icon=icons.actions.mark_all_read,
95
101
  )
96
102
 
97
103
  create_qaction(
@@ -99,6 +105,7 @@ class QCanvasWindow(QMainWindow):
99
105
  shortcut=QKeySequence("Ctrl+Q"),
100
106
  triggered=lambda: self.close(),
101
107
  parent=app_menu,
108
+ icon=icons.actions.exit,
102
109
  )
103
110
 
104
111
  options_menu = menu_bar.addMenu("Options")
@@ -229,6 +236,12 @@ class QCanvasWindow(QMainWindow):
229
236
  @Slot()
230
237
  def _open_downloads_folder(self) -> None:
231
238
  directory = self._qcanvas.resource_manager.downloads_folder
239
+
240
+ if self._course_viewer_container.selected_course is not None:
241
+ directory /= self._qcanvas.resource_manager.course_folder_name(
242
+ self._course_viewer_container.selected_course
243
+ )
244
+
232
245
  directory.mkdir(parents=True, exist_ok=True)
233
246
 
234
247
  QDesktopServices.openUrl(file_url(directory))
@@ -54,6 +54,7 @@ class TreeMemory:
54
54
  def set_expanded(self, node_id: str, expanded: bool) -> None:
55
55
  contains = node_id in self._state.collapsed_items
56
56
 
57
+ # fixme when using a slow usb stick, this can momentarily block the event loop.
57
58
  if expanded and contains:
58
59
  self._state.collapsed_items.remove(node_id)
59
60
  self._state.save()
@@ -17,7 +17,7 @@ class MemoryTreeWidget(QTreeWidget):
17
17
  parent: Optional[QWidget] = None,
18
18
  ):
19
19
  super().__init__(parent)
20
- self._id_map: dict[str, MemoryTreeWidgetItem] = {}
20
+ self._id_map: dict[str, QTreeWidgetItem] = {}
21
21
  self._memory = TreeMemory(tree_name)
22
22
  self._suppress_expansion_signals = False
23
23
  self._suppress_selection_signal = False
@@ -100,7 +100,7 @@ class MemoryTreeWidget(QTreeWidget):
100
100
  while len(widget_stack) > 0:
101
101
  item = widget_stack.pop()
102
102
 
103
- if isinstance(item, MemoryTreeWidgetItem):
103
+ if hasattr(item, "id"):
104
104
  if item.id in self._id_map or item.id in map_updates:
105
105
  raise ValueError(f"Item with ID {item.id} is already in the tree")
106
106
 
@@ -93,7 +93,7 @@ class SetupDialog(QDialog):
93
93
  self.setWindowTitle("Configure QCanvas")
94
94
  self.setMinimumSize(550, 200)
95
95
  self.resize(550, 200)
96
- self.setWindowIcon(QIcon(icons.main_icon))
96
+ self.setWindowIcon(QIcon(icons.branding.main_icon))
97
97
 
98
98
  self._semaphore = Semaphore()
99
99
 
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import os.path
2
3
 
3
4
  from qtpy.QtCore import QFileInfo, QMimeDatabase
4
5
  from qtpy.QtGui import QIcon
@@ -17,20 +18,37 @@ if runtime.is_running_on_windows:
17
18
 
18
19
  else:
19
20
  _mime_database = QMimeDatabase()
20
- _default_icon = None
21
+ # This must be initialised lazily because the QApplication might not be initialised at this time
22
+ _default_icon: QIcon | None = None
23
+ _icon_for_suffix: dict[str, QIcon] = {}
21
24
 
22
25
  def icon_for_filename(file_name: str) -> QIcon:
23
26
  global _default_icon
24
27
 
28
+ file_suffix = os.path.splitext(file_name)[1]
29
+
30
+ # Check if we already know what icon this file type has
31
+ if file_suffix in _icon_for_suffix:
32
+ return _icon_for_suffix[file_suffix]
33
+
34
+ # Try to find an icon for this file type
25
35
  for mime_type in _mime_database.mimeTypesForFileName(file_name):
26
36
  icon = QIcon.fromTheme(mime_type.iconName())
27
37
 
28
38
  if not icon.isNull():
39
+ _icon_for_suffix[file_suffix] = icon
29
40
  return icon
30
41
 
42
+ _lazy_init_default_icon()
43
+
44
+ # No icon for this type of file was found, return default icon
45
+ _icon_for_suffix[file_suffix] = _default_icon
46
+ return _default_icon
47
+
48
+ def _lazy_init_default_icon() -> None:
49
+ global _default_icon
50
+
31
51
  if _default_icon is None:
32
52
  _default_icon = QApplication.style().standardIcon(
33
53
  QStyle.StandardPixmap.SP_FileIcon
34
54
  )
35
-
36
- return _default_icon
@@ -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
@@ -22,11 +22,14 @@ def layout_widget(layout_type: Type[T], *items: QWidget, **kwargs) -> QWidget:
22
22
  return widget
23
23
 
24
24
 
25
- def layout(layout_type: Type[T], *items: QWidget, **kwargs) -> T:
25
+ def layout(layout_type: Type[T], *items: QWidget | QLayout, **kwargs) -> T:
26
26
  result_layout: QLayout = layout_type(**kwargs)
27
27
 
28
28
  for item in items:
29
- result_layout.addWidget(item)
29
+ if isinstance(item, QLayout):
30
+ result_layout.addItem(item)
31
+ else:
32
+ result_layout.addWidget(item)
30
33
 
31
34
  return result_layout
32
35
 
@@ -53,8 +53,13 @@ class BoolSetting(MappedSetting[bool]):
53
53
  try:
54
54
  # noinspection PyTypeChecker
55
55
  value: str = super()._read()
56
+
57
+ if isinstance(value, bool):
58
+ return value
59
+
56
60
  return value.lower() == "true"
57
- except:
61
+ except Exception as e:
62
+ _logger.error("Could not read setting", exc_info=e)
58
63
  return self.default
59
64
 
60
65
  # @override
@@ -2,6 +2,7 @@ import logging
2
2
 
3
3
  import qdarktheme
4
4
  from qtpy.QtCore import Slot
5
+ from qtpy.QtGui import QIcon
5
6
  from qtpy.QtWidgets import QApplication, QStyleFactory
6
7
 
7
8
  from qcanvas.util.themes._colour_scheme_helper import (
@@ -17,6 +18,10 @@ default_theme = "auto"
17
18
  _is_dark_mode: bool | None = None
18
19
  _selected_theme: SelectedTheme | None = None
19
20
 
21
+ _universal_path = ":icons/universal"
22
+ _dark_path = ":icons/dark"
23
+ _light_path = ":icons/light"
24
+
20
25
 
21
26
  def apply(theme: str) -> None:
22
27
  global _is_dark_mode, _selected_theme
@@ -45,6 +50,7 @@ def apply(theme: str) -> None:
45
50
  _selected_theme = SelectedTheme.NATIVE
46
51
 
47
52
  if was_dark_mode != _is_dark_mode:
53
+ _set_fallback_paths()
48
54
  theme_changed().emit()
49
55
 
50
56
 
@@ -66,9 +72,15 @@ def _scheme_changed():
66
72
  if _selected_theme == SelectedTheme.AUTO:
67
73
  apply("auto")
68
74
  elif _selected_theme == SelectedTheme.NATIVE:
69
- # noinspection PyTestUnpassedFixture
70
75
  _is_dark_mode = is_dark_colour_scheme()
76
+ _set_fallback_paths()
71
77
  theme_changed().emit()
72
78
 
73
79
 
80
+ def _set_fallback_paths():
81
+ QIcon.setFallbackSearchPaths(
82
+ [_dark_path if _is_dark_mode else _light_path, _universal_path]
83
+ )
84
+
85
+
74
86
  colour_scheme_changed().connect(_scheme_changed)
qcanvas/util/ui_tools.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from typing import Any
3
3
 
4
- from qtpy.QtGui import QKeySequence
4
+ from qtpy.QtGui import QIcon, QKeySequence, QPixmap
5
5
  from qtpy.QtWidgets import *
6
6
 
7
7
  _logger = logging.getLogger(__name__)
@@ -19,6 +19,7 @@ def create_qaction(
19
19
  triggered: Any = None,
20
20
  checkable: bool | None = None,
21
21
  checked: bool | None = None,
22
+ icon: QIcon | QPixmap | None = None
22
23
  ) -> QAction:
23
24
  action = QAction(name)
24
25
 
@@ -38,4 +39,7 @@ def create_qaction(
38
39
  if checked is not None:
39
40
  action.setChecked(checked)
40
41
 
42
+ if icon is not None:
43
+ action.setIcon(icon)
44
+
41
45
  return action
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qcanvas
3
- Version: 1.2.0a1
3
+ Version: 1.2.1
4
4
  Summary: QCanvas is a desktop client for Canvas LMS.
5
5
  Author: QCanvas
6
6
  Author-email: QCanvas@noreply.codeberg.org
7
- Requires-Python: >=3.11,<3.13
7
+ Requires-Python: >3.11.0,<3.13
8
8
  Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.11
10
9
  Classifier: Programming Language :: Python :: 3.12
11
10
  Requires-Dist: aiosqlite (>=0.20.0,<0.21.0)
12
11
  Requires-Dist: asynctaskpool (>=0.2.1,<0.3.0)
@@ -15,7 +14,7 @@ Requires-Dist: platformdirs (>=4.2.2,<5.0.0)
15
14
  Requires-Dist: pyqtdarktheme-fork (>=2.3.2,<3.0.0)
16
15
  Requires-Dist: qasync (>=0.27.1,<0.28.0)
17
16
  Requires-Dist: qcanvas-api-clients (>=0.3.0,<0.4.0)
18
- Requires-Dist: qcanvas-backend (>=0.2.2,<0.3.0)
17
+ Requires-Dist: qcanvas-backend (>=0.2.7,<0.3.0)
19
18
  Requires-Dist: qtpy (>=2.4.1,<3.0.0)
20
19
  Requires-Dist: sqlalchemy (>=2.0.31,<3.0.0)
21
20
  Requires-Dist: validators (>=0.33.0,<0.34.0)
@@ -31,7 +30,14 @@ https://github.com/QCanvas/QCanvasApp
31
30
 
32
31
  # Downloads
33
32
 
34
- Download it from [releases](https://github.com/QCanvas/QCanvasApp/releases)
33
+ <a href='https://flathub.org/apps/io.github.qcanvas.QCanvasApp'>
34
+ <img width='240' alt='Get it on Flathub' src='https://flathub.org/api/badge?svg&locale=en'/>
35
+ </a>
36
+
37
+ You can download a **windows** version from [releases](https://github.com/QCanvas/QCanvasApp/releases)
38
+
39
+ The appimage version is *not recommended* as it is not a proper portable appimage. It will only work on debian/ubuntu
40
+ based distros.
35
41
 
36
42
  # Development/Run from source
37
43