qcanvas 1.2.0a0__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 (83) hide show
  1. qcanvas/icons/__init__.py +56 -8
  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/branding/logo_transparent.svg +303 -0
  10. qcanvas/icons/dark/options/auto_download.svg +3 -0
  11. qcanvas/icons/dark/options/theme.svg +3 -0
  12. qcanvas/icons/dark/tabs/assignments.svg +3 -0
  13. qcanvas/icons/dark/tabs/mail.svg +3 -0
  14. qcanvas/icons/dark/tabs/pages.svg +3 -0
  15. qcanvas/icons/dark/tree_items/assignment.svg +3 -0
  16. qcanvas/icons/dark/tree_items/mail.svg +3 -0
  17. qcanvas/icons/dark/tree_items/module.svg +3 -0
  18. qcanvas/icons/dark/tree_items/page.svg +3 -0
  19. qcanvas/icons/icons.qrc +43 -8
  20. qcanvas/icons/light/actions/exit.svg +3 -0
  21. qcanvas/icons/light/actions/mark_all_read.svg +3 -0
  22. qcanvas/icons/light/actions/open_downloads.svg +3 -0
  23. qcanvas/icons/light/actions/quick_login.svg +3 -0
  24. qcanvas/icons/light/actions/sync.svg +3 -0
  25. qcanvas/icons/light/options/auto_download.svg +3 -0
  26. qcanvas/icons/light/options/ignore_old.svg +3 -0
  27. qcanvas/icons/light/options/include_videos.svg +3 -0
  28. qcanvas/icons/light/options/theme.svg +3 -0
  29. qcanvas/icons/light/tabs/assignments.svg +3 -0
  30. qcanvas/icons/light/tabs/mail.svg +3 -0
  31. qcanvas/icons/light/tabs/pages.svg +3 -0
  32. qcanvas/icons/light/tree_items/assignment.svg +3 -0
  33. qcanvas/icons/light/tree_items/mail.svg +3 -0
  34. qcanvas/icons/light/tree_items/module.svg +3 -0
  35. qcanvas/icons/light/tree_items/page.svg +3 -0
  36. qcanvas/icons/rc_icons.py +2179 -638
  37. qcanvas/icons/{file-downloaded.svg → universal/downloads/downloaded.svg} +1 -1
  38. qcanvas/icons/universal/tabs/assignments_new_content.svg +3 -0
  39. qcanvas/icons/universal/tabs/mail_new_content.svg +3 -0
  40. qcanvas/icons/universal/tabs/pages_new_content.svg +3 -0
  41. qcanvas/icons/universal/tree_items/semester.svg +108 -0
  42. qcanvas/run.py +1 -2
  43. qcanvas/ui/course_viewer/content_tree.py +7 -3
  44. qcanvas/ui/course_viewer/course_tree/__init__.py +1 -0
  45. qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +86 -0
  46. qcanvas/ui/course_viewer/{course_tree.py → course_tree/course_tree.py} +20 -6
  47. qcanvas/ui/course_viewer/course_viewer.py +72 -30
  48. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +6 -2
  49. qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +17 -13
  50. qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +15 -9
  51. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +7 -4
  52. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +6 -2
  53. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +36 -52
  54. qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
  55. qcanvas/ui/main_ui/course_viewer_container.py +22 -9
  56. qcanvas/ui/main_ui/options/auto_download_resources_option.py +3 -1
  57. qcanvas/ui/main_ui/options/theme_selection_menu.py +2 -0
  58. qcanvas/ui/main_ui/qcanvas_window.py +17 -4
  59. qcanvas/ui/memory_tree/_tree_memory.py +1 -0
  60. qcanvas/ui/memory_tree/memory_tree_widget.py +2 -2
  61. qcanvas/ui/setup/setup_dialog.py +1 -1
  62. qcanvas/util/file_icons.py +21 -3
  63. qcanvas/util/html_cleaner.py +2 -0
  64. qcanvas/util/layouts.py +5 -2
  65. qcanvas/util/settings/_mapped_setting.py +6 -1
  66. qcanvas/util/themes/__init__.py +2 -0
  67. qcanvas/util/themes/_colour_scheme_helper.py +38 -0
  68. qcanvas/util/themes/_selected_theme.py +10 -0
  69. qcanvas/util/themes/_theme_changed_event.py +17 -0
  70. qcanvas/util/themes/_theme_changer.py +86 -0
  71. qcanvas/util/ui_tools.py +5 -1
  72. {qcanvas-1.2.0a0.dist-info → qcanvas-1.2.1.dist-info}/METADATA +11 -5
  73. qcanvas-1.2.1.dist-info/RECORD +118 -0
  74. qcanvas/icons/sync.svg +0 -7
  75. qcanvas/util/themes.py +0 -33
  76. qcanvas-1.2.0a0.dist-info/RECORD +0 -75
  77. /qcanvas/icons/{logo-transparent-light.svg → light/branding/logo_transparent.svg} +0 -0
  78. /qcanvas/icons/{main_icon.svg → universal/branding/main_icon.svg} +0 -0
  79. /qcanvas/icons/{file-download-failed.svg → universal/downloads/download_failed.svg} +0 -0
  80. /qcanvas/icons/{file-not-downloaded.svg → universal/downloads/not_downloaded.svg} +0 -0
  81. /qcanvas/icons/{file-unknown.svg → universal/downloads/unknown.svg} +0 -0
  82. {qcanvas-1.2.0a0.dist-info → qcanvas-1.2.1.dist-info}/WHEEL +0 -0
  83. {qcanvas-1.2.0a0.dist-info → qcanvas-1.2.1.dist-info}/entry_points.txt +0 -0
