qcanvas 1.0.12.dev3__py3-none-any.whl → 1.2.0__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/app_start/__init__.py +6 -1
  2. qcanvas/icons/__init__.py +55 -6
  3. qcanvas/icons/_icon_type.py +42 -0
  4. qcanvas/icons/_update_icons.py +89 -0
  5. qcanvas/icons/dark/actions/exit.svg +3 -0
  6. qcanvas/icons/dark/actions/mark_all_read.svg +3 -0
  7. qcanvas/icons/dark/actions/open_downloads.svg +3 -0
  8. qcanvas/icons/dark/actions/quick_login.svg +3 -0
  9. qcanvas/icons/dark/actions/sync.svg +3 -0
  10. qcanvas/icons/dark/branding/logo_transparent.svg +303 -0
  11. qcanvas/icons/dark/options/auto_download.svg +3 -0
  12. qcanvas/icons/dark/options/theme.svg +3 -0
  13. qcanvas/icons/dark/tabs/assignments.svg +3 -0
  14. qcanvas/icons/dark/tabs/mail.svg +3 -0
  15. qcanvas/icons/dark/tabs/pages.svg +3 -0
  16. qcanvas/icons/dark/tree_items/assignment.svg +3 -0
  17. qcanvas/icons/dark/tree_items/mail.svg +3 -0
  18. qcanvas/icons/dark/tree_items/module.svg +3 -0
  19. qcanvas/icons/dark/tree_items/page.svg +3 -0
  20. qcanvas/icons/icons.qrc +44 -8
  21. qcanvas/icons/light/actions/exit.svg +3 -0
  22. qcanvas/icons/light/actions/mark_all_read.svg +3 -0
  23. qcanvas/icons/light/actions/open_downloads.svg +3 -0
  24. qcanvas/icons/light/actions/quick_login.svg +3 -0
  25. qcanvas/icons/light/actions/sync.svg +3 -0
  26. qcanvas/icons/light/branding/logo_transparent.svg +304 -0
  27. qcanvas/icons/light/options/auto_download.svg +3 -0
  28. qcanvas/icons/light/options/ignore_old.svg +3 -0
  29. qcanvas/icons/light/options/include_videos.svg +3 -0
  30. qcanvas/icons/light/options/theme.svg +3 -0
  31. qcanvas/icons/light/tabs/assignments.svg +3 -0
  32. qcanvas/icons/light/tabs/mail.svg +3 -0
  33. qcanvas/icons/light/tabs/pages.svg +3 -0
  34. qcanvas/icons/light/tree_items/assignment.svg +3 -0
  35. qcanvas/icons/light/tree_items/mail.svg +3 -0
  36. qcanvas/icons/light/tree_items/module.svg +3 -0
  37. qcanvas/icons/light/tree_items/page.svg +3 -0
  38. qcanvas/icons/rc_icons.py +2165 -355
  39. qcanvas/icons/universal/downloads/download_failed.svg +23 -0
  40. qcanvas/icons/universal/downloads/downloaded.svg +23 -0
  41. qcanvas/icons/universal/downloads/not_downloaded.svg +23 -0
  42. qcanvas/icons/universal/tabs/assignments_new_content.svg +3 -0
  43. qcanvas/icons/universal/tabs/mail_new_content.svg +3 -0
  44. qcanvas/icons/universal/tabs/pages_new_content.svg +3 -0
  45. qcanvas/icons/universal/tree_items/semester.svg +108 -0
  46. qcanvas/run.py +24 -0
  47. qcanvas/ui/course_viewer/content_tree.py +28 -7
  48. qcanvas/ui/course_viewer/course_tree/__init__.py +1 -0
  49. qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +86 -0
  50. qcanvas/ui/course_viewer/{course_tree.py → course_tree/course_tree.py} +20 -6
  51. qcanvas/ui/course_viewer/course_viewer.py +71 -24
  52. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +15 -14
  53. qcanvas/ui/course_viewer/tabs/file_tab/__init__.py +1 -0
  54. qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +46 -0
  55. qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +99 -0
  56. qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +56 -0
  57. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +11 -11
  58. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +13 -11
  59. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +57 -56
  60. qcanvas/ui/course_viewer/tabs/util.py +10 -0
  61. qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
  62. qcanvas/ui/main_ui/course_viewer_container.py +46 -3
  63. qcanvas/ui/main_ui/options/auto_download_resources_option.py +3 -1
  64. qcanvas/ui/main_ui/options/theme_selection_menu.py +2 -0
  65. qcanvas/ui/main_ui/qcanvas_window.py +18 -5
  66. qcanvas/ui/main_ui/status_bar_progress_display.py +17 -8
  67. qcanvas/ui/memory_tree/_tree_memory.py +1 -0
  68. qcanvas/ui/memory_tree/memory_tree_widget.py +2 -2
  69. qcanvas/ui/setup/setup_checker.py +2 -2
  70. qcanvas/ui/setup/setup_dialog.py +145 -66
  71. qcanvas/util/__init__.py +0 -2
  72. qcanvas/util/auto_downloader.py +1 -2
  73. qcanvas/util/file_icons.py +54 -0
  74. qcanvas/util/html_cleaner.py +2 -0
  75. qcanvas/util/layouts.py +5 -2
  76. qcanvas/util/paths.py +15 -26
  77. qcanvas/util/runtime.py +20 -0
  78. qcanvas/util/settings/_client_settings.py +11 -2
  79. qcanvas/util/settings/_mapped_setting.py +6 -1
  80. qcanvas/util/themes/__init__.py +2 -0
  81. qcanvas/util/themes/_colour_scheme_helper.py +38 -0
  82. qcanvas/util/themes/_selected_theme.py +10 -0
  83. qcanvas/util/themes/_theme_changed_event.py +17 -0
  84. qcanvas/util/themes/_theme_changer.py +86 -0
  85. qcanvas/util/ui_tools.py +5 -1
  86. {qcanvas-1.0.12.dev3.dist-info → qcanvas-1.2.0.dist-info}/METADATA +16 -6
  87. qcanvas-1.2.0.dist-info/RECORD +118 -0
  88. qcanvas/icons/file-download-failed.svg +0 -6
  89. qcanvas/icons/file-downloaded.svg +0 -6
  90. qcanvas/icons/file-not-downloaded.svg +0 -6
  91. qcanvas/icons/sync.svg +0 -7
  92. qcanvas/util/themes.py +0 -24
  93. qcanvas-1.0.12.dev3.dist-info/RECORD +0 -68
  94. /qcanvas/icons/{main_icon.svg → universal/branding/main_icon.svg} +0 -0
  95. /qcanvas/icons/{file-unknown.svg → universal/downloads/unknown.svg} +0 -0
  96. {qcanvas-1.0.12.dev3.dist-info → qcanvas-1.2.0.dist-info}/WHEEL +0 -0
  97. {qcanvas-1.0.12.dev3.dist-info → qcanvas-1.2.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
2
+ <!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
3
+ <svg width="800px"
4
+ height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
5
+ version="1.1" fill="#000000">
6
+
7
+ <g id="SVGRepo_bgCarrier" stroke-width="0"/>
8
+
9
+ <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
10
+
11
+ <g id="SVGRepo_iconCarrier">
12
+ <g transform="translate(0 -1028.4)">
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
+ fill="#a31f1f"/>
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="#db4343"/>
17
+ <path d="m21 1035.4-6-6v4c0 1.1 0.895 2 2 2h4z" fill="#a31f1f"/>
18
+ <path d="m6 8v1h12v-1h-12zm0 3v1h12v-1h-12zm0 3v1h12v-1h-12zm0 3v1h12v-1h-12z"
19
+ transform="translate(0 1028.4)" fill="#a31f1f"/>
20
+ </g>
21
+ </g>
22
+
23
+ </svg>
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
2
+ <!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
3
+ <svg width="800px"
4
+ height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
5
+ version="1.1" fill="#000000">
6
+
7
+ <g id="SVGRepo_bgCarrier" stroke-width="0"/>
8
+
9
+ <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
10
+
11
+ <g id="SVGRepo_iconCarrier">
12
+ <g transform="translate(0 -1028.4)">
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
+ fill="#35a413"/>
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="#6eea48"/>
17
+ <path d="m21 1035.4-6-6v4c0 1.1 0.895 2 2 2h4z" fill="#35a413"/>
18
+ <path d="m6 8v1h12v-1h-12zm0 3v1h12v-1h-12zm0 3v1h12v-1h-12zm0 3v1h12v-1h-12z"
19
+ transform="translate(0 1028.4)" fill="#35a413"/>
20
+ </g>
21
+ </g>
22
+
23
+ </svg>
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
2
+ <!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
3
+ <svg width="800px"
4
+ height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
5
+ version="1.1" fill="#000000">
6
+
7
+ <g id="SVGRepo_bgCarrier" stroke-width="0"/>
8
+
9
+ <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
10
+
11
+ <g id="SVGRepo_iconCarrier">
12
+ <g transform="translate(0 -1028.4)">
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
+ fill="#74898b"/>
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="#bdc3c7"/>
17
+ <path d="m21 1035.4-6-6v4c0 1.1 0.895 2 2 2h4z" fill="#74898b"/>
18
+ <path d="m6 8v1h12v-1h-12zm0 3v1h12v-1h-12zm0 3v1h12v-1h-12zm0 3v1h12v-1h-12z"
19
+ transform="translate(0 1028.4)" fill="#74898b"/>
20
+ </g>
21
+ </g>
22
+
23
+ </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="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
@@ -1,3 +1,27 @@
1
+ # nuitka-project: --enable-plugin=pyside6
2
+ # nuitka-project: --windows-icon-from-ico=./deploy/windows/qcanvas.ico
3
+ # nuitka-project: --windows-console-mode=attach
4
+ # nuitka-project: --linux-icon=deploy/appimage/qcanvas.svg
5
+ # nuitka-project: --standalone
6
+ # nuitka-project: --include-module=yt_dlp
7
+
8
+ # Anti-bloat
9
+
10
+ ##########################################################
11
+ # nuitka-project: --include-package=aiosqlite
12
+ # nuitka-project: --nofollow-import-to=aiosqlite.tests.*
13
+ ##########################################################
14
+ # nuitka-project: --nofollow-import-to=rich
15
+ ##########################################################
16
+ # nuitka-project: --nofollow-import-to=sqlalchemy.dialects.mssql
17
+ # nuitka-project: --nofollow-import-to=sqlalchemy.dialects.postgresql
18
+ # nuitka-project: --nofollow-import-to=sqlalchemy.dialects.oracle
19
+ # nuitka-project: --nofollow-import-to=sqlalchemy.dialects.mysql
20
+ ##########################################################
21
+ # nuitka-project: --nofollow-import-to=rich
22
+ ##########################################################
23
+ # nuitka-project: --nofollow-import-to=yt_dlp.extractor.lazy_extractors
24
+
1
25
  import logging
2
26
  from logging import INFO, WARNING
3
27
 
@@ -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__)
@@ -45,8 +46,9 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
45
46
  *,
