qcanvas 1.2.0a1__py3-none-any.whl → 1.2.2__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.
- qcanvas/icons/__init__.py +56 -9
- 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/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 +43 -9
- 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/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 +1891 -629
- qcanvas/icons/{file-downloaded.svg → universal/downloads/downloaded.svg} +1 -1
- 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 +7 -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 +72 -30
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +6 -2
- qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +17 -13
- qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +15 -9
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +7 -4
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +6 -2
- qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +36 -52
- qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
- qcanvas/ui/main_ui/course_viewer_container.py +11 -10
- 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 +21 -7
- 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 +21 -3
- qcanvas/util/html_cleaner.py +2 -0
- qcanvas/util/layouts.py +5 -2
- qcanvas/util/settings/_mapped_setting.py +6 -1
- qcanvas/util/settings/_ui_settings.py +1 -1
- qcanvas/util/themes/_theme_changer.py +13 -1
- qcanvas/util/ui_tools.py +5 -1
- {qcanvas-1.2.0a1.dist-info → qcanvas-1.2.2.dist-info}/METADATA +13 -7
- qcanvas-1.2.2.dist-info/RECORD +118 -0
- qcanvas/icons/sync.svg +0 -7
- qcanvas-1.2.0a1.dist-info/RECORD +0 -80
- /qcanvas/icons/{logo-transparent-dark.svg → dark/branding/logo_transparent.svg} +0 -0
- /qcanvas/icons/{logo-transparent-light.svg → light/branding/logo_transparent.svg} +0 -0
- /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.2.0a1.dist-info → qcanvas-1.2.2.dist-info}/WHEEL +0 -0
- {qcanvas-1.2.0a1.dist-info → qcanvas-1.2.2.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="#
|
|
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>
|
|
@@ -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,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[
|
|
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,
|
|
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(
|
|
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,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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
|
107
|
-
|
|
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
|
-
) ->
|
|
72
|
-
assignment_widget =
|
|
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 =
|
|
67
|
-
|
|
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
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
50
|
+
if len(resource_widgets) > 0:
|
|
51
|
+
group_widget.addChildren(resource_widgets)
|
|
46
52
|
|
|
47
|
-
if group_widget
|
|
53
|
+
if group_widget is not None:
|
|
48
54
|
widgets.append(group_widget)
|
|
49
55
|
|
|
50
56
|
return widgets
|
|
@@ -5,8 +5,9 @@ import qcanvas_backend.database.types as db
|
|
|
5
5
|
from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
|
|
6
6
|
from qtpy.QtWidgets import *
|
|
7
7
|
|
|
8
|
+
from qcanvas import icons
|
|
8
9
|
from qcanvas.ui.course_viewer.content_tree import ContentTree
|
|
9
|
-
from qcanvas.ui.
|
|
10
|
+
from qcanvas.ui.course_viewer.tree_widget_data_item import TreeWidgetDataItem
|
|
10
11
|
|
|
11
12
|
_logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
@@ -24,6 +25,7 @@ class MailTree(ContentTree[db.Course]):
|
|
|
24
25
|
max_width=500,
|
|
25
26
|
min_width=300,
|
|
26
27
|
indentation=20,
|
|
28
|
+
alternating_row_colours=True,
|
|
27
29
|
)
|
|
28
30
|
|
|
29
31
|
self.set_columns_resize_mode(
|
|
@@ -32,19 +34,20 @@ class MailTree(ContentTree[db.Course]):
|
|
|
32
34
|
|
|
33
35
|
def create_tree_items(
|
|
34
36
|
self, course: db.Course, sync_receipt: SyncReceipt
|
|
35
|
-
) -> Sequence[
|
|
37
|
+
) -> Sequence[TreeWidgetDataItem]:
|
|
36
38
|
widgets = []
|
|
37
39
|
|
|
38
40
|
for message in course.messages: # type: db.CourseMessage
|
|
39
41
|
message_widget = self._create_mail_widget(message, sync_receipt)
|
|
42
|
+
message_widget.setIcon(0, icons.tree_items.mail)
|
|
40
43
|
widgets.append(message_widget)
|
|
41
44
|
|
|
42
45
|
return widgets
|
|
43
46
|
|
|
44
47
|
def _create_mail_widget(
|
|
45
48
|
self, message: db.CourseMessage, sync_receipt: SyncReceipt
|
|
46
|
-
) ->
|
|
47
|
-
message_widget =
|
|
49
|
+
) -> TreeWidgetDataItem:
|
|
50
|
+
message_widget = TreeWidgetDataItem(
|
|
48
51
|
id=message.id,
|
|
49
52
|
data=message,
|
|
50
53
|
strings=[message.name, message.sender_name],
|