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,6 @@
1
+ from ._client_settings import _ClientSettings
2
+ from ._ui_settings import _UISettings
3
+ from ._course_settings import course_configs
4
+
5
+ client = _ClientSettings()
6
+ ui = _UISettings()
@@ -1,11 +1,11 @@
1
1
  import logging
2
- from typing import *
2
+ from typing import Optional
3
3
 
4
- from qcanvas_api_clients.canvas import CanvasClientConfig
5
- from qcanvas_api_clients.panopto import PanoptoClientConfig
4
+ from libqcanvas_clients.canvas import CanvasClientConfig
5
+ from libqcanvas_clients.panopto import PanoptoClientConfig
6
6
 
7
7
  from qcanvas.util import paths
8
- from qcanvas.util.settings._mapped_setting import BoolSetting, MappedSetting
8
+ from ._mapped_setting import BoolSetting, MappedSetting
9
9
 
10
10
  _logger = logging.getLogger(__name__)
11
11
 
@@ -15,6 +15,7 @@ class _ClientSettings:
15
15
  canvas_url: MappedSetting[Optional[str]] = MappedSetting(default=None)
16
16
  canvas_api_key: MappedSetting[Optional[str]] = MappedSetting(default=None)
17
17
  panopto_url: MappedSetting[Optional[str]] = MappedSetting(default=None)
18
+ panopto_disabled = BoolSetting(default=False)
18
19
  quick_sync_enabled = BoolSetting(default=False)
19
20
  sync_on_start = BoolSetting(default=False)
20
21
  download_new_resources = BoolSetting(default=False)
@@ -27,5 +28,13 @@ class _ClientSettings:
27
28
  )
28
29
 
29
30
  @property
30
- def panopto_config(self) -> PanoptoClientConfig:
31
- return PanoptoClientConfig(panopto_url=self.panopto_url)
31
+ def panopto_config(self) -> Optional[PanoptoClientConfig]:
32
+ """
33
+ Generates a panopto client config. If panopto is disabled, it returns None.
34
+ """
35
+
36
+ if self.panopto_disabled:
37
+ _logger.debug("Panopto is disabled")
38
+ return None
39
+ else:
40
+ return PanoptoClientConfig(panopto_url=self.panopto_url)
@@ -0,0 +1,54 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ from aiofile import async_open
5
+ from pydantic import BaseModel, RootModel, Field, ValidationError
6
+
7
+ from qcanvas.util import paths
8
+
9
+ _logger = logging.getLogger(__name__)
10
+
11
+
12
+ class CourseConfigData(BaseModel):
13
+ nickname: str | None = Field(default=None)
14
+
15
+ async def save(self) -> None:
16
+ await course_configs.save()
17
+
18
+
19
+ _CourseConfigurations = RootModel[dict[str, CourseConfigData]]
20
+
21
+
22
+ class _CourseConfig:
23
+ def __init__(self):
24
+ self._root_model = self._load_root_model()
25
+
26
+ def _load_root_model(self) -> _CourseConfigurations:
27
+ if self._storage_path.exists():
28
+ try:
29
+ return _CourseConfigurations.model_validate_json(
30
+ self._storage_path.read_text()
31
+ )
32
+ except ValidationError as e:
33
+ _logger.error("Failed to load course configs", exc_info=e)
34
+
35
+ return _CourseConfigurations({})
36
+
37
+ async def save(self) -> None:
38
+ async with async_open(self._storage_path, "wt") as file:
39
+ await file.write(self._root_model.model_dump_json(indent=4))
40
+
41
+ @property
42
+ def _storage_path(self) -> Path:
43
+ return paths.config_storage() / "course_settings.json"
44
+
45
+ def __getitem__(self, item: str) -> CourseConfigData:
46
+ if item in self._root_model.root:
47
+ return self._root_model.root[item]
48
+ else:
49
+ new_config = CourseConfigData()
50
+ self._root_model.root[item] = new_config
51
+ return new_config
52
+
53
+
54
+ course_configs = _CourseConfig()
@@ -1,14 +1,11 @@
1
1
  import logging