46
47
  header_text: str | Sequence[str],
47
48
  indentation: int = 20,
48
- max_width: int,
49
- min_width: int,
49
+ max_width: Optional[int] = None,
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)
@@ -54,8 +56,27 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
54
56
  self.setHeaderLabel(header_text)
55
57
 
56
58
  self.setIndentation(indentation)
57
- self.setMaximumWidth(max_width)
58
- self.setMinimumWidth(min_width)
59
+
60
+ if max_width is not None:
61
+ self.setMaximumWidth(max_width)
62
+
63
+ if min_width is not None:
64
+ self.setMinimumWidth(min_width)
65
+
66
+ self.setAlternatingRowColors(alternating_row_colours)
67
+
68
+ def set_columns_resize_mode(
69
+ self,
70
+ resize_mode_for_columns: list[QHeaderView.ResizeMode],
71
+ *,
72
+ stretch_last: bool = False,
73
+ ) -> None:
74
+ header = self.header()
75
+
76
+ for index, mode in enumerate(resize_mode_for_columns):
77
+ header.setSectionResizeMode(index, mode)
78
+
79
+ header.setStretchLastSection(stretch_last)
59
80
 
60
81
  def reload(self, data: T, *, sync_receipt: SyncReceipt) -> None:
61
82
  self._reloading = True