@@ -13,7 +13,7 @@
13
13
  <path d="m5 1030.4c-1.1046 0-2 0.9-2 2v8 4 6c0 1.1 0.8954 2 2 2h14c1.105 0 2-0.9 2-2v-6-4-4l-6-6h-10z"
14
14
  fill="#35a413"/>
15
15
  <path d="m5 1029.4c-1.1046 0-2 0.9-2 2v8 4 6c0 1.1 0.8954 2 2 2h14c1.105 0 2-0.9 2-2v-6-4-4l-6-6h-10z"
16
- fill="#7dec5b"/>
16
+ fill="#6eea48"/>
17
17
  <path d="m21 1035.4-6-6v4c0 1.1 0.895 2 2 2h4z" fill="#35a413"/>
18
18
  <path d="m6 8v1h12v-1h-12zm0 3v1h12v-1h-12zm0 3v1h12v-1h-12zm0 3v1h12v-1h-12z"
19
19
  transform="translate(0 1028.4)" fill="#35a413"/>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1e7bff">
2
+ <path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h168q13-36 43.5-58t68.5-22q38 0 68.5 22t43.5 58h168q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm80-80h280v-80H280v80Zm0-160h400v-80H280v80Zm0-160h400v-80H280v80Zm200-190q13 0 21.5-8.5T510-820q0-13-8.5-21.5T480-850q-13 0-21.5 8.5T450-820q0 13 8.5 21.5T480-790ZM200-200v-560 560Z"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1e7bff">
2
+ <path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm320-280L160-640v400h640v-400L480-440Zm0-80 320-200H160l320 200ZM160-640v-80 480-400Z"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1e7bff">
2
+ <path d="M320-240h320v-80H320v80Zm0-160h320v-80H320v80ZM240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h320l240 240v480q0 33-23.5 56.5T720-80H240Zm280-520v-200H240v640h480v-440H520ZM240-800v200-200 640-640Z"/>
3
+ </svg>
@@ -0,0 +1,108 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
4
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
5
+ viewBox="0 0 127.17 127.21"
6
+ version="1.1"
7
+ id="svg16"
8
+ sodipodi:docname="semester.svg"
9
+ inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
10
+ xmlns="http://www.w3.org/2000/svg">
11
+ <sodipodi:namedview
12
+ id="namedview16"
13
+ pagecolor="#ffffff"
14
+ bordercolor="#000000"
15
+ borderopacity="0.25"
16
+ inkscape:showpageshadow="2"
17
+ inkscape:pageopacity="0.0"
18
+ inkscape:pagecheckerboard="0"
19
+ inkscape:deskcolor="#d1d1d1"
20
+ inkscape:zoom="6.4538952"
21
+ inkscape:cx="63.605"
22
+ inkscape:cy="63.605"
23
+ inkscape:window-width="1920"
24
+ inkscape:window-height="1018"
25
+ inkscape:window-x="0"
26
+ inkscape:window-y="0"
27
+ inkscape:window-maximized="1"
28
+ inkscape:current-layer="svg16"/>
29
+ <defs
30
+ id="defs1">
31
+ <style
32
+ id="style1">.cls-1{fill:#e72429;}
33
+ </style>
34
+ </defs>
35
+ <g
36
+ id="Layer_2"
37
+ data-name="Layer 2"
38
+ transform="matrix(0.91348418,0,0,0.91348418,5.5011087,5.502839)">
39
+ <g
40
+ id="Layer_1-2"
41
+ data-name="Layer 1">
42
+ <path
43
+ class="cls-1"
44
+ d="M 18.45,63.47 A 18.4,18.4 0 0 0 2.31,45.23 73.13,73.13 0 0 0 2.31,81.7 18.38,18.38 0 0 0 18.45,63.47"
45
+ id="path1"/>
46
+ <path
47
+ class="cls-1"
48
+ d="M 29.13,57.7 A 5.77,5.77 0 1 0 34.9,63.47 5.77,5.77 0 0 0 29.13,57.7"
49
+ id="path2"/>
50
+ <path
51
+ class="cls-1"
52
+ d="m 108.72,63.47 a 18.38,18.38 0 0 0 16.14,18.23 73.13,73.13 0 0 0 0,-36.47 18.4,18.4 0 0 0 -16.14,18.24"
53
+ id="path3"/>
54
+ <path
55
+ class="cls-1"
56
+ d="m 98,57.7 a 5.77,5.77 0 1 0 5.76,5.77 A 5.77,5.77 0 0 0 98,57.7"
57
+ id="path4"/>
58
+ <path
59
+ class="cls-1"
60
+ d="m 63.46,108.77 a 18.39,18.39 0 0 0 -18.23,16.13 73.13,73.13 0 0 0 36.47,0 18.38,18.38 0 0 0 -18.24,-16.13"
61
+ id="path5"/>
62
+ <path
63
+ class="cls-1"
64
+ d="m 63.47,92.31 a 5.77,5.77 0 1 0 5.76,5.77 5.77,5.77 0 0 0 -5.76,-5.77"
65
+ id="path6"/>
66
+ <path
67
+ class="cls-1"
68
+ d="M 63.47,18.44 A 18.37,18.37 0 0 0 81.7,2.31 a 73.13,73.13 0 0 0 -36.47,0 18.39,18.39 0 0 0 18.24,16.13"
69
+ id="path7"/>
70
+ <path
71
+ class="cls-1"
72
+ d="m 63.47,23.37 a 5.77,5.77 0 1 0 5.76,5.76 5.76,5.76 0 0 0 -5.76,-5.76"
73
+ id="path8"/>
74
+ <path
75
+ class="cls-1"
76
+ d="m 95.44,95.44 a 18.4,18.4 0 0 0 -1.5,24.29 73,73 0 0 0 25.78,-25.79 18.39,18.39 0 0 0 -24.28,1.5"
77
+ id="path9"/>
78
+ <path
79
+ class="cls-1"
80
+ d="m 83.8,83.8 a 5.77,5.77 0 1 0 8.16,0 5.78,5.78 0 0 0 -8.16,0"
81
+ id="path10"/>
82
+ <path
83
+ class="cls-1"
84
+ d="M 31.59,31.59 A 18.36,18.36 0 0 0 33.09,7.31 72.93,72.93 0 0 0 7.31,33.09 18.36,18.36 0 0 0 31.59,31.59"
85
+ id="path11"/>
86
+ <path
87
+ class="cls-1"
88
+ d="m 35.07,35.08 a 5.77,5.77 0 1 0 8.16,0 5.78,5.78 0 0 0 -8.16,0"
89
+ id="path12"/>
90
+ <path
91
+ class="cls-1"
92
+ d="M 95.4,31.53 A 18.39,18.39 0 0 0 119.69,33 72.88,72.88 0 0 0 93.9,7.25 18.39,18.39 0 0 0 95.4,31.53"
93
+ id="path13"/>
94
+ <path
95
+ class="cls-1"
96
+ d="m 91.92,43.17 a 5.76,5.76 0 1 0 -8.15,0 5.76,5.76 0 0 0 8.15,0"
97
+ id="path14"/>
98
+ <path
99
+ class="cls-1"
100
+ d="M 31.56,95.37 A 18.39,18.39 0 0 0 7.28,93.87 73,73 0 0 0 33.06,119.66 18.38,18.38 0 0 0 31.56,95.37"
101
+ id="path15"/>
102
+ <path
103
+ class="cls-1"
104
+ d="m 35,83.73 a 5.77,5.77 0 1 0 8.16,0 5.79,5.79 0 0 0 -8.16,0"
105
+ id="path16"/>
106
+ </g>
107
+ </g>
108
+ </svg>
qcanvas/run.py CHANGED
@@ -23,7 +23,7 @@
23
23
  # nuitka-project: --nofollow-import-to=yt_dlp.extractor.lazy_extractors
24
24
 
25
25
  import logging
26
- from logging import DEBUG, INFO, WARNING
26
+ from logging import INFO, WARNING
27
27
 
28
28
  import qcanvas.app_start
29
29
  from qcanvas.util import logs, paths
@@ -42,7 +42,6 @@ logs.set_levels(
42
42
  "qcanvas.ui": WARNING,
43
43
  "qcanvas_backend": INFO,
44
44
  "qcanvas.ui.main_ui.status_bar_progress_display": INFO,
45
- "qcanvas.util.themes": DEBUG,
46
45
  }
47
46
  )
