qcanvas 1.1.0__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.
- qcanvas/app_start/__init__.py +6 -1
- qcanvas/icons/__init__.py +55 -6
- qcanvas/icons/_icon_type.py +42 -0
- qcanvas/icons/_update_icons.py +89 -0
- qcanvas/icons/dark/actions/exit.svg +3 -0
- qcanvas/icons/dark/actions/mark_all_read.svg +3 -0
- qcanvas/icons/dark/actions/open_downloads.svg +3 -0
- qcanvas/icons/dark/actions/quick_login.svg +3 -0
- qcanvas/icons/dark/actions/sync.svg +3 -0
- qcanvas/icons/dark/branding/logo_transparent.svg +303 -0
- qcanvas/icons/dark/options/auto_download.svg +3 -0
- qcanvas/icons/dark/options/theme.svg +3 -0
- qcanvas/icons/dark/tabs/assignments.svg +3 -0
- qcanvas/icons/dark/tabs/mail.svg +3 -0
- qcanvas/icons/dark/tabs/pages.svg +3 -0
- qcanvas/icons/dark/tree_items/assignment.svg +3 -0
- qcanvas/icons/dark/tree_items/mail.svg +3 -0
- qcanvas/icons/dark/tree_items/module.svg +3 -0
- qcanvas/icons/dark/tree_items/page.svg +3 -0
- qcanvas/icons/icons.qrc +44 -8
- qcanvas/icons/light/actions/exit.svg +3 -0
- qcanvas/icons/light/actions/mark_all_read.svg +3 -0
- qcanvas/icons/light/actions/open_downloads.svg +3 -0
- qcanvas/icons/light/actions/quick_login.svg +3 -0
- qcanvas/icons/light/actions/sync.svg +3 -0
- qcanvas/icons/light/branding/logo_transparent.svg +304 -0
- qcanvas/icons/light/options/auto_download.svg +3 -0
- qcanvas/icons/light/options/ignore_old.svg +3 -0
- qcanvas/icons/light/options/include_videos.svg +3 -0
- qcanvas/icons/light/options/theme.svg +3 -0
- qcanvas/icons/light/tabs/assignments.svg +3 -0
- qcanvas/icons/light/tabs/mail.svg +3 -0
- qcanvas/icons/light/tabs/pages.svg +3 -0
- qcanvas/icons/light/tree_items/assignment.svg +3 -0
- qcanvas/icons/light/tree_items/mail.svg +3 -0
- qcanvas/icons/light/tree_items/module.svg +3 -0
- qcanvas/icons/light/tree_items/page.svg +3 -0
- qcanvas/icons/rc_icons.py +2164 -349
- qcanvas/icons/{file-downloaded.svg → universal/downloads/downloaded.svg} +4 -4
- qcanvas/icons/universal/tabs/assignments_new_content.svg +3 -0
- qcanvas/icons/universal/tabs/mail_new_content.svg +3 -0
- qcanvas/icons/universal/tabs/pages_new_content.svg +3 -0
- qcanvas/icons/universal/tree_items/semester.svg +108 -0
- qcanvas/ui/course_viewer/content_tree.py +20 -3
- qcanvas/ui/course_viewer/course_tree/__init__.py +1 -0
- qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +86 -0
- qcanvas/ui/course_viewer/{course_tree.py → course_tree/course_tree.py} +20 -6
- qcanvas/ui/course_viewer/course_viewer.py +80 -23
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +15 -14
- qcanvas/ui/course_viewer/tabs/file_tab/__init__.py +1 -0
- qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +46 -0
- qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +99 -0
- qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +56 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +11 -11
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +13 -11
- qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +44 -52
- qcanvas/ui/course_viewer/tabs/util.py +10 -0
- qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
- qcanvas/ui/main_ui/course_viewer_container.py +46 -3
- qcanvas/ui/main_ui/options/auto_download_resources_option.py +3 -1
- qcanvas/ui/main_ui/options/theme_selection_menu.py +2 -0
- qcanvas/ui/main_ui/qcanvas_window.py +17 -4
- qcanvas/ui/main_ui/status_bar_progress_display.py +17 -8
- qcanvas/ui/memory_tree/_tree_memory.py +1 -0
- qcanvas/ui/memory_tree/memory_tree_widget.py +2 -2
- qcanvas/ui/setup/setup_dialog.py +1 -1
- qcanvas/util/file_icons.py +54 -0
- qcanvas/util/html_cleaner.py +2 -0
- qcanvas/util/layouts.py +5 -2
- qcanvas/util/settings/_mapped_setting.py +6 -1
- qcanvas/util/themes/__init__.py +2 -0
- qcanvas/util/themes/_colour_scheme_helper.py +38 -0
- qcanvas/util/themes/_selected_theme.py +10 -0
- qcanvas/util/themes/_theme_changed_event.py +17 -0
- qcanvas/util/themes/_theme_changer.py +86 -0
- qcanvas/util/ui_tools.py +5 -1
- {qcanvas-1.1.0.dist-info → qcanvas-1.2.0.dist-info}/METADATA +15 -5
- qcanvas-1.2.0.dist-info/RECORD +118 -0
- qcanvas/icons/sync.svg +0 -7
- qcanvas/util/themes.py +0 -27
- qcanvas-1.1.0.dist-info/RECORD +0 -69
- /qcanvas/icons/{main_icon.svg → universal/branding/main_icon.svg} +0 -0
- /qcanvas/icons/{file-download-failed.svg → universal/downloads/download_failed.svg} +0 -0
- /qcanvas/icons/{file-not-downloaded.svg → universal/downloads/not_downloaded.svg} +0 -0
- /qcanvas/icons/{file-unknown.svg → universal/downloads/unknown.svg} +0 -0
- {qcanvas-1.1.0.dist-info → qcanvas-1.2.0.dist-info}/WHEEL +0 -0
- {qcanvas-1.1.0.dist-info → qcanvas-1.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
<g id="SVGRepo_iconCarrier">
|
|
12
12
|
<g transform="translate(0 -1028.4)">
|
|
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
|
-
fill="#
|
|
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="#
|
|
17
|
-
<path d="m21 1035.4-6-6v4c0 1.1 0.895 2 2 2h4z" fill="#
|
|
16
|
+
fill="#6eea48"/>
|
|
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
|
-
transform="translate(0 1028.4)" fill="#
|
|
19
|
+
transform="translate(0 1028.4)" fill="#35a413"/>
|
|
20
20
|
</g>
|
|
21
21
|
</g>
|
|
22
22
|
|
|
@@ -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>
|
|
@@ -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.
|
|
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,21 @@ 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
|
+
|
|
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)
|
|
80
|
+
|
|
64
81
|
def reload(self, data: T, *, sync_receipt: SyncReceipt) -> None:
|
|
65
82
|
self._reloading = True
|
|
66
83
|
|
|
@@ -81,7 +98,7 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
|
|
|
81
98
|
@abstractmethod
|
|
82
99
|
def create_tree_items(
|
|
83
100
|
self, data: T, sync_receipt: SyncReceipt
|
|
84
|
-
) -> Sequence[
|
|
101
|
+
) -> Sequence[QTreeWidgetItem]: ...
|
|
85
102
|
|
|
86
103
|
@Slot(QItemSelection, QItemSelection)
|
|
87
104
|
def _selection_changed(self, _0: QItemSelection, _1: QItemSelection) -> None:
|
|
@@ -97,7 +114,7 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
|
|
|
97
114
|
if self.is_unseen(selected):
|
|
98
115
|
self.mark_as_seen(selected)
|
|
99
116
|
|
|
100
|
-
if not isinstance(selected,
|
|
117
|
+
if not isinstance(selected, AnyTreeDataItem):
|
|
101
118
|
self._clear_selection()
|
|
102
119
|
return
|
|
103
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(
|
|
19
|
+
class _CourseTreeItem(TreeWidgetDataItem):
|
|
15
20
|
def __init__(self, course: db.Course, owner: "CourseTree"):
|
|
16
|
-
|
|
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(
|
|
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(
|
|
64
|
-
|
|
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,
|
|
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)
|
|
@@ -47,53 +59,98 @@ class CourseViewer(QWidget):
|
|
|
47
59
|
downloader=downloader,
|
|
48
60
|
sync_receipt=sync_receipt,
|
|
49
61
|
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
# self._files_tab = FileTab.create_from_receipt(
|
|
63
|
+
# course=course,
|
|
64
|
+
# downloader=downloader,
|
|
65
|
+
# sync_receipt=sync_receipt,
|
|
66
|
+
# )
|
|
67
|
+
|
|
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
|
+
)
|
|
56
95
|
# self._tabs.addTab(QLabel("Not implemented"), "Panopto") # The meme lives on!
|
|
57
96
|
|
|
58
|
-
self.setLayout(layout(QVBoxLayout, self._course_label, self.
|
|
97
|
+
self.setLayout(layout(QVBoxLayout, self._course_label, self._tab_widget))
|
|
59
98
|
|
|
60
|
-
self.
|
|
99
|
+
self._tab_widget.currentChanged.connect(self._tab_changed)
|
|
61
100
|
|
|
62
101
|
self._highlight_tabs(sync_receipt)
|
|
63
|
-
self.
|
|
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)
|
|
64
111
|
|
|
65
112
|
def reload(self, course: db.Course, *, sync_receipt: SyncReceipt) -> None:
|
|
113
|
+
# self._files_tab.reload(course, sync_receipt=sync_receipt)
|
|
66
114
|
self._pages_tab.reload(course, sync_receipt=sync_receipt)
|
|
67
115
|
self._assignments_tab.reload(course, sync_receipt=sync_receipt)
|
|
68
116
|
self._mail_tab.reload(course, sync_receipt=sync_receipt)
|
|
69
|
-
|
|
70
117
|
self._highlight_tabs(sync_receipt)
|
|
71
118
|
|
|
72
119
|
@Slot(int)
|
|
73
120
|
def _tab_changed(self, index: int) -> None:
|
|
74
121
|
if index != -1:
|
|
75
|
-
self._unhighlight_tab(
|
|
122
|
+
self._unhighlight_tab(self._tab_widget.tabText(self._previous_tab_index))
|
|
123
|
+
self._previous_tab_index = index
|
|
76
124
|
|
|
77
125
|
def _highlight_tabs(self, sync_receipt: SyncReceipt) -> None:
|
|
78
126
|
updates = sync_receipt.updates_by_course.get(self._course_id, None)
|
|
79
127
|
|
|
80
128
|
if updates is not None:
|
|
129
|
+
# if len(updates.updated_resources) > 0:
|
|
130
|
+
# self._highlight_tab(0)
|
|
131
|
+
|
|
81
132
|
if len(updates.updated_pages) > 0:
|
|
82
|
-
self._highlight_tab(
|
|
133
|
+
self._highlight_tab("Pages")
|
|
134
|
+
else:
|
|
135
|
+
self._unhighlight_tab("Pages")
|
|
83
136
|
|
|
84
137
|
if len(updates.updated_assignments) > 0:
|
|
85
|
-
self._highlight_tab(
|
|
138
|
+
self._highlight_tab("Assignments")
|
|
139
|
+
else:
|
|
140
|
+
self._unhighlight_tab("Assignments")
|
|
86
141
|
|
|
87
142
|
if len(updates.updated_messages) > 0:
|
|
88
|
-
self._highlight_tab(
|
|
143
|
+
self._highlight_tab("Mail")
|
|
144
|
+
else:
|
|
145
|
+
self._unhighlight_tab("Mail")
|
|
89
146
|
else:
|
|
90
|
-
for
|
|
91
|
-
self._unhighlight_tab(
|
|
147
|
+
for tab_name in self._tabs.keys():
|
|
148
|
+
self._unhighlight_tab(tab_name)
|
|
92
149
|
|
|
93
|
-
def _highlight_tab(self,
|
|
94
|
-
|
|
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)
|
|
95
153
|
|
|
96
|
-
def _unhighlight_tab(self,
|
|
97
|
-
self._tabs
|
|
98
|
-
|
|
99
|
-
)
|
|
154
|
+
def _unhighlight_tab(self, tab_name: str) -> None:
|
|
155
|
+
tab = self._tabs[tab_name]
|
|
156
|
+
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__)
|
|
@@ -18,6 +20,7 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
18
20
|
tree_name=f"course.{course_id}.assignment_groups",
|
|
19
21
|
emit_selection_signal_for_type=db.Assignment,
|
|
20
22
|
)
|
|
23
|
+
|
|
21
24
|
self.ui_setup(
|
|
22
25
|
header_text=["Assignments", "Weight"],
|
|
23
26
|
indentation=15,
|
|
@@ -25,13 +28,9 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
25
28
|
min_width=150,
|
|
26
29
|
)
|
|
27
30
|
|
|
28
|
-
self.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
header = self.header()
|
|
32
|
-
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
33
|
-
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
|
34
|
-
header.setStretchLastSection(False)
|
|
31
|
+
self.set_columns_resize_mode(
|
|
32
|
+
[QHeaderView.ResizeMode.Stretch, QHeaderView.ResizeMode.ResizeToContents]
|
|
33
|
+
)
|
|
35
34
|
|
|
36
35
|
def create_tree_items(
|
|
37
36
|
self, course: db.Course, sync_receipt: SyncReceipt
|
|
@@ -39,8 +38,11 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
39
38
|
widgets = []
|
|
40
39
|
|
|
41
40
|
for assignment_group in course.assignment_groups: # type: db.AssignmentGroup
|
|
41
|
+
if len(assignment_group.assignments) == 0:
|
|
42
|
+
continue
|
|
43
|
+
|
|
42
44
|
assignment_group_widget = self._create_assignment_group_widget(
|
|
43
|
-
assignment_group
|
|
45
|
+
assignment_group
|
|
44
46
|
)
|
|
45
47
|
|
|
46
48
|
widgets.append(assignment_group_widget)
|
|
@@ -54,7 +56,7 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
54
56
|
return widgets
|
|
55
57
|
|
|
56
58
|
def _create_assignment_group_widget(
|
|
57
|
-
self, assignment_group: db.AssignmentGroup
|
|
59
|
+
self, assignment_group: db.AssignmentGroup
|
|
58
60
|
) -> MemoryTreeWidgetItem:
|
|
59
61
|
assignment_group_widget = MemoryTreeWidgetItem(
|
|
60
62
|
id=assignment_group.id,
|
|
@@ -63,22 +65,21 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
63
65
|
)
|
|
64
66
|
|
|
65
67
|
assignment_group_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
|
|
66
|
-
|
|
67
|
-
if sync_receipt.was_updated(assignment_group):
|
|
68
|
-
self.mark_as_unseen(assignment_group_widget)
|
|
68
|
+
assignment_group_widget.setIcon(0, icons.tree_items.module)
|
|
69
69
|
|
|
70
70
|
return assignment_group_widget
|
|
71
71
|
|
|
72
72
|
def _create_assignment_widget(
|
|
73
73
|
self, assignment: db.Assignment, sync_receipt: SyncReceipt
|
|
74
|
-
) ->
|
|
75
|
-
assignment_widget =
|
|
74
|
+
) -> TreeWidgetDataItem:
|
|
75
|
+
assignment_widget = TreeWidgetDataItem(
|
|
76
76
|
id=assignment.id, data=assignment, strings=[assignment.name]
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
assignment_widget.setFlags(
|
|
80
80
|
Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
|
|
81
81
|
)
|
|
82
|
+
assignment_widget.setIcon(0, icons.tree_items.assignment)
|
|
82
83
|
|
|
83
84
|
if sync_receipt.was_updated(assignment):
|
|
84
85
|
self.mark_as_unseen(assignment_widget)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .file_tab import FileTab
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import qcanvas_backend.database.types as db
|
|
4
|
+
from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
|
|
5
|
+
from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
|
|
6
|
+
from qtpy.QtWidgets import *
|
|
7
|
+
|
|
8
|
+
from qcanvas.ui.course_viewer.tabs.file_tab.pages_file_tree import PagesFileTree
|
|
9
|
+
from qcanvas.util.layouts import layout
|
|
10
|
+
|
|
11
|
+
_logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FileTab(QWidget):
|
|
15
|
+
@staticmethod
|
|
16
|
+
def create_from_receipt(
|
|
17
|
+
*,
|
|
18
|
+
course: db.Course,
|
|
19
|
+
sync_receipt: SyncReceipt,
|
|
20
|
+
downloader: ResourceManager,
|
|
21
|
+
) -> "FileTab":
|
|
22
|
+
return FileTab(course=course, sync_receipt=sync_receipt, downloader=downloader)
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
course: db.Course,
|
|
27
|
+
downloader: ResourceManager,
|
|
28
|
+
*,
|
|
29
|
+
sync_receipt: SyncReceipt = None,
|
|
30
|
+
):
|
|
31
|
+
super().__init__()
|
|
32
|
+
|
|
33
|
+
self._page_file_tree = PagesFileTree.create_from_receipt(
|
|
34
|
+
course, sync_receipt=sync_receipt, resource_manager=downloader
|
|
35
|
+
)
|
|
36
|
+
# self._assignment_file_tree = FileTree.create_from_receipt(
|
|
37
|
+
# course,
|
|
38
|
+
# sync_receipt=sync_receipt,
|
|
39
|
+
# mode="assignments",
|
|
40
|
+
# resource_manager=downloader,
|
|
41
|
+
# )
|
|
42
|
+
|
|
43
|
+
self.setLayout(layout(QHBoxLayout, self._page_file_tree))
|
|
44
|
+
|
|
45
|
+
def reload(self, course: db.Course, *, sync_receipt: SyncReceipt) -> None:
|
|
46
|
+
pass
|