@@ -77,7 +98,7 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
77
98
  @abstractmethod
78
99
  def create_tree_items(
79
100
  self, data: T, sync_receipt: SyncReceipt
80
- ) -> Sequence[MemoryTreeWidgetItem]: ...
101
+ ) -> Sequence[QTreeWidgetItem]: ...
81
102
 
82
103
  @Slot(QItemSelection, QItemSelection)
83
104
  def _selection_changed(self, _0: QItemSelection, _1: QItemSelection) -> None:
@@ -93,7 +114,7 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
93
114
  if self.is_unseen(selected):
94
115
  self.mark_as_seen(selected)
95
116
 
96
- if not isinstance(selected, MemoryTreeWidgetItem):
117
+ if not isinstance(selected, AnyTreeDataItem):
97
118
  self._clear_selection()
98
119
  return
99
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,11 +1,14 @@
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
13
  from qcanvas.ui.course_viewer.tabs.mail_tab import MailTab
11
14
  from qcanvas.ui.course_viewer.tabs.page_tab import PageTab
@@ -16,17 +19,26 @@ from qcanvas.util.ui_tools import make_truncatable
16
19
  _logger = logging.getLogger(__name__)
17
20
 
18
21
 
22
+ @dataclass
23
+ class _Tab:
24
+ index: int
25
+ icon: QIcon
26
+ highlighted_icon: QIcon
27
+
28
+
19
29
  class CourseViewer(QWidget):