48
47
 
@@ -7,7 +7,8 @@ from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
7
  from qtpy.QtCore import QItemSelection, Signal, Slot
8
8
  from qtpy.QtWidgets import *
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__)
@@ -47,6 +48,7 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
47
48
  indentation: int = 20,
48
49
  max_width: Optional[int] = None,
49
50
  min_width: Optional[int] = None,
51
+ alternating_row_colours: bool = False,
50
52
  ) -> None:
51
53
  if not isinstance(header_text, str) and isinstance(header_text, Sequence):
52
54
  self.setHeaderLabels(header_text)
@@ -61,6 +63,8 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
61
63
  if min_width is not None:
62
64
  self.setMinimumWidth(min_width)
63
65
 
66
+ self.setAlternatingRowColors(alternating_row_colours)
67
+
64
68
  def set_columns_resize_mode(
65
69
  self,
66
70
  resize_mode_for_columns: list[QHeaderView.ResizeMode],
@@ -94,7 +98,7 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
94
98
  @abstractmethod
95
99
  def create_tree_items(
96
100
  self, data: T, sync_receipt: SyncReceipt
97
- ) -> Sequence[MemoryTreeWidgetItem]: ...
101
+ ) -> Sequence[QTreeWidgetItem]: ...
98
102
 
99
103
  @Slot(QItemSelection, QItemSelection)