2
- from typing import *
3
2
 
4
- from qtpy.QtCore import QSettings
3
+ from PySide6.QtCore import QSettings
5
4
 
6
5
  _logger = logging.getLogger(__name__)
7
6
 
8
- T = TypeVar("T")
9
7
 
10
-
11
- class MappedSetting(Generic[T]):
8
+ class MappedSetting[T]:
12
9
  """
13
10
  Acts as a proxy for a named value in a QSettings object.
14
11
  Stores the value in memory when initialised and updates it accordingly, to protect it from changes on disk.
@@ -53,8 +50,13 @@ class BoolSetting(MappedSetting[bool]):
53
50
  try:
54
51
  # noinspection PyTypeChecker
55
52
  value: str = super()._read()
53
+
54
+ if isinstance(value, bool):
55
+ return value
56
+
56
57
  return value.lower() == "true"
57
- except:
58
+ except Exception as e:
59
+ _logger.error("Could not read setting", exc_info=e)
58
60
  return self.default
59
61
 
60
62
  # @override
@@ -1,16 +1,16 @@
1
1
  import logging
2
2
 
3
- from qtpy.QtCore import QByteArray, QSettings
3
+ from PySide6.QtCore import QByteArray, QSettings
4
4
 
5
- from qcanvas.util.settings._mapped_setting import MappedSetting
6
- from qcanvas.util.themes import default_theme, ensure_theme_is_valid
5
+ from ._mapped_setting import MappedSetting
6
+ from qcanvas.theme import ensure_theme_is_valid
7
7
 
8
8
  _logger = logging.getLogger(__name__)
9
9
 
10
10
 
11
11
  class ThemeSetting(MappedSetting):
12
12
  def __init__(self):
13
- super().__init__(default=default_theme)
13
+ super().__init__(default="auto")
14
14
 
15
15
  def __get__(self, instance, owner):
16
16
  return ensure_theme_is_valid(super().__get__(instance, owner))
@@ -20,7 +20,7 @@ class ThemeSetting(MappedSetting):
20
20
 
21
21
 
22
22
  class _UISettings:
23
- settings = QSettings("QCanvasTeam", "QCanvas")
23
+ settings = QSettings("QCanvasTeam", "UI")
24
24
  theme: ThemeSetting = ThemeSetting()
25
25
  last_geometry: MappedSetting[QByteArray] = MappedSetting()
26
26
  last_window_state: MappedSetting[QByteArray] = MappedSetting()
qcanvas/theme.py ADDED
@@ -0,0 +1,101 @@
1
+ import logging
2
+ from typing import Literal
3
+
4
+ import qdarktheme
5
+ from PySide6.QtCore import QObject, Signal, Property, Slot
6
+ from PySide6.QtGui import QGuiApplication, Qt, QIcon
7
+ from PySide6.QtWidgets import QStyleFactory, QApplication
8
+
9
+ type Theme = Literal["native", "auto", "dark", "light"]
10
+
11
+ _logger = logging.getLogger(__name__)
12
+
13
+
14
+ class _AppTheme(QObject):
15
+ themeChanged = Signal()
16
+ darkModeChanged = Signal()
17
+
18
+ def __init__(self):
19
+ super().__init__()
20
+ self._last_system_theme = QGuiApplication.styleHints().colorScheme()
21
+ self._theme: Theme | None = None
22
+ self._dark_mode: bool | None = None
23
+
24
+ self.darkModeChanged.connect(self._set_icon_paths)
25
+ QGuiApplication.styleHints().colorSchemeChanged.connect(
26
+ self._on_system_theme_changed
27
+ )
28
+
29
+ @Property(bool, notify=darkModeChanged)
30
+ def dark_mode(self) -> bool:
31
+ assert self._theme is not None, "Theme has not been set"
32
+ return self._dark_mode
33
+
34
+ @Property(str, notify=themeChanged)
35
+ def theme(self) -> Theme:
36
+ assert self._theme is not None, "Theme has not been set"
37
+ return self._theme
38
+
39
+ @theme.setter
40
+ def theme(self, value: str):
41
+ value = ensure_theme_is_valid(value)
42
+
43
+ if value != self._theme:
44
+ self._update_theme(value)
45
+
46
+ def _update_theme(self, theme: str):
47
+ if theme is None or (theme == self._theme and theme not in ["native", "auto"]):
48
+ return
49
+
50
+ was_dark_mode = self._dark_mode
51
+
52
+ if theme != "native":
53
+ if theme == "auto":
54
+ self._dark_mode = _is_system_using_dark_mode()
55
+ selected_colour_scheme = "dark" if self._dark_mode else "light"
56
+ else:
57
+ self._dark_mode = theme == "dark"
58
+ selected_colour_scheme = theme
59
+
60
+ if was_dark_mode != self._dark_mode:
61
+ qdarktheme.setup_theme(
62
+ selected_colour_scheme,
63
+ custom_colors={"primary": "e02424"},
64
+ )
65
+
66
+ QApplication.setStyle(QStyleFactory.create("Fusion"))
67
+ else:
68
+ self._dark_mode = _is_system_using_dark_mode()
69
+
70
+ if theme != self._theme:
71
+ self._theme = theme
72
+ self.themeChanged.emit()
73
+
74
+ if was_dark_mode != self._dark_mode:
75
+ self.darkModeChanged.emit()
76
+
77
+ @Slot()
78
+ def _set_icon_paths(self):
79
+ QIcon.setFallbackSearchPaths(
80
+ [":icons/dark" if self._dark_mode else ":icons/light", ":icons/universal"]
81
+ )
82
+
83
+ @Slot()
84
+ def _on_system_theme_changed(self, scheme: Qt.ColorScheme):
85
+ if scheme != self._last_system_theme:
86
+ self._last_system_theme = scheme
87
+ self._update_theme(self._theme)
88
+
89
+
90
+ def _is_system_using_dark_mode() -> bool:
91
+ return QGuiApplication.styleHints().colorScheme() == Qt.ColorScheme.Dark
92
+
93
+
94
+ def ensure_theme_is_valid(theme_name: str) -> Theme:
95
+ if theme_name not in ["auto", "light", "dark", "native"]:
96
+ return "auto"
97
+ else:
98
+ return theme_name
99
+
100
+
101
+ app_theme = _AppTheme()
@@ -1,28 +1,26 @@
1
1
  import logging
2
2
  from abc import abstractmethod
3
- from typing import *
3
+ from typing import Optional, Self, Sequence
4
4
 
5
- import qcanvas_backend.database.types as db
6
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
- from qtpy.QtCore import QItemSelection, Signal, Slot
8
- from qtpy.QtWidgets import *
5
+ from libqcanvas import db
6
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
7
+ from PySide6.QtCore import QItemSelection, Signal, Slot
8
+ from PySide6.QtWidgets import QHeaderView, QTreeWidgetItem
9
9
 
10
- from qcanvas.ui.memory_tree import MemoryTreeWidget, MemoryTreeWidgetItem
10
+ from qcanvas.ui.course_viewer.tree_widget_data_item import AnyTreeDataItem
11
+ from qcanvas.ui.memory_tree import MemoryTreeWidget
11
12
  from qcanvas.util.basic_fonts import bold_font, normal_font
12
13
 
13
14
  _logger = logging.getLogger(__name__)
14
15
 
15
- T = TypeVar("T")
16
- U = TypeVar("U", bound=Type["ContentTree"])
17
16
 
18
-
19
- class ContentTree(MemoryTreeWidget, Generic[T]):
17
+ class ContentTree[T](MemoryTreeWidget):
20
18
  item_selected = Signal(object)
21
19
 
22
20
  @classmethod
23
- def create_from_receipt(
21
+ def create_from_receipt[U: Self](
24
22
  cls: U, course: db.Course, *, sync_receipt: SyncReceipt
25
- ) -> Type[U]:
23
+ ) -> type[U]:
26
24
  tree = cls(course.id)
27
25
  tree.reload(course, sync_receipt=sync_receipt)
28
26
  return tree
@@ -31,7 +29,7 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
31
29
  self,
32
30
  tree_name: str,
33
31
  *,
34
- emit_selection_signal_for_type: Type,
32
+ emit_selection_signal_for_type: type,
35
33
  ):
36
34
  super().__init__(tree_name)
37
35
  self._reloading = False
@@ -45,8 +43,9 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
45
43
  *,
46
44
  header_text: str | Sequence[str],
47
45
  indentation: int = 20,
48
- max_width: int,
49
- min_width: int,
46
+ max_width: Optional[int] = None,
47
+ min_width: Optional[int] = None,
48
+ alternating_row_colours: bool = False,
50
49
  ) -> None:
51
50
  if not isinstance(header_text, str) and isinstance(header_text, Sequence):
52
51
  self.setHeaderLabels(header_text)
@@ -54,8 +53,27 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
54
53
  self.setHeaderLabel(header_text)
55
54
 
56
55
  self.setIndentation(indentation)
57
- self.setMaximumWidth(max_width)
58
- self.setMinimumWidth(min_width)
56
+
57
+ if max_width is not None:
58
+ self.setMaximumWidth(max_width)
59
+
60
+ if min_width is not None:
61
+ self.setMinimumWidth(min_width)
62
+
63
+ self.setAlternatingRowColors(alternating_row_colours)
64
+
65
+ def set_columns_resize_mode(
66
+ self,
67
+ resize_mode_for_columns: list[QHeaderView.ResizeMode],
68
+ *,
69
+ stretch_last: bool = False,
70
+ ) -> None:
71
+ header = self.header()
72
+
73
+ for index, mode in enumerate(resize_mode_for_columns):
74
+ header.setSectionResizeMode(index, mode)
75
+
76
+ header.setStretchLastSection(stretch_last)
59
77
 
60
78
  def reload(self, data: T, *, sync_receipt: SyncReceipt) -> None:
61
79
  self._reloading = True
@@ -77,7 +95,7 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
77
95
  @abstractmethod
78
96
  def create_tree_items(
79
97
  self, data: T, sync_receipt: SyncReceipt
80
- ) -> Sequence[MemoryTreeWidgetItem]: ...
98
+ ) -> Sequence[QTreeWidgetItem]: ...
81
99
 
82
100
  @Slot(QItemSelection, QItemSelection)
83
101
  def _selection_changed(self, _0: QItemSelection, _1: QItemSelection) -> None:
@@ -93,7 +111,7 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
93
111
  if self.is_unseen(selected):
94
112
  self.mark_as_seen(selected)
95
113
 
96
- if not isinstance(selected, MemoryTreeWidgetItem):
114
+ if not isinstance(selected, AnyTreeDataItem):
97
115
  self._clear_selection()
98
116
  return
99
117
 
@@ -0,0 +1 @@
1
+ from .course_tree import CourseTree
@@ -0,0 +1,86 @@
1
+ import itertools
2
+ import logging
3
+ import random
4
+
5
+ from cachetools import cached
6
+ from PySide6.QtCore import QByteArray
7
+ from PySide6.QtGui import QColor, QPainter, QPixmap
8
+ from PySide6.QtSvg import QSvgRenderer
9
+
10
+ _logger = logging.getLogger(__name__)
11
+ _transparent = QColor("#00000000")
12
+ _colours = [
13
+ QColor(f"#{colour}")
14
+ for colour in [
15
+ "2ad6cb",
16
+ "2d50ed",
17
+ "7a10e4",
18
+ "c61aaf",
19
+ "d91b1b",
20
+ "c7541b",
21
+ "facd07", # facd07
22
+ "a9cf12",
23
+ ]
24
+ ]
25
+
26
+
27
+ class CourseIconGenerator:
28
+ @staticmethod
29
+ @cached(cache={})
30
+ def get_for_term(term_id: str) -> "CourseIconGenerator":
31
+ return CourseIconGenerator(term_id)
32
+
33
+ def __init__(self, term_id: str):
34
+ shuffled = list(_colours)
35
+
36
+ # This is the dumbest way I've ever seen a language implement setting a seed for a RNG.
37
+ # WTF python?! Why???
38
+ random.seed(term_id)
39
+ random.shuffle(shuffled)
40
+
41
+ self._iterator = itertools.cycle(shuffled)
42
+
43
+ def get_icon(self) -> QPixmap:
44
+ return _create_icon(self._get_colour())
45
+
46
+ def _get_colour(self) -> QColor:
47
+ return next(self._iterator)
48
+
49
+
50
+ @cached(cache={}, key=lambda colour: colour.name(QColor.NameFormat.HexRgb))
51
+ def _create_icon(base_colour: QColor) -> QPixmap:
52
+ dark_colour = QColor.fromHslF(
53
+ base_colour.hslHueF(),
54
+ base_colour.hslSaturationF(),
55
+ base_colour.lightnessF() * 0.6875,
56
+ )
57
+
58
+ result_pixmap = QPixmap(256, 256)
59
+ result_pixmap.fill(_transparent)
60
+
61
+ with (painter := QPainter(result_pixmap)):
62
+ svg = _create_svg_from_colours(base_colour, dark_colour)
63
+ svg.render(painter)
64
+
65
+ return result_pixmap
66
+
67
+
68
+ def _create_svg_from_colours(light_colour: QColor, dark_colour: QColor) -> QSvgRenderer:
69
+ # Original SVG is from SVGRepo.com
70
+ return QSvgRenderer(
71
+ QByteArray(
72
+ f"""
73
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
74
+ <svg width="800px" height="800px" viewBox="0 0 24 24" >
75
+ <g transform="translate(0 -1028.4)">
76
+ <path d="m3 8v2 1 3 1 5 1c0 1.105 0.8954 2 2 2h14c1.105 0 2-0.895 2-2v-1-5-4-3h-18z" transform="translate(0 1028.4)" fill="{dark_colour.name()}"/>
77
+ <path d="m3 1035.4v2 1 3 1 5 1c0 1.1 0.8954 2 2 2h14c1.105 0 2-0.9 2-2v-1-5-4-3h-18z" fill="#ecf0f1"/>
78
+ <path d="m3 1034.4v2 1 3 1 5 1c0 1.1 0.8954 2 2 2h14c1.105 0 2-0.9 2-2v-1-5-4-3h-18z" fill="#bdc3c7"/>
79
+ <path d="m3 1033.4v2 1 3 1 5 1c0 1.1 0.8954 2 2 2h14c1.105 0 2-0.9 2-2v-1-5-4-3h-18z" fill="#ecf0f1"/>
80
+ <path d="m5 1c-1.1046 0-2 0.8954-2 2v1 4 2 1 3 1 5 1c0 1.105 0.8954 2 2 2h2v-1h-1.5c-0.8284 0-1.5-0.672-1.5-1.5s0.6716-1.5 1.5-1.5h12.5 1c1.105 0 2-0.895 2-2v-1-5-4-3-1c0-1.1046-0.895-2-2-2h-4-10z" transform="translate(0 1028.4)" fill="{dark_colour.name()}"/>
81
+ <path d="m8 1v18h1 9 1c1.105 0 2-0.895 2-2v-1-5-4-3-1c0-1.1046-0.895-2-2-2h-4-6-1z" transform="translate(0 1028.4)" fill="{light_colour.name()}"/>
82
+ </g>
83
+ </svg>
84
+ """.strip().encode()
85
+ )
86
+ )
@@ -1,23 +1,29 @@
1
1
  import logging
2
- from typing import *
2
+ from typing import Sequence
3
3
 
4
- import qcanvas_backend.database.types as db
5
- from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
- from qtpy.QtCore import Qt, Signal
4
+ from libqcanvas import db
5
+ from libqcanvas.net.sync.sync_receipt import SyncReceipt
6
+ from PySide6.QtCore import Qt, Signal
7
7
 
8
+ from qcanvas import icons
9
+ from qcanvas.settings import course_configs
8
10
  from qcanvas.ui.course_viewer.content_tree import ContentTree
11
+ from qcanvas.ui.course_viewer.course_tree._course_icon_generator import (
12
+ CourseIconGenerator,
13
+ )
14
+ from qcanvas.ui.course_viewer.tree_widget_data_item import TreeWidgetDataItem
9
15
  from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
10
16
 
11
17
  _logger = logging.getLogger(__name__)
12
18
 
13
19
 
14
- class _CourseTreeItem(MemoryTreeWidgetItem):
20
+ class _CourseTreeItem(TreeWidgetDataItem):
15
21
  def __init__(self, course: db.Course, owner: "CourseTree"):
16
- MemoryTreeWidgetItem.__init__(
22
+ TreeWidgetDataItem.__init__(
17
23
  self,
18
24
  id=course.id,
19
25
  data=course,
20
- strings=[course.configuration.nickname or course.name],
26
+ strings=[course_configs[course.id].nickname or course.name],
21
27
  )
22
28
 
23
29
  self._owner = owner
@@ -29,7 +35,7 @@ class _CourseTreeItem(MemoryTreeWidgetItem):
29
35
  | Qt.ItemFlag.ItemIsEnabled
30
36
  )
31
37
 
32
- def setData(self, column: int, role: int, value: Any):
38
+ def setData(self, column: int, role: int, value: object):
33
39
  if column != 0 or not isinstance(value, str):
34
40
  return super().setData(column, role, value)
35
41
 
@@ -49,19 +55,23 @@ class CourseTree(ContentTree[Sequence[db.Term]]):
49
55
  def __init__(self):
50
56
  super().__init__("course_tree", emit_selection_signal_for_type=db.Course)
51
57
 
52
- self.ui_setup(max_width=250, min_width=150, header_text="Courses")
58
+ self.ui_setup(
59
+ max_width=250, min_width=150, header_text="Courses", indentation=15
60
+ )
53
61
 
54
62
  def create_tree_items(
55
- self, terms: List[db.Term], sync_receipt: SyncReceipt
63
+ self, terms: list[db.Term], sync_receipt: SyncReceipt
56
64
  ) -> Sequence[MemoryTreeWidgetItem]:
57
65
  widgets = []
58
66
 
59
- for term in reversed(terms):
67
+ for term in terms:
60
68
  term_widget = self._create_term_widget(term)
69
+ course_icon_generator = CourseIconGenerator(term.id)
61
70
 
62
71
  for course in term.courses:
63
- course_widget = self._create_course_widget(course, sync_receipt)
64
- # course_widget.renamed.connect(self._on_course_renamed)
72
+ course_widget = self._create_course_widget(
73
+ course, course_icon_generator, sync_receipt
74
+ )
65
75
  term_widget.addChild(course_widget)
66
76
 
67
77
  widgets.append(term_widget)
@@ -69,9 +79,13 @@ class CourseTree(ContentTree[Sequence[db.Term]]):
69
79
  return widgets
70
80
 
71
81
  def _create_course_widget(
72
- self, course: db.Course, sync_receipt: SyncReceipt
82
+ self,
83
+ course: db.Course,
84
+ course_icon_generator: CourseIconGenerator,
85
+ sync_receipt: SyncReceipt,
73
86
  ) -> _CourseTreeItem:
74
87
  course_widget = _CourseTreeItem(course, self)
88
+ course_widget.setIcon(0, course_icon_generator.get_icon())
75
89
 
76
90
  if sync_receipt.was_updated(course):
77
91
  self.mark_as_unseen(course_widget)
@@ -81,5 +95,6 @@ class CourseTree(ContentTree[Sequence[db.Term]]):
81
95
  def _create_term_widget(self, term: db.Term) -> MemoryTreeWidgetItem:
82
96
  term_widget = MemoryTreeWidgetItem(id=term.id, data=term, strings=[term.name])
83
97
  term_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
98
+ term_widget.setIcon(0, icons.tree_items.semester)
84
99
 
85
100
  return term_widget