30
+
20
31
  def __init__(
21
32
  self,
22
33
  course: db.Course,
23
34
  downloader: ResourceManager,
24
35
  *,
25
- sync_receipt: SyncReceipt
36
+ sync_receipt: SyncReceipt,
26
37
  ):
27
38
  super().__init__()
28
39
  # todo this is a mess. there are several other messes like this too, do they all have to be a mess?
29
40
  self._course_id = course.id
41
+ self._previous_tab_index = 0
30
42
 
31
43
  self._course_label = QLabel(course.name)
32
44
  self._course_label.setFont(bold_font)
@@ -53,57 +65,92 @@ class CourseViewer(QWidget):
53
65
  # sync_receipt=sync_receipt,
54
66
  # )
55
67
 
56
- self._tabs = QTabWidget()
57
- self._tabs.addTab(self._pages_tab, "Pages")
58
- self._tabs.addTab(self._assignments_tab, "Assignments")
59
- self._tabs.addTab(self._mail_tab, "Mail")
60
- self._tabs.addTab(QLabel("Not implemented"), "Files")
61
- # self._tabs.addTab(self._files_tab, "Files")
68
+ self._tab_widget = QTabWidget()
69
+ self._tabs: dict[str, _Tab] = {}
70
+
71
+ # self._setup_tab(
72
+ # name="Files",
73
+ # widget=self._files_tab,
74
+ # icon=icons.tabs.pages,
75
+ # highlighted_icon=icons.tabs.pages_new_content,
76
+ # )
77
+ self._setup_tab(
78
+ name="Pages",
79
+ widget=self._pages_tab,
80
+ icon=icons.tabs.pages,
81
+ highlighted_icon=icons.tabs.pages_new_content,
82
+ )
83
+ self._setup_tab(
84
+ name="Assignments",
85
+ widget=self._assignments_tab,
86
+ icon=icons.tabs.assignments,
87
+ highlighted_icon=icons.tabs.assignments_new_content,
88
+ )
89
+ self._setup_tab(
90
+ name="Mail",
91
+ widget=self._mail_tab,
92
+ icon=icons.tabs.mail,
93
+ highlighted_icon=icons.tabs.mail_new_content,
94
+ )
62
95
  # self._tabs.addTab(QLabel("Not implemented"), "Panopto") # The meme lives on!