100
104
  def _selection_changed(self, _0: QItemSelection, _1: QItemSelection) -> None:
@@ -110,7 +114,7 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
110
114
  if self.is_unseen(selected):
111
115
  self.mark_as_seen(selected)
112
116
 
113
- if not isinstance(selected, MemoryTreeWidgetItem):
117
+ if not isinstance(selected, AnyTreeDataItem):
114
118
  self._clear_selection()
115
119
  return
116
120
 
@@ -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 qtpy.QtCore import QByteArray
7
+ from qtpy.QtGui import QColor, QPainter, QPixmap
8
+ from qtpy.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
+ )
@@ -5,15 +5,20 @@ 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, Signal
7
7
 
8
+ from qcanvas import icons
8
9
  from qcanvas.ui.course_viewer.content_tree import ContentTree
10
+ from qcanvas.ui.course_viewer.course_tree._course_icon_generator import (
11
+ CourseIconGenerator,
12
+ )
13
+ from qcanvas.ui.course_viewer.tree_widget_data_item import TreeWidgetDataItem
9
14
  from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
10
15
 
11
16
  _logger = logging.getLogger(__name__)
12
17
 
13
18
 
14
- class _CourseTreeItem(MemoryTreeWidgetItem):
19
+ class _CourseTreeItem(TreeWidgetDataItem):
15
20
  def __init__(self, course: db.Course, owner: "CourseTree"):
