qcanvas 1.1.0__py3-none-any.whl → 1.2.0a1__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/app_start/__init__.py +6 -1
- qcanvas/icons/__init__.py +2 -0
- qcanvas/icons/file-downloaded.svg +4 -4
- qcanvas/icons/icons.qrc +2 -0
- qcanvas/icons/logo-transparent-dark.svg +303 -0
- qcanvas/icons/logo-transparent-light.svg +304 -0
- qcanvas/icons/rc_icons.py +726 -173
- qcanvas/ui/course_viewer/content_tree.py +13 -0
- qcanvas/ui/course_viewer/course_viewer.py +14 -5
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +9 -12
- 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 +95 -0
- qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +50 -0
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +4 -7
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +7 -9
- qcanvas/ui/course_viewer/tabs/util.py +10 -0
- qcanvas/ui/main_ui/course_viewer_container.py +45 -3
- qcanvas/ui/main_ui/status_bar_progress_display.py +17 -8
- qcanvas/util/file_icons.py +36 -0
- 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 +74 -0
- {qcanvas-1.1.0.dist-info → qcanvas-1.2.0a1.dist-info}/METADATA +6 -2
- {qcanvas-1.1.0.dist-info → qcanvas-1.2.0a1.dist-info}/RECORD +29 -18
- qcanvas/util/themes.py +0 -27
- {qcanvas-1.1.0.dist-info → qcanvas-1.2.0a1.dist-info}/WHEEL +0 -0
- {qcanvas-1.1.0.dist-info → qcanvas-1.2.0a1.dist-info}/entry_points.txt +0 -0
|
@@ -61,6 +61,19 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
|
|
|
61
61
|
if min_width is not None:
|
|
62
62
|
self.setMinimumWidth(min_width)
|
|
63
63
|
|
|
64
|
+
def set_columns_resize_mode(
|
|
65
|
+
self,
|
|
66
|
+
resize_mode_for_columns: list[QHeaderView.ResizeMode],
|
|
67
|
+
*,
|
|
68
|
+
stretch_last: bool = False,
|
|
69
|
+
) -> None:
|
|
70
|
+
header = self.header()
|
|
71
|
+
|
|
72
|
+
for index, mode in enumerate(resize_mode_for_columns):
|
|
73
|
+
header.setSectionResizeMode(index, mode)
|
|
74
|
+
|
|
75
|
+
header.setStretchLastSection(stretch_last)
|
|
76
|
+
|
|
64
77
|
def reload(self, data: T, *, sync_receipt: SyncReceipt) -> None:
|
|
65
78
|
self._reloading = True
|
|
66
79
|
|
|
@@ -7,6 +7,7 @@ from qtpy.QtCore import Slot
|
|
|
7
7
|
from qtpy.QtWidgets import *
|
|
8
8
|
|
|
9
9
|
from qcanvas.ui.course_viewer.tabs.assignment_tab import AssignmentTab
|
|
10
|
+
from qcanvas.ui.course_viewer.tabs.file_tab import FileTab
|
|
10
11
|
from qcanvas.ui.course_viewer.tabs.mail_tab import MailTab
|
|
11
12
|
from qcanvas.ui.course_viewer.tabs.page_tab import PageTab
|
|
12
13
|
from qcanvas.util.basic_fonts import bold_font
|
|
@@ -47,12 +48,17 @@ class CourseViewer(QWidget):
|
|
|
47
48
|
downloader=downloader,
|
|
48
49
|
sync_receipt=sync_receipt,
|
|
49
50
|
)
|
|
51
|
+
self._files_tab = FileTab.create_from_receipt(
|
|
52
|
+
course=course,
|
|
53
|
+
downloader=downloader,
|
|
54
|
+
sync_receipt=sync_receipt,
|
|
55
|
+
)
|
|
50
56
|
|
|
51
57
|
self._tabs = QTabWidget()
|
|
58
|
+
self._tabs.addTab(self._files_tab, "Files")
|
|
52
59
|
self._tabs.addTab(self._pages_tab, "Pages")
|
|
53
60
|
self._tabs.addTab(self._assignments_tab, "Assignments")
|
|
54
61
|
self._tabs.addTab(self._mail_tab, "Mail")
|
|
55
|
-
self._tabs.addTab(QLabel("Not implemented"), "Files")
|
|
56
62
|
# self._tabs.addTab(QLabel("Not implemented"), "Panopto") # The meme lives on!
|
|
57
63
|
|
|
58
64
|
self.setLayout(layout(QVBoxLayout, self._course_label, self._tabs))
|
|
@@ -63,10 +69,10 @@ class CourseViewer(QWidget):
|
|
|
63
69
|
self._unhighlight_tab(0) # Because the first tab always gets auto-selected
|
|
64
70
|
|
|
65
71
|
def reload(self, course: db.Course, *, sync_receipt: SyncReceipt) -> None:
|
|
72
|
+
self._files_tab.reload(course, sync_receipt=sync_receipt)
|
|
66
73
|
self._pages_tab.reload(course, sync_receipt=sync_receipt)
|
|
67
74
|
self._assignments_tab.reload(course, sync_receipt=sync_receipt)
|
|
68
75
|
self._mail_tab.reload(course, sync_receipt=sync_receipt)
|
|
69
|
-
|
|
70
76
|
self._highlight_tabs(sync_receipt)
|
|
71
77
|
|
|
72
78
|
@Slot(int)
|
|
@@ -78,14 +84,17 @@ class CourseViewer(QWidget):
|
|
|
78
84
|
updates = sync_receipt.updates_by_course.get(self._course_id, None)
|
|
79
85
|
|
|
80
86
|
if updates is not None:
|
|
81
|
-
if len(updates.
|
|
87
|
+
if len(updates.updated_resources) > 0:
|
|
82
88
|
self._highlight_tab(0)
|
|
83
89
|
|
|
84
|
-
if len(updates.
|
|
90
|
+
if len(updates.updated_pages) > 0:
|
|
85
91
|
self._highlight_tab(1)
|
|
86
92
|
|
|
87
|
-
if len(updates.
|
|
93
|
+
if len(updates.updated_assignments) > 0:
|
|
88
94
|
self._highlight_tab(2)
|
|
95
|
+
|
|
96
|
+
if len(updates.updated_messages) > 0:
|
|
97
|
+
self._highlight_tab(3)
|
|
89
98
|
else:
|
|
90
99
|
for index in range(0, 4):
|
|
91
100
|
self._unhighlight_tab(index)
|
|
@@ -18,6 +18,7 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
18
18
|
tree_name=f"course.{course_id}.assignment_groups",
|
|
19
19
|
emit_selection_signal_for_type=db.Assignment,
|
|
20
20
|
)
|
|
21
|
+
|
|
21
22
|
self.ui_setup(
|
|
22
23
|
header_text=["Assignments", "Weight"],
|
|
23
24
|
indentation=15,
|
|
@@ -25,13 +26,9 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
25
26
|
min_width=150,
|
|
26
27
|
)
|
|
27
28
|
|
|
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)
|
|
29
|
+
self.set_columns_resize_mode(
|
|
30
|
+
[QHeaderView.ResizeMode.Stretch, QHeaderView.ResizeMode.ResizeToContents]
|
|
31
|
+
)
|
|
35
32
|
|
|
36
33
|
def create_tree_items(
|
|
37
34
|
self, course: db.Course, sync_receipt: SyncReceipt
|
|
@@ -39,8 +36,11 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
39
36
|
widgets = []
|
|
40
37
|
|
|
41
38
|
for assignment_group in course.assignment_groups: # type: db.AssignmentGroup
|
|
39
|
+
if len(assignment_group.assignments) == 0:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
42
|
assignment_group_widget = self._create_assignment_group_widget(
|
|
43
|
-
assignment_group
|
|
43
|
+
assignment_group
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
widgets.append(assignment_group_widget)
|
|
@@ -54,7 +54,7 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
54
54
|
return widgets
|
|
55
55
|
|
|
56
56
|
def _create_assignment_group_widget(
|
|
57
|
-
self, assignment_group: db.AssignmentGroup
|
|
57
|
+
self, assignment_group: db.AssignmentGroup
|
|
58
58
|
) -> MemoryTreeWidgetItem:
|
|
59
59
|
assignment_group_widget = MemoryTreeWidgetItem(
|
|
60
60
|
id=assignment_group.id,
|
|
@@ -64,9 +64,6 @@ class AssignmentTree(ContentTree[db.Course]):
|
|
|
64
64
|
|
|
65
65
|
assignment_group_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
|
|
66
66
|
|
|
67
|
-
if sync_receipt.was_updated(assignment_group):
|
|
68
|
-
self.mark_as_unseen(assignment_group_widget)
|
|
69
|
-
|
|
70
67
|
return assignment_group_widget
|
|
71
68
|
|
|
72
69
|
def _create_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
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import *
|
|
3
|
+
|
|
4
|
+
import qcanvas_backend.database.types as db
|
|
5
|
+
from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
|
|
6
|
+
from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
|
|
7
|
+
from qtpy.QtCore import QPoint, Qt, Slot
|
|
8
|
+
from qtpy.QtWidgets import *
|
|
9
|
+
|
|
10
|
+
from qcanvas.ui.course_viewer.content_tree import ContentTree
|
|
11
|
+
from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
|
|
12
|
+
from qcanvas.util.file_icons import icon_for_filename
|
|
13
|
+
from qcanvas.util.ui_tools import create_qaction
|
|
14
|
+
|
|
15
|
+
_logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FileTree(ContentTree[db.Course]):
|
|
22
|
+
@classmethod
|
|
23
|
+
def create_from_receipt(
|
|
24
|
+
cls,
|
|
25
|
+
course: db.Course,
|
|
26
|
+
*,
|
|
27
|
+
sync_receipt: SyncReceipt,
|
|
28
|
+
resource_manager: ResourceManager,
|
|
29
|
+
) -> "FileTree":
|
|
30
|
+
tree = cls(tree_name=course.id, resource_manager=resource_manager)
|
|
31
|
+
tree.reload(course, sync_receipt=sync_receipt)
|
|
32
|
+
return tree
|
|
33
|
+
|
|
34
|
+
def __init__(self, tree_name: str, *, resource_manager: ResourceManager):
|
|
35
|
+
super().__init__(tree_name, emit_selection_signal_for_type=object)
|
|
36
|
+
self._resource_manager = resource_manager
|
|
37
|
+
|
|
38
|
+
self.ui_setup(header_text=["File", "Date"])
|
|
39
|
+
self.set_columns_resize_mode(
|
|
40
|
+
[QHeaderView.ResizeMode.Stretch, QHeaderView.ResizeMode.ResizeToContents]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
44
|
+
self.customContextMenuRequested.connect(self._context_menu)
|
|
45
|
+
|
|
46
|
+
def _create_group_widget(
|
|
47
|
+
self, group: db.ContentGroup, sync_receipt: SyncReceipt
|
|
48
|
+
) -> MemoryTreeWidgetItem:
|
|
49
|
+
group_widget = MemoryTreeWidgetItem(
|
|
50
|
+
id=group.id, data=group, strings=[group.name]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
group_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
|
|
54
|
+
|
|
55
|
+
if sync_receipt.was_updated(group):
|
|
56
|
+
self.mark_as_unseen(group_widget)
|
|
57
|
+
|
|
58
|
+
return group_widget
|
|
59
|
+
|
|
60
|
+
def _create_resource_widget(
|
|
61
|
+
self, resource: db.Resource, sync_receipt: SyncReceipt
|
|
62
|
+
) -> QTreeWidgetItem:
|
|
63
|
+
# fixme the reesource widget items shouls NOT be a memory widget item because they can't be collapsed, but
|
|
64
|
+
# mostly because the same file can appear in the tree multiple times in different places, which memory tree
|
|
65
|
+
# can NOT deal with!
|
|
66
|
+
item_widget = QTreeWidgetItem(
|
|
67
|
+
[resource.file_name, str(resource.discovery_date.date())],
|
|
68
|
+
)
|
|
69
|
+
item_widget.setIcon(
|
|
70
|
+
0,
|
|
71
|
+
icon_for_filename(resource.file_name),
|
|
72
|
+
)
|
|
73
|
+
item_widget.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
|
|
74
|
+
|
|
75
|
+
if sync_receipt.was_updated(resource):
|
|
76
|
+
self.mark_as_unseen(item_widget)
|
|
77
|
+
|
|
78
|
+
return item_widget
|
|
79
|
+
|
|
80
|
+
@Slot(QPoint)
|
|
81
|
+
def _context_menu(self, point: QPoint) -> None:
|
|
82
|
+
item = self.itemAt(point)
|
|
83
|
+
|
|
84
|
+
if item is None or not isinstance(item, MemoryTreeWidgetItem):
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
menu = QMenu()
|
|
88
|
+
create_qaction(
|
|
89
|
+
name="Test",
|
|
90
|
+
parent=menu,
|
|
91
|
+
triggered=lambda: print(f"Clicked {item.extra_data.file_name}"),
|
|
92
|
+
)
|
|
93
|
+
menu.addAction("Another thing")
|
|
94
|
+
|
|
95
|
+
menu.exec(self.mapToGlobal(point))
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import *
|
|
3
|
+
|
|
4
|
+
import qcanvas_backend.database.types as db
|
|
5
|
+
from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
|
|
6
|
+
from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
|
|
7
|
+
|
|
8
|
+
from qcanvas.ui.course_viewer.tabs.file_tab.file_tree import FileTree
|
|
9
|
+
from qcanvas.ui.memory_tree import MemoryTreeWidgetItem
|
|
10
|
+
|
|
11
|
+
_logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PagesFileTree(FileTree):
|
|
15
|
+
|
|
16
|
+
def __init__(self, tree_name: str, *, resource_manager: ResourceManager):
|
|
17
|
+
super().__init__(
|
|
18
|
+
tree_name=f"{tree_name}.pages", resource_manager=resource_manager
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def create_tree_items(
|
|
22
|
+
self, data: db.Course, sync_receipt: SyncReceipt
|
|
23
|
+
) -> Sequence[MemoryTreeWidgetItem]:
|
|
24
|
+
widgets = []
|
|
25
|
+
|
|
26
|
+
for group in data.modules: # type: db.Module
|
|
27
|
+
if len(group.content_items) == 0:
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
group_widget = self._create_group_widget(group, sync_receipt)
|
|
31
|
+
items_in_group = set()
|
|
32
|
+
|
|
33
|
+
for item in group.content_items:
|
|
34
|
+
for resource in item.resources: # type: db.Resource
|
|
35
|
+
|
|
36
|
+
if resource.id not in items_in_group:
|
|
37
|
+
items_in_group.add(resource.id)
|
|
38
|
+
else:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
resource_widget = self._create_resource_widget(
|
|
42
|
+
resource, sync_receipt
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
group_widget.addChild(resource_widget)
|
|
46
|
+
|
|
47
|
+
if group_widget.childCount() > 0:
|
|
48
|
+
widgets.append(group_widget)
|
|
49
|
+
|
|
50
|
+
return widgets
|
|
@@ -18,6 +18,7 @@ class MailTree(ContentTree[db.Course]):
|
|
|
18
18
|
tree_name=f"course.{course_id}.mail",
|
|
19
19
|
emit_selection_signal_for_type=db.CourseMessage,
|
|
20
20
|
)
|
|
21
|
+
|
|
21
22
|
self.ui_setup(
|
|
22
23
|
header_text=["Subject", "Sender"],
|
|
23
24
|
max_width=500,
|
|
@@ -25,13 +26,9 @@ class MailTree(ContentTree[db.Course]):
|
|
|
25
26
|
indentation=20,
|
|
26
27
|
)
|
|
27
28
|
|
|
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)
|
|
29
|
+
self.set_columns_resize_mode(
|
|
30
|
+
[QHeaderView.ResizeMode.Stretch, QHeaderView.ResizeMode.ResizeToContents]
|
|
31
|
+
)
|
|
35
32
|
|
|
36
33
|
def create_tree_items(
|
|
37
34
|
self, course: db.Course, sync_receipt: SyncReceipt
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Sequence
|
|
3
3
|
|
|
4
4
|
import qcanvas_backend.database.types as db
|
|
5
5
|
from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
|
|
@@ -17,6 +17,7 @@ class PageTree(ContentTree[db.Course]):
|
|
|
17
17
|
tree_name=f"course.{course_id}.modules",
|
|
18
18
|
emit_selection_signal_for_type=db.ModulePage,
|
|
19
19
|
)
|
|
20
|
+
|
|
20
21
|
self.ui_setup(
|
|
21
22
|
header_text="Content", indentation=15, max_width=300, min_width=150
|
|
22
23
|
)
|
|
@@ -27,7 +28,10 @@ class PageTree(ContentTree[db.Course]):
|
|
|
27
28
|
widgets = []
|
|
28
29
|
|
|
29
30
|
for module in course.modules: # type: db.Module
|
|
30
|
-
|
|
31
|
+
if len(module.pages) == 0:
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
module_widget = self._create_module_widget(module)
|
|
31
35
|
widgets.append(module_widget)
|
|
32
36
|
|
|
33
37
|
for page in module.pages: # type: db.ModulePage
|
|
@@ -36,18 +40,12 @@ class PageTree(ContentTree[db.Course]):
|
|
|
36
40
|
|
|
37
41
|
return widgets
|
|
38
42
|
|
|
39
|
-
def _create_module_widget(
|
|
40
|
-
self, module: db.Module, sync_receipt: SyncReceipt
|
|
41
|
-
) -> MemoryTreeWidgetItem:
|
|
43
|
+
def _create_module_widget(self, module: db.Module) -> MemoryTreeWidgetItem:
|
|
42
44
|
module_widget = MemoryTreeWidgetItem(
|
|
43
45
|
id=module.id, data=module, strings=[module.name]
|
|
44
46
|
)
|
|
45
47
|
module_widget.setFlags(Qt.ItemFlag.ItemIsEnabled)
|
|
46
48
|
|
|
47
|
-
# Todo not sure if modules should get highlighted since they can't be unhighlighted by selecting them...
|
|
48
|
-
if sync_receipt.was_updated(module):
|
|
49
|
-
self.mark_as_unseen(module_widget)
|
|
50
|
-
|
|
51
49
|
return module_widget
|
|
52
50
|
|
|
53
51
|
def _create_page_widget(
|
|
@@ -1 +1,11 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
import qcanvas_backend.database.types as db
|
|
4
|
+
from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
|
|
5
|
+
|
|
1
6
|
date_strftime_format = "%A, %Y-%m-%d, %H:%M:%S"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# todo what the hell is the point of this?
|
|
10
|
+
class SupportsReload(Protocol):
|
|
11
|
+
def reload(self, course: db.Course, *, sync_receipt: SyncReceipt) -> None: ...
|
|
@@ -1,17 +1,60 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from math import floor
|
|
2
3
|
from typing import *
|
|
3
4
|
|
|
4
5
|
import qcanvas_backend.database.types as db
|
|
5
6
|
from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
|
|
6
7
|
from qcanvas_backend.net.sync.sync_receipt import SyncReceipt, empty_receipt
|
|
7
|
-
from qtpy.QtCore import Qt
|
|
8
|
+
from qtpy.QtCore import Qt, Slot
|
|
9
|
+
from qtpy.QtGui import QIcon
|
|
8
10
|
from qtpy.QtWidgets import *
|
|
9
11
|
|
|
12
|
+
from qcanvas import icons
|
|
10
13
|
from qcanvas.ui.course_viewer.course_viewer import CourseViewer
|
|
14
|
+
from qcanvas.util import themes
|
|
11
15
|
|
|
12
16
|
_logger = logging.getLogger(__name__)
|
|
13
17
|
|
|
14
18
|
|
|
19
|
+
# todo needs to handle dark mode
|
|
20
|
+
class _PlaceholderLogo(QLabel):
|
|
21
|
+
"""
|
|
22
|
+
Automatically resizing logo icon for when no course is selected
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
super().__init__()
|
|
27
|
+
self._light_icon = QIcon(icons.logo_transparent_light)
|
|
28
|
+
self._dark_icon = QIcon(icons.logo_transparent_dark)
|
|
29
|
+
self._old_width = -1
|
|
30
|
+
self._old_height = -1
|
|
31
|
+
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
32
|
+
self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
|
|
33
|
+
themes.theme_changed().connect(self._theme_changed)
|
|
34
|
+
|
|
35
|
+
def resizeEvent(self, event) -> None:
|
|
36
|
+
self._update_image()
|
|
37
|
+
|
|
38
|
+
@Slot()
|
|
39
|
+
def _theme_changed(self) -> None:
|
|
40
|
+
self._update_image(force=True)
|
|
41
|
+
|
|
42
|
+
def _update_image(self, force: bool = False) -> None:
|
|
43
|
+
# Calculate the size of the logo as half of the width/height with a max size of 1000x1000
|
|
44
|
+
width = min(floor(self.width() * 0.5), 500)
|
|
45
|
+
height = min(floor(self.height() * 0.5), 500)
|
|
46
|
+
|
|
47
|
+
if force or (width != self._old_width and height != self._old_height):
|
|
48
|
+
self._old_width = width
|
|
49
|
+
self._old_height = height
|
|
50
|
+
else:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
icon = self._dark_icon if themes.is_dark_mode() else self._light_icon
|
|
54
|
+
|
|
55
|
+
self.setPixmap(icon.pixmap(width, height))
|
|
56
|
+
|
|
57
|
+
|
|
15
58
|
class CourseViewerContainer(QStackedWidget):
|
|
16
59
|
def __init__(self, downloader: ResourceManager):
|
|
17
60
|
super().__init__()
|
|
@@ -19,8 +62,7 @@ class CourseViewerContainer(QStackedWidget):
|
|
|
19
62
|
self._downloader = downloader
|
|
20
63
|
self._last_course_id: Optional[str] = None
|
|
21
64
|
self._last_sync_receipt: SyncReceipt = empty_receipt()
|
|
22
|
-
self._placeholder =
|
|
23
|
-
self._placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
65
|
+
self._placeholder = _PlaceholderLogo()
|
|
24
66
|
self.addWidget(self._placeholder)
|
|
25
67
|
|
|
26
68
|
def show_blank(self) -> None:
|
|
@@ -77,10 +77,13 @@ class StatusBarProgressDisplay(QStatusBar):
|
|
|
77
77
|
async with self._lock:
|
|
78
78
|
if self._has_no_tasks:
|
|
79
79
|
self._show_done()
|
|
80
|
-
elif self._has_single_task:
|
|
81
|
-
self._show_single_task_progress(list(self._tasks.items())[0])
|
|
82
80
|
else:
|
|
83
|
-
|
|
81
|
+
tasks = list(self._tasks.items())
|
|
82
|
+
|
|
83
|
+
if self._has_single_task:
|
|
84
|
+
self._show_single_task_progress(tasks[0])
|
|
85
|
+
else:
|
|
86
|
+
self._show_multiple_tasks_progress(tasks)
|
|
84
87
|
|
|
85
88
|
def _show_done(self) -> None:
|
|
86
89
|
_logger.info("Finished tasks. Tasks: %s", self._tasks)
|
|
@@ -94,18 +97,24 @@ class StatusBarProgressDisplay(QStatusBar):
|
|
|
94
97
|
self._show_progress(progress)
|
|
95
98
|
self.showMessage(id.step_name)
|
|
96
99
|
|
|
97
|
-
def _show_multiple_tasks_progress(
|
|
100
|
+
def _show_multiple_tasks_progress(
|
|
101
|
+
self, tasks: list[Tuple[TaskID, _TaskProgress]]
|
|
102
|
+
) -> None:
|
|
98
103
|
_logger.debug("Multiple tasks %s", tasks)
|
|
99
|
-
self.showMessage(
|
|
104
|
+
self.showMessage(
|
|
105
|
+
f"{len(tasks)} tasks in progress - {', '.join([task[0].step_name for task in tasks])}"
|
|
106
|
+
)
|
|
100
107
|
self._show_progress(self._calculate_progress(tasks))
|
|
101
108
|
|
|
102
|
-
def _calculate_progress(
|
|
103
|
-
|
|
109
|
+
def _calculate_progress(
|
|
110
|
+
self, tasks: list[Tuple[TaskID, _TaskProgress]]
|
|
111
|
+
) -> _TaskProgress:
|
|
112
|
+
# Task progresses are floats from 0 to 1, multiplier is used to turn them into ints
|
|
104
113
|
multiplier = 1000
|
|
105
114
|
current_sum = 0
|
|
106
115
|
total_sum = 0
|
|
107
116
|
|
|
108
|
-
for task in tasks:
|
|
117
|
+
for _, task in tasks:
|
|
109
118
|
if task.total != 0:
|
|
110
119
|
current_sum += (task.current / task.total) * multiplier
|
|
111
120
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from qtpy.QtCore import QFileInfo, QMimeDatabase
|
|
4
|
+
from qtpy.QtGui import QIcon
|
|
5
|
+
from qtpy.QtWidgets import QApplication, QFileIconProvider, QStyle
|
|
6
|
+
|
|
7
|
+
import qcanvas.util.runtime as runtime
|
|
8
|
+
|
|
9
|
+
_logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Windows and linux have different ways of doing this
|
|
12
|
+
if runtime.is_running_on_windows:
|
|
13
|
+
_icon_provider = QFileIconProvider()
|
|
14
|
+
|
|
15
|
+
def icon_for_filename(file_name: str) -> QIcon:
|
|
16
|
+
return _icon_provider.icon(QFileInfo(file_name))
|
|
17
|
+
|
|
18
|
+
else:
|
|
19
|
+
_mime_database = QMimeDatabase()
|
|
20
|
+
_default_icon = None
|
|
21
|
+
|
|
22
|
+
def icon_for_filename(file_name: str) -> QIcon:
|
|
23
|
+
global _default_icon
|
|
24
|
+
|
|
25
|
+
for mime_type in _mime_database.mimeTypesForFileName(file_name):
|
|
26
|
+
icon = QIcon.fromTheme(mime_type.iconName())
|
|
27
|
+
|
|
28
|
+
if not icon.isNull():
|
|
29
|
+
return icon
|
|
30
|
+
|
|
31
|
+
if _default_icon is None:
|
|
32
|
+
_default_icon = QApplication.style().standardIcon(
|
|
33
|
+
QStyle.StandardPixmap.SP_FileIcon
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return _default_icon
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from qtpy.QtCore import QObject, Signal, Slot
|
|
4
|
+
from qtpy.QtGui import QGuiApplication, Qt
|
|
5
|
+
|
|
6
|
+
_logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def colour_scheme() -> Qt.ColorScheme:
|
|
10
|
+
return QGuiApplication.styleHints().colorScheme()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_dark_colour_scheme() -> bool:
|
|
14
|
+
return colour_scheme() == Qt.ColorScheme.Dark
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ColourSchemeChangeEvent(QObject):
|
|
18
|
+
theme_changed = Signal()
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
super().__init__(None)
|
|
22
|
+
self._last_theme = colour_scheme()
|
|
23
|
+
QGuiApplication.styleHints().colorSchemeChanged.connect(self._theme_changed)
|
|
24
|
+
|
|
25
|
+
@Slot(Qt.ColorScheme)
|
|
26
|
+
def _theme_changed(self, colour_scheme: Qt.ColorScheme) -> None:
|
|
27
|
+
# Ensure the signal isn't fired when there wasn't actually a change
|
|
28
|
+
if colour_scheme != self._last_theme:
|
|
29
|
+
self._last_theme = colour_scheme
|
|
30
|
+
self.theme_changed.emit()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_theme_changed_listener = ColourSchemeChangeEvent()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def colour_scheme_changed() -> Signal:
|
|
37
|
+
global _theme_changed_listener
|
|
38
|
+
return _theme_changed_listener.theme_changed
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from qtpy.QtCore import QObject, Signal
|
|
4
|
+
|
|
5
|
+
_logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ThemeChangedEvent(QObject):
|
|
9
|
+
theme_changed = Signal()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
theme_changed_event = ThemeChangedEvent(None)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def theme_changed() -> Signal:
|
|
16
|
+
global theme_changed_event
|
|
17
|
+
return theme_changed_event.theme_changed
|