63
96
 
64
- self.setLayout(layout(QVBoxLayout, self._course_label, self._tabs))
97
+ self.setLayout(layout(QVBoxLayout, self._course_label, self._tab_widget))
65
98
 
66
- self._tabs.currentChanged.connect(self._tab_changed)
99
+ self._tab_widget.currentChanged.connect(self._tab_changed)
67
100
 
68
101
  self._highlight_tabs(sync_receipt)
69
- self._unhighlight_tab(0) # Because the first tab always gets auto-selected
102
+ # self._highlight_tab(
103
+ # self._tab_widget.tabText(0)
104
+ # ) # Because the first tab always gets auto-selected
105
+
106
+ def _setup_tab(
107
+ self, widget: QWidget, icon: QIcon, highlighted_icon: QIcon, name: str
108
+ ):
109
+ index = self._tab_widget.addTab(widget, icon, name)
110
+ self._tabs[name] = _Tab(index, icon, highlighted_icon)
70
111
 
71
112
  def reload(self, course: db.Course, *, sync_receipt: SyncReceipt) -> None:
113
+ # self._files_tab.reload(course, sync_receipt=sync_receipt)
72
114
  self._pages_tab.reload(course, sync_receipt=sync_receipt)
73
115
  self._assignments_tab.reload(course, sync_receipt=sync_receipt)
74
116
  self._mail_tab.reload(course, sync_receipt=sync_receipt)
75
-
76
117
  self._highlight_tabs(sync_receipt)
77
118
 
78
119
  @Slot(int)
79
120
  def _tab_changed(self, index: int) -> None:
80
121
  if index != -1:
81
- self._unhighlight_tab(index)
122
+ self._unhighlight_tab(self._tab_widget.tabText(self._previous_tab_index))
123
+ self._previous_tab_index = index
82
124
 
83
125
  def _highlight_tabs(self, sync_receipt: SyncReceipt) -> None:
84
126
  updates = sync_receipt.updates_by_course.get(self._course_id, None)
85
127
 
86
128
  if updates is not None:
87
129
  # if len(updates.updated_resources) > 0:
88
- # raise Exception("Looks like you forgot to update the other numbers??????"")
89
130
  # self._highlight_tab(0)
90
131
 
91
132
  if len(updates.updated_pages) > 0:
92
- self._highlight_tab(0)
133
+ self._highlight_tab("Pages")
134
+ else:
135
+ self._unhighlight_tab("Pages")
93
136
 
94
137
  if len(updates.updated_assignments) > 0:
95
- self._highlight_tab(1)
138
+ self._highlight_tab("Assignments")
139
+ else:
140
+ self._unhighlight_tab("Assignments")
96
141
 
97
142
  if len(updates.updated_messages) > 0:
98
- self._highlight_tab(2)
143
+ self._highlight_tab("Mail")
144
+ else:
145
+ self._unhighlight_tab("Mail")
99
146
  else:
100
- for index in range(0, 4):
101
- self._unhighlight_tab(index)
147
+ for tab_name in self._tabs.keys():
148
+ self._unhighlight_tab(tab_name)
102
149
 
103
- def _highlight_tab(self, tab_index: int) -> None:
104
- self._tabs.setTabText(tab_index, "* " + self._tabs.tabText(tab_index))
150
+ def _highlight_tab(self, tab_name: str) -> None:
151
+ tab = self._tabs[tab_name]
152
+ self._tab_widget.setTabIcon(tab.index, tab.highlighted_icon)
105
153
 
106
- def _unhighlight_tab(self, tab_index: int) -> None:
107
- self._tabs.setTabText(
108
- tab_index, self._tabs.tabText(tab_index).replace("* ", "")
109
- )
154
+ def _unhighlight_tab(self, tab_name: str) -> None:
155
+ tab = self._tabs[tab_name]
156
+ self._tab_widget.setTabIcon(tab.index, tab.icon)