16
- MemoryTreeWidgetItem.__init__(
21
+ TreeWidgetDataItem.__init__(
17
22
  self,
18
23
  id=course.id,
19
24
  data=course,
@@ -49,7 +54,9 @@ class CourseTree(ContentTree[Sequence[db.Term]]):
49
54
  def __init__(self):
50
55
  super().__init__("course_tree", emit_selection_signal_for_type=db.Course)
51
56
 
52
- self.ui_setup(max_width=250, min_width=150, header_text="Courses")
57
+ self.ui_setup(
58
+ max_width=250, min_width=150, header_text="Courses", indentation=15
59
+ )
53
60
 
54
61
  def create_tree_items(
55
62
  self, terms: List[db.Term], sync_receipt: SyncReceipt
@@ -58,10 +65,12 @@ class CourseTree(ContentTree[Sequence[db.Term]]):
58
65
 
59
66
  for term in reversed(terms):
60
67
  term_widget = self._create_term_widget(term)
68
+ course_icon_generator = CourseIconGenerator(term.id)
61
69
 
62
70
  for course in term.courses:
63
- course_widget = self._create_course_widget(course, sync_receipt)
64
- # course_widget.renamed.connect(self._on_course_renamed)
71
+ course_widget = self._create_course_widget(
72
+ course, course_icon_generator, sync_receipt
73
+ )
65
74
  term_widget.addChild(course_widget)
66
75
 
67
76
  widgets.append(term_widget)
@@ -69,9 +78,13 @@ class CourseTree(ContentTree[Sequence[db.Term]]):
69
78
  return widgets
70
79
 
71
80
  def _create_course_widget(
72
- self, course: db.Course, sync_receipt: SyncReceipt
81
+ self,
82
+ course: db.Course,
83
+ course_icon_generator: CourseIconGenerator,
84
+ sync_receipt: SyncReceipt,
73
85
  ) -> _CourseTreeItem:
74
86
  course_widget = _CourseTreeItem(course, self)
87
+ course_widget.setIcon(0, course_icon_generator.get_icon())
75
88
 
76
89
  if sync_receipt.was_updated(course):
77
90
  self.mark_as_unseen(course_widget)
@@ -81,5 +94,6 @@ class CourseTree(ContentTree[Sequence[db.Term]]):
81
94
  def _create_term_widget(self, term: db.Term) -> MemoryTreeWidgetItem:
82
95
  term_widget = MemoryTreeWidgetItem(id=term.id, data=term, strings=[term.name])
83
96
  term_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
97
+ term_widget.setIcon(0, icons.tree_items.semester)
84
98
 
85
99
  return term_widget
@@ -1,13 +1,15 @@
1
1
  import logging
2
+ from dataclasses import dataclass
2
3
 
3
4
  import qcanvas_backend.database.types as db
4
5
  from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
5
6
  from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
7
  from qtpy.QtCore import Slot
8
+ from qtpy.QtGui import QIcon
7
9
  from qtpy.QtWidgets import *
8
10
 
11
+ from qcanvas import icons
9
12
  from qcanvas.ui.course_viewer.tabs.assignment_tab import AssignmentTab
10
- from qcanvas.ui.course_viewer.tabs.file_tab import FileTab
11
13
  from qcanvas.ui.course_viewer.tabs.mail_tab import MailTab
12
14
  from qcanvas.ui.course_viewer.tabs.page_tab import PageTab
13
15
  from qcanvas.util.basic_fonts import bold_font
@@ -17,17 +19,25 @@ from qcanvas.util.ui_tools import make_truncatable
17
19
  _logger = logging.getLogger(__name__)
18
20
 
19
21
 
22
+ @dataclass
23
+ class _Tab:
24
+ icon: QIcon
25
+ highlighted_icon: QIcon
26
+
27
+
20
28
  class CourseViewer(QWidget):
29
+
21
30
  def __init__(
22
31
  self,
23
32
  course: db.Course,
24
33
  downloader: ResourceManager,
25
34
  *,
26
- sync_receipt: SyncReceipt
35
+ sync_receipt: SyncReceipt,
27
36
  ):
28
37
  super().__init__()
29
38
  # todo this is a mess. there are several other messes like this too, do they all have to be a mess?
30
39
  self._course_id = course.id
40
+ self._previous_tab_index = 0
31
41
 
32
42
  self._course_label = QLabel(course.name)
33
43
  self._course_label.setFont(bold_font)
@@ -48,28 +58,54 @@ class CourseViewer(QWidget):
48
58
  downloader=downloader,
49
59
  sync_receipt=sync_receipt,
50
60
  )
51
- self._files_tab = FileTab.create_from_receipt(
52
- course=course,
53
- downloader=downloader,
54
- sync_receipt=sync_receipt,
61
+ # self._files_tab = FileTab.create_from_receipt(
62
+ # course=course,
63
+ # downloader=downloader,
64
+ # sync_receipt=sync_receipt,
65
+ # )
66
+
67
+ self._tab_widget = QTabWidget()
68
+ self._tabs: dict[int, _Tab] = {}
69
+
70
+ # self._setup_tab(
71
+ # name="Files",
72
+ # widget=self._files_tab,
73
+ # icon=icons.tabs.pages,
74
+ # highlighted_icon=icons.tabs.pages_new_content,
75
+ # )
76
+ self._PAGES_TAB = self._setup_tab(
77
+ name="Pages",
78
+ widget=self._pages_tab,
79
+ icon=icons.tabs.pages,
80
+ highlighted_icon=icons.tabs.pages_new_content,
81
+ )
82
+ self._ASSIGNMENTS_TAB = self._setup_tab(
83
+ name="Assignments",
84
+ widget=self._assignments_tab,
85
+ icon=icons.tabs.assignments,
86
+ highlighted_icon=icons.tabs.assignments_new_content,
87
+ )
88
+ self._MAIL_TAB = self._setup_tab(
89
+ name="Mail",
90
+ widget=self._mail_tab,
91
+ icon=icons.tabs.mail,
92
+ highlighted_icon=icons.tabs.mail_new_content,
55
93
  )
56
-
57
- self._tabs = QTabWidget()
58
- self._tabs.addTab(self._files_tab, "Files")
59
- self._tabs.addTab(self._pages_tab, "Pages")
60
- self._tabs.addTab(self._assignments_tab, "Assignments")
61
- self._tabs.addTab(self._mail_tab, "Mail")
62
94
  # self._tabs.addTab(QLabel("Not implemented"), "Panopto") # The meme lives on!
63
95
 
64
- self.setLayout(layout(QVBoxLayout, self._course_label, self._tabs))
65
-
66
- self._tabs.currentChanged.connect(self._tab_changed)
67
-
96
+ self.setLayout(layout(QVBoxLayout, self._course_label, self._tab_widget))
97
+ self._tab_widget.currentChanged.connect(self._tab_changed)
68
98
  self._highlight_tabs(sync_receipt)
69
- self._unhighlight_tab(0) # Because the first tab always gets auto-selected
99
+
100
+ def _setup_tab(
101
+ self, widget: QWidget, icon: QIcon, highlighted_icon: QIcon, name: str
102
+ ) -> int:
103
+ index = self._tab_widget.addTab(widget, icon, name)
104
+ self._tabs[index] = _Tab(icon, highlighted_icon)
105
+ return index
70
106
 
71
107
  def reload(self, course: db.Course, *, sync_receipt: SyncReceipt) -> None:
72
- self._files_tab.reload(course, sync_receipt=sync_receipt)
108
+ # self._files_tab.reload(course, sync_receipt=sync_receipt)
73
109
  self._pages_tab.reload(course, sync_receipt=sync_receipt)
74
110
  self._assignments_tab.reload(course, sync_receipt=sync_receipt)
75
111
  self._mail_tab.reload(course, sync_receipt=sync_receipt)
@@ -77,32 +113,38 @@ class CourseViewer(QWidget):
77
113
 
78
114
  @Slot(int)
79
115
  def _tab_changed(self, index: int) -> None:
116
+ _logger.debug(f"Index = {index}")
80
117
  if index != -1:
81
- self._unhighlight_tab(index)
118
+ _logger.debug(f"Previous tab = {self._previous_tab_index}")
119
+ self._unhighlight_tab(self._previous_tab_index)
120
+ self._previous_tab_index = index
82
121
 
83
122
  def _highlight_tabs(self, sync_receipt: SyncReceipt) -> None:
84
123
  updates = sync_receipt.updates_by_course.get(self._course_id, None)
85
124
 
86
125
  if updates is not None:
87
- if len(updates.updated_resources) > 0:
88
- self._highlight_tab(0)
89
-
90
126
  if len(updates.updated_pages) > 0:
91
- self._highlight_tab(1)
127
+ self._highlight_tab(self._PAGES_TAB)
128
+ else:
129
+ self._unhighlight_tab(self._PAGES_TAB)
92
130
 
93
131
  if len(updates.updated_assignments) > 0:
94
- self._highlight_tab(2)
132
+ self._highlight_tab(self._ASSIGNMENTS_TAB)
133
+ else:
134
+ self._unhighlight_tab(self._ASSIGNMENTS_TAB)
95
135
 
96
136
  if len(updates.updated_messages) > 0:
97
- self._highlight_tab(3)
137
+ self._highlight_tab(self._MAIL_TAB)
138
+ else:
139
+ self._unhighlight_tab(self._MAIL_TAB)
98
140
  else:
99
- for index in range(0, 4):
141
+ for index in range(0, len(self._tabs)):
100
142
  self._unhighlight_tab(index)
101
143
 
102
144
  def _highlight_tab(self, tab_index: int) -> None:
103
- self._tabs.setTabText(tab_index, "* " + self._tabs.tabText(tab_index))
145
+ tab = self._tabs[tab_index]
146
+ self._tab_widget.setTabIcon(tab_index, tab.highlighted_icon)
104
147
 
105
148
  def _unhighlight_tab(self, tab_index: int) -> None:
106
- self._tabs.setTabText(
107
- tab_index, self._tabs.tabText(tab_index).replace("* ", "")
108
- )
149
+ tab = self._tabs[tab_index]
150
+ self._tab_widget.setTabIcon(tab_index, tab.icon)
@@ -6,7 +6,9 @@ from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
6
6
  from qtpy.QtCore import Qt
7
7
  from qtpy.QtWidgets import QHeaderView
8
8
 
9
+ from qcanvas import icons
9
10
  from qcanvas.ui.course_viewer.content_tree import ContentTree
11
+ from qcanvas.ui.course_viewer.tree_widget_data_item import TreeWidgetDataItem
10
12
  from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
11
13
 
12
14
  _logger = logging.getLogger(__name__)
@@ -63,19 +65,21 @@ class AssignmentTree(ContentTree[db.Course]):
63
65
  )
64
66
 
65
67
  assignment_group_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
68
+ assignment_group_widget.setIcon(0, icons.tree_items.module)
66
69
 
67
70
  return assignment_group_widget
68
71
 
69
72
  def _create_assignment_widget(
70
73
  self, assignment: db.Assignment, sync_receipt: SyncReceipt
71
- ) -> MemoryTreeWidgetItem:
72
- assignment_widget = MemoryTreeWidgetItem(
74
+ ) -> TreeWidgetDataItem:
75
+ assignment_widget = TreeWidgetDataItem(
73
76
  id=assignment.id, data=assignment, strings=[assignment.name]
74
77
  )
75
78
 
76
79
  assignment_widget.setFlags(
77
80
  Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
78
81
  )
82
+ assignment_widget.setIcon(0, icons.tree_items.assignment)
79
83
 
80
84
  if sync_receipt.was_updated(assignment):
81
85
  self.mark_as_unseen(assignment_widget)
@@ -8,6 +8,10 @@ from qtpy.QtCore import QPoint, Qt, Slot
8
8
  from qtpy.QtWidgets import *
9
9
 
10
10
  from qcanvas.ui.course_viewer.content_tree import ContentTree
11
+ from qcanvas.ui.course_viewer.tree_widget_data_item import (
12
+ AnyTreeDataItem,
13
+ TreeWidgetDataItem,
14
+ )
11
15
  from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
12
16
  from qcanvas.util.file_icons import icon_for_filename
13
17
  from qcanvas.util.ui_tools import create_qaction
@@ -63,8 +67,10 @@ class FileTree(ContentTree[db.Course]):
63
67
  # fixme the reesource widget items shouls NOT be a memory widget item because they can't be collapsed, but
64
68
  # mostly because the same file can appear in the tree multiple times in different places, which memory tree
65
69
  # can NOT deal with!
66
- item_widget = QTreeWidgetItem(
67
- [resource.file_name, str(resource.discovery_date.date())],
70
+ item_widget = TreeWidgetDataItem(
71
+ id=resource.id,
72
+ data=resource,
73
+ strings=[resource.file_name, str(resource.discovery_date.date())],
68
74
  )
69
75
  item_widget.setIcon(
70
76
  0,
@@ -81,15 +87,13 @@ class FileTree(ContentTree[db.Course]):
81
87
  def _context_menu(self, point: QPoint) -> None:
82
88
  item = self.itemAt(point)
83
89
 
84
- if item is None or not isinstance(item, MemoryTreeWidgetItem):
85
- return
90
+ if isinstance(item, AnyTreeDataItem):
91
+ menu = QMenu()
92
+ create_qaction(
93
+ name="Test",
94
+ parent=menu,
95
+ triggered=lambda: print(f"Clicked {item.extra_data.file_name}"),
96
+ )
97
+ menu.addAction("Another thing")
86
98
 
87
- menu = QMenu()
88
- create_qaction(
89
- name="Test",
90
- parent=menu,
91
- triggered=lambda: print(f"Clicked {item.extra_data.file_name}"),
92
- )
93
- menu.addAction("Another thing")
94
-
95
- menu.exec(self.mapToGlobal(point))
99
+ menu.exec(self.mapToGlobal(point))
@@ -27,24 +27,30 @@ class PagesFileTree(FileTree):
27
27
  if len(group.content_items) == 0:
28
28
  continue
29
29
 
30
- group_widget = self._create_group_widget(group, sync_receipt)
30
+ # Init group_widget lazily to prevent creating pointless tree widgets
31
+ group_widget: MemoryTreeWidgetItem | None = None
31
32
  items_in_group = set()
32
33
 
33
34
  for item in group.content_items:
34
- for resource in item.resources: # type: db.Resource
35
+ resource_widgets = []
35
36
 
37
+ for resource in item.resources: # type: db.Resource
36
38
  if resource.id not in items_in_group:
37
39
  items_in_group.add(resource.id)
38
- else:
39
- continue
40
40
 
41
- resource_widget = self._create_resource_widget(
42
- resource, sync_receipt
43
- )
41
+ if group_widget is None:
42
+ group_widget = self._create_group_widget(
43
+ group, sync_receipt
44
+ )
45
+
46
+ resource_widgets.append(
47
+ self._create_resource_widget(resource, sync_receipt)
48
+ )
44
49
 
45
- group_widget.addChild(resource_widget)
50
+ if len(resource_widgets) > 0:
51
+ group_widget.addChildren(resource_widgets)
46
52
 
47
- if group_widget.childCount() > 0:
53
+ if group_widget is not None:
48
54
  widgets.append(group_widget)
49
55
 
50
56
  return widgets