qcanvas 1.2.2__py3-none-any.whl → 2.0.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.
Potentially problematic release.
This version of qcanvas might be problematic. Click here for more details.
- qcanvas/__init__.py +54 -0
- qcanvas/app.py +93 -0
- qcanvas/backend_connectors/frontend_resource_manager.py +13 -5
- qcanvas/backend_connectors/qcanvas_task_master.py +2 -2
- qcanvas/icons/__init__.py +5 -5
- qcanvas/icons/_icon_type.py +1 -1
- qcanvas/icons/icons.qrc +47 -43
- qcanvas/icons/rc_icons.py +1298 -1197
- qcanvas/settings/__init__.py +6 -0
- qcanvas/{util/settings → settings}/_client_settings.py +4 -4
- qcanvas/settings/_course_settings.py +54 -0
- qcanvas/{util/settings → settings}/_mapped_setting.py +2 -5
- qcanvas/{util/settings → settings}/_ui_settings.py +4 -4
- qcanvas/theme.py +101 -0
- qcanvas/ui/course_viewer/content_tree.py +9 -12
- qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +3 -3
- qcanvas/ui/course_viewer/course_tree/course_tree.py +9 -8
- qcanvas/ui/course_viewer/course_viewer.py +35 -43
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +107 -29
- qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +4 -4
- qcanvas/ui/course_viewer/tabs/constants.py +1 -0
- qcanvas/ui/course_viewer/tabs/content_tab.py +33 -39
- qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +4 -4
- qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +7 -10
- qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +6 -7
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +50 -27
- qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +7 -8
- qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +3 -3
- qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +5 -5
- qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +15 -21
- qcanvas/ui/course_viewer/tree_widget_data_item.py +1 -1
- qcanvas/ui/memory_tree/_tree_memory.py +45 -42
- qcanvas/ui/memory_tree/memory_tree_widget.py +22 -18
- qcanvas/ui/memory_tree/memory_tree_widget_item.py +3 -3
- qcanvas/ui/qcanvas_window/__init__.py +1 -0
- qcanvas/ui/{main_ui → qcanvas_window}/course_viewer_container.py +10 -10
- qcanvas/ui/{main_ui → qcanvas_window}/options/auto_download_resources_option.py +5 -5
- qcanvas/ui/{main_ui → qcanvas_window}/options/quick_sync_option.py +7 -6
- qcanvas/ui/{main_ui → qcanvas_window}/options/sync_on_start_option.py +7 -6
- qcanvas/ui/{main_ui → qcanvas_window}/options/theme_selection_menu.py +10 -10
- qcanvas/ui/{main_ui → qcanvas_window}/qcanvas_window.py +38 -33
- qcanvas/ui/{main_ui → qcanvas_window}/status_bar_progress_display.py +5 -6
- qcanvas/ui/qml_components/AttachmentsList.ui.qml +15 -0
- qcanvas/ui/qml_components/AttachmentsListDelegate.qml +77 -0
- qcanvas/ui/qml_components/AttachmentsListModel.qml +19 -0
- qcanvas/ui/qml_components/AttachmentsPane.qml +12 -0
- qcanvas/ui/qml_components/CommentsList.ui.qml +15 -0
- qcanvas/ui/qml_components/CommentsListDelegate.ui.qml +118 -0
- qcanvas/ui/qml_components/CommentsListModel.qml +56 -0
- qcanvas/ui/qml_components/CommentsPane.qml +12 -0
- qcanvas/ui/qml_components/DarkTheme.qml +12 -0
- qcanvas/ui/qml_components/DecoratedText.ui.qml +44 -0
- qcanvas/ui/qml_components/LightTheme.qml +11 -0
- qcanvas/ui/qml_components/Spacer.ui.qml +7 -0
- qcanvas/ui/qml_components/ThemedRectangle.qml +37 -0
- qcanvas/ui/qml_components/__init__.py +3 -0
- qcanvas/ui/qml_components/attachments_pane.py +72 -0
- qcanvas/ui/qml_components/comments_pane.py +85 -0
- qcanvas/ui/qml_components/qml_bridge_types.py +95 -0
- qcanvas/ui/qml_components/qml_pane.py +22 -0
- qcanvas/ui/setup/setup_checker.py +1 -1
- qcanvas/ui/setup/setup_dialog.py +27 -10
- qcanvas/util/auto_downloader.py +9 -7
- qcanvas/util/basic_fonts.py +2 -2
- qcanvas/util/context_dict.py +12 -0
- qcanvas/util/file_icons.py +11 -19
- qcanvas/util/layouts.py +5 -7
- qcanvas/util/paths.py +17 -6
- qcanvas/util/qurl_util.py +1 -1
- qcanvas/util/ui_tools.py +118 -8
- qcanvas/util/url_checker.py +1 -1
- {qcanvas-1.2.2.dist-info → qcanvas-2.0.0.dist-info}/METADATA +12 -10
- qcanvas-2.0.0.dist-info/RECORD +91 -0
- {qcanvas-1.2.2.dist-info → qcanvas-2.0.0.dist-info}/WHEEL +1 -1
- qcanvas-2.0.0.dist-info/entry_points.txt +3 -0
- qcanvas/app_start/__init__.py +0 -59
- qcanvas/icons/_update_icons.py +0 -89
- qcanvas/icons/dark/actions/exit.svg +0 -3
- qcanvas/icons/dark/actions/mark_all_read.svg +0 -3
- qcanvas/icons/dark/actions/open_downloads.svg +0 -3
- qcanvas/icons/dark/actions/quick_login.svg +0 -3
- qcanvas/icons/dark/actions/sync.svg +0 -3
- qcanvas/icons/dark/branding/logo_transparent.svg +0 -303
- qcanvas/icons/dark/options/auto_download.svg +0 -3
- qcanvas/icons/dark/options/theme.svg +0 -3
- qcanvas/icons/dark/tabs/assignments.svg +0 -3
- qcanvas/icons/dark/tabs/mail.svg +0 -3
- qcanvas/icons/dark/tabs/pages.svg +0 -3
- qcanvas/icons/dark/tree_items/assignment.svg +0 -3
- qcanvas/icons/dark/tree_items/mail.svg +0 -3
- qcanvas/icons/dark/tree_items/module.svg +0 -3
- qcanvas/icons/dark/tree_items/page.svg +0 -3
- qcanvas/icons/light/actions/exit.svg +0 -3
- qcanvas/icons/light/actions/mark_all_read.svg +0 -3
- qcanvas/icons/light/actions/open_downloads.svg +0 -3
- qcanvas/icons/light/actions/quick_login.svg +0 -3
- qcanvas/icons/light/actions/sync.svg +0 -3
- qcanvas/icons/light/branding/logo_transparent.svg +0 -304
- qcanvas/icons/light/options/auto_download.svg +0 -3
- qcanvas/icons/light/options/ignore_old.svg +0 -3
- qcanvas/icons/light/options/include_videos.svg +0 -3
- qcanvas/icons/light/options/theme.svg +0 -3
- qcanvas/icons/light/tabs/assignments.svg +0 -3
- qcanvas/icons/light/tabs/mail.svg +0 -3
- qcanvas/icons/light/tabs/pages.svg +0 -3
- qcanvas/icons/light/tree_items/assignment.svg +0 -3
- qcanvas/icons/light/tree_items/mail.svg +0 -3
- qcanvas/icons/light/tree_items/module.svg +0 -3
- qcanvas/icons/light/tree_items/page.svg +0 -3
- qcanvas/icons/universal/branding/main_icon.svg +0 -325
- qcanvas/icons/universal/downloads/download_failed.svg +0 -23
- qcanvas/icons/universal/downloads/downloaded.svg +0 -23
- qcanvas/icons/universal/downloads/not_downloaded.svg +0 -23
- qcanvas/icons/universal/downloads/unknown.svg +0 -6
- qcanvas/icons/universal/tabs/assignments_new_content.svg +0 -3
- qcanvas/icons/universal/tabs/mail_new_content.svg +0 -3
- qcanvas/icons/universal/tabs/pages_new_content.svg +0 -3
- qcanvas/icons/universal/tree_items/semester.svg +0 -108
- qcanvas/run.py +0 -54
- qcanvas/ui/course_viewer/tabs/util.py +0 -11
- qcanvas/ui/main_ui/__init__.py +0 -0
- qcanvas/util/settings/__init__.py +0 -9
- qcanvas/util/themes/__init__.py +0 -2
- qcanvas/util/themes/_colour_scheme_helper.py +0 -38
- qcanvas/util/themes/_selected_theme.py +0 -10
- qcanvas/util/themes/_theme_changed_event.py +0 -17
- qcanvas/util/themes/_theme_changer.py +0 -86
- qcanvas-1.2.2.dist-info/RECORD +0 -118
- qcanvas-1.2.2.dist-info/entry_points.txt +0 -3
- /qcanvas/ui/{main_ui → qcanvas_window}/options/__init__.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Optional
|
|
3
3
|
|
|
4
|
-
from
|
|
5
|
-
from
|
|
4
|
+
from libqcanvas_clients.canvas import CanvasClientConfig
|
|
5
|
+
from libqcanvas_clients.panopto import PanoptoClientConfig
|
|
6
6
|
|
|
7
7
|
from qcanvas.util import paths
|
|
8
|
-
from
|
|
8
|
+
from ._mapped_setting import BoolSetting, MappedSetting
|
|
9
9
|
|
|
10
10
|
_logger = logging.getLogger(__name__)
|
|
11
11
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from aiofile import async_open
|
|
5
|
+
from pydantic import BaseModel, RootModel, Field, ValidationError
|
|
6
|
+
|
|
7
|
+
from qcanvas.util import paths
|
|
8
|
+
|
|
9
|
+
_logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CourseConfigData(BaseModel):
|
|
13
|
+
nickname: str | None = Field(default=None)
|
|
14
|
+
|
|
15
|
+
async def save(self) -> None:
|
|
16
|
+
await course_configs.save()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_CourseConfigurations = RootModel[dict[str, CourseConfigData]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _CourseConfig:
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self._root_model = self._load_root_model()
|
|
25
|
+
|
|
26
|
+
def _load_root_model(self) -> _CourseConfigurations:
|
|
27
|
+
if self._storage_path.exists():
|
|
28
|
+
try:
|
|
29
|
+
return _CourseConfigurations.model_validate_json(
|
|
30
|
+
self._storage_path.read_text()
|
|
31
|
+
)
|
|
32
|
+
except ValidationError as e:
|
|
33
|
+
_logger.error("Failed to load course configs", exc_info=e)
|
|
34
|
+
|
|
35
|
+
return _CourseConfigurations({})
|
|
36
|
+
|
|
37
|
+
async def save(self) -> None:
|
|
38
|
+
async with async_open(self._storage_path, "wt") as file:
|
|
39
|
+
await file.write(self._root_model.model_dump_json(indent=4))
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def _storage_path(self) -> Path:
|
|
43
|
+
return paths.config_storage() / "course_settings.json"
|
|
44
|
+
|
|
45
|
+
def __getitem__(self, item: str) -> CourseConfigData:
|
|
46
|
+
if item in self._root_model.root:
|
|
47
|
+
return self._root_model.root[item]
|
|
48
|
+
else:
|
|
49
|
+
new_config = CourseConfigData()
|
|
50
|
+
self._root_model.root[item] = new_config
|
|
51
|
+
return new_config
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
course_configs = _CourseConfig()
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import *
|
|
3
2
|
|
|
4
|
-
from
|
|
3
|
+
from PySide6.QtCore import QSettings
|
|
5
4
|
|
|
6
5
|
_logger = logging.getLogger(__name__)
|
|
7
6
|
|
|
8
|
-
T = TypeVar("T")
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
class MappedSetting(Generic[T]):
|
|
8
|
+
class MappedSetting[T]:
|
|
12
9
|
"""
|
|
13
10
|
Acts as a proxy for a named value in a QSettings object.
|
|
14
11
|
Stores the value in memory when initialised and updates it accordingly, to protect it from changes on disk.
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from PySide6.QtCore import QByteArray, QSettings
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from qcanvas.
|
|
5
|
+
from ._mapped_setting import MappedSetting
|
|
6
|
+
from qcanvas.theme import ensure_theme_is_valid
|
|
7
7
|
|
|
8
8
|
_logger = logging.getLogger(__name__)
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class ThemeSetting(MappedSetting):
|
|
12
12
|
def __init__(self):
|
|
13
|
-
super().__init__(default=
|
|
13
|
+
super().__init__(default="auto")
|
|
14
14
|
|
|
15
15
|
def __get__(self, instance, owner):
|
|
16
16
|
return ensure_theme_is_valid(super().__get__(instance, owner))
|
qcanvas/theme.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
import qdarktheme
|
|
5
|
+
from PySide6.QtCore import QObject, Signal, Property, Slot
|
|
6
|
+
from PySide6.QtGui import QGuiApplication, Qt, QIcon
|
|
7
|
+
from PySide6.QtWidgets import QStyleFactory, QApplication
|
|
8
|
+
|
|
9
|
+
type Theme = Literal["native", "auto", "dark", "light"]
|
|
10
|
+
|
|
11
|
+
_logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _AppTheme(QObject):
|
|
15
|
+
themeChanged = Signal()
|
|
16
|
+
darkModeChanged = Signal()
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self._last_system_theme = QGuiApplication.styleHints().colorScheme()
|
|
21
|
+
self._theme: Theme | None = None
|
|
22
|
+
self._dark_mode: bool | None = None
|
|
23
|
+
|
|
24
|
+
self.darkModeChanged.connect(self._set_icon_paths)
|
|
25
|
+
QGuiApplication.styleHints().colorSchemeChanged.connect(
|
|
26
|
+
self._on_system_theme_changed
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@Property(bool, notify=darkModeChanged)
|
|
30
|
+
def dark_mode(self) -> bool:
|
|
31
|
+
assert self._theme is not None, "Theme has not been set"
|
|
32
|
+
return self._dark_mode
|
|
33
|
+
|
|
34
|
+
@Property(str, notify=themeChanged)
|
|
35
|
+
def theme(self) -> Theme:
|
|
36
|
+
assert self._theme is not None, "Theme has not been set"
|
|
37
|
+
return self._theme
|
|
38
|
+
|
|
39
|
+
@theme.setter
|
|
40
|
+
def theme(self, value: str):
|
|
41
|
+
value = ensure_theme_is_valid(value)
|
|
42
|
+
|
|
43
|
+
if value != self._theme:
|
|
44
|
+
self._update_theme(value)
|
|
45
|
+
|
|
46
|
+
def _update_theme(self, theme: str):
|
|
47
|
+
if theme is None or (theme == self._theme and theme not in ["native", "auto"]):
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
was_dark_mode = self._dark_mode
|
|
51
|
+
|
|
52
|
+
if theme != "native":
|
|
53
|
+
if theme == "auto":
|
|
54
|
+
self._dark_mode = _is_system_using_dark_mode()
|
|
55
|
+
selected_colour_scheme = "dark" if self._dark_mode else "light"
|
|
56
|
+
else:
|
|
57
|
+
self._dark_mode = theme == "dark"
|
|
58
|
+
selected_colour_scheme = theme
|
|
59
|
+
|
|
60
|
+
if was_dark_mode != self._dark_mode:
|
|
61
|
+
qdarktheme.setup_theme(
|
|
62
|
+
selected_colour_scheme,
|
|
63
|
+
custom_colors={"primary": "e02424"},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
QApplication.setStyle(QStyleFactory.create("Fusion"))
|
|
67
|
+
else:
|
|
68
|
+
self._dark_mode = _is_system_using_dark_mode()
|
|
69
|
+
|
|
70
|
+
if theme != self._theme:
|
|
71
|
+
self._theme = theme
|
|
72
|
+
self.themeChanged.emit()
|
|
73
|
+
|
|
74
|
+
if was_dark_mode != self._dark_mode:
|
|
75
|
+
self.darkModeChanged.emit()
|
|
76
|
+
|
|
77
|
+
@Slot()
|
|
78
|
+
def _set_icon_paths(self):
|
|
79
|
+
QIcon.setFallbackSearchPaths(
|
|
80
|
+
[":icons/dark" if self._dark_mode else ":icons/light", ":icons/universal"]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@Slot()
|
|
84
|
+
def _on_system_theme_changed(self, scheme: Qt.ColorScheme):
|
|
85
|
+
if scheme != self._last_system_theme:
|
|
86
|
+
self._last_system_theme = scheme
|
|
87
|
+
self._update_theme(self._theme)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _is_system_using_dark_mode() -> bool:
|
|
91
|
+
return QGuiApplication.styleHints().colorScheme() == Qt.ColorScheme.Dark
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def ensure_theme_is_valid(theme_name: str) -> Theme:
|
|
95
|
+
if theme_name not in ["auto", "light", "dark", "native"]:
|
|
96
|
+
return "auto"
|
|
97
|
+
else:
|
|
98
|
+
return theme_name
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
app_theme = _AppTheme()
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from abc import abstractmethod
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Optional, Self, Sequence
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
from
|
|
5
|
+
from libqcanvas import db
|
|
6
|
+
from libqcanvas.net.sync.sync_receipt import SyncReceipt
|
|
7
|
+
from PySide6.QtCore import QItemSelection, Signal, Slot
|
|
8
|
+
from PySide6.QtWidgets import QHeaderView, QTreeWidgetItem
|
|
9
9
|
|
|
10
10
|
from qcanvas.ui.course_viewer.tree_widget_data_item import AnyTreeDataItem
|
|
11
11
|
from qcanvas.ui.memory_tree import MemoryTreeWidget
|
|
@@ -13,17 +13,14 @@ from qcanvas.util.basic_fonts import bold_font, normal_font
|
|
|
13
13
|
|
|
14
14
|
_logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
|
-
T = TypeVar("T")
|
|
17
|
-
U = TypeVar("U", bound=Type["ContentTree"])
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
class ContentTree(MemoryTreeWidget, Generic[T]):
|
|
17
|
+
class ContentTree[T](MemoryTreeWidget):
|
|
21
18
|
item_selected = Signal(object)
|
|
22
19
|
|
|
23
20
|
@classmethod
|
|
24
|
-
def create_from_receipt(
|
|
21
|
+
def create_from_receipt[U: Self](
|
|
25
22
|
cls: U, course: db.Course, *, sync_receipt: SyncReceipt
|
|
26
|
-
) ->
|
|
23
|
+
) -> type[U]:
|
|
27
24
|
tree = cls(course.id)
|
|
28
25
|
tree.reload(course, sync_receipt=sync_receipt)
|
|
29
26
|
return tree
|
|
@@ -32,7 +29,7 @@ class ContentTree(MemoryTreeWidget, Generic[T]):
|
|
|
32
29
|
self,
|
|
33
30
|
tree_name: str,
|
|
34
31
|
*,
|
|
35
|
-
emit_selection_signal_for_type:
|
|
32
|
+
emit_selection_signal_for_type: type,
|
|
36
33
|
):
|
|
37
34
|
super().__init__(tree_name)
|
|
38
35
|
self._reloading = False
|
|
@@ -3,9 +3,9 @@ import logging
|
|
|
3
3
|
import random
|
|
4
4
|
|
|
5
5
|
from cachetools import cached
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
from
|
|
6
|
+
from PySide6.QtCore import QByteArray
|
|
7
|
+
from PySide6.QtGui import QColor, QPainter, QPixmap
|
|
8
|
+
from PySide6.QtSvg import QSvgRenderer
|
|
9
9
|
|
|
10
10
|
_logger = logging.getLogger(__name__)
|
|
11
11
|
_transparent = QColor("#00000000")
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Sequence
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
4
|
+
from libqcanvas import db
|
|
5
|
+
from libqcanvas.net.sync.sync_receipt import SyncReceipt
|
|
6
|
+
from PySide6.QtCore import Qt, Signal
|
|
7
7
|
|
|
8
8
|
from qcanvas import icons
|
|
9
|
+
from qcanvas.settings import course_configs
|
|
9
10
|
from qcanvas.ui.course_viewer.content_tree import ContentTree
|
|
10
11
|
from qcanvas.ui.course_viewer.course_tree._course_icon_generator import (
|
|
11
12
|
CourseIconGenerator,
|
|
@@ -22,7 +23,7 @@ class _CourseTreeItem(TreeWidgetDataItem):
|
|
|
22
23
|
self,
|
|
23
24
|
id=course.id,
|
|
24
25
|
data=course,
|
|
25
|
-
strings=[course.
|
|
26
|
+
strings=[course_configs[course.id].nickname or course.name],
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
self._owner = owner
|
|
@@ -34,7 +35,7 @@ class _CourseTreeItem(TreeWidgetDataItem):
|
|
|
34
35
|
| Qt.ItemFlag.ItemIsEnabled
|
|
35
36
|
)
|
|
36
37
|
|
|
37
|
-
def setData(self, column: int, role: int, value:
|
|
38
|
+
def setData(self, column: int, role: int, value: object):
|
|
38
39
|
if column != 0 or not isinstance(value, str):
|
|
39
40
|
return super().setData(column, role, value)
|
|
40
41
|
|
|
@@ -59,11 +60,11 @@ class CourseTree(ContentTree[Sequence[db.Term]]):
|
|
|
59
60
|
)
|
|
60
61
|
|
|
61
62
|
def create_tree_items(
|
|
62
|
-
self, terms:
|
|
63
|
+
self, terms: list[db.Term], sync_receipt: SyncReceipt
|
|
63
64
|
) -> Sequence[MemoryTreeWidgetItem]:
|
|
64
65
|
widgets = []
|
|
65
66
|
|
|
66
|
-
for term in
|
|
67
|
+
for term in terms:
|
|
67
68
|
term_widget = self._create_term_widget(term)
|
|
68
69
|
course_icon_generator = CourseIconGenerator(term.id)
|
|
69
70
|
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from
|
|
4
|
+
from libqcanvas import db
|
|
5
|
+
from libqcanvas.net.resources.download.resource_manager import ResourceManager
|
|
6
|
+
from libqcanvas.net.sync.sync_receipt import SyncReceipt
|
|
7
|
+
from PySide6.QtCore import Slot
|
|
8
|
+
from PySide6.QtGui import QIcon
|
|
9
|
+
from PySide6.QtWidgets import QTabWidget, QVBoxLayout, QWidget
|
|
10
10
|
|
|
11
11
|
from qcanvas import icons
|
|
12
12
|
from qcanvas.ui.course_viewer.tabs.assignment_tab import AssignmentTab
|
|
13
13
|
from qcanvas.ui.course_viewer.tabs.mail_tab import MailTab
|
|
14
14
|
from qcanvas.ui.course_viewer.tabs.page_tab import PageTab
|
|
15
|
-
from qcanvas.util.basic_fonts import bold_font
|
|
16
15
|
from qcanvas.util.layouts import layout
|
|
17
|
-
|
|
16
|
+
import qcanvas.util.ui_tools as ui
|
|
18
17
|
|
|
19
18
|
_logger = logging.getLogger(__name__)
|
|
20
19
|
|
|
@@ -23,10 +22,10 @@ _logger = logging.getLogger(__name__)
|
|
|
23
22
|
class _Tab:
|
|
24
23
|
icon: QIcon
|
|
25
24
|
highlighted_icon: QIcon
|
|
25
|
+
update_type: type
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class CourseViewer(QWidget):
|
|
29
|
-
|
|
30
29
|
def __init__(
|
|
31
30
|
self,
|
|
32
31
|
course: db.Course,
|
|
@@ -39,9 +38,9 @@ class CourseViewer(QWidget):
|
|
|
39
38
|
self._course_id = course.id
|
|
40
39
|
self._previous_tab_index = 0
|
|
41
40
|
|
|
42
|
-
self._course_label =
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
self._course_label = ui.label(
|
|
42
|
+
course.name, font=ui.font(point_size=13, bold=True), allow_truncation=True
|
|
43
|
+
)
|
|
45
44
|
|
|
46
45
|
self._pages_tab = PageTab.create_from_receipt(
|
|
47
46
|
course=course,
|
|
@@ -73,23 +72,26 @@ class CourseViewer(QWidget):
|
|
|
73
72
|
# icon=icons.tabs.pages,
|
|
74
73
|
# highlighted_icon=icons.tabs.pages_new_content,
|
|
75
74
|
# )
|
|
76
|
-
self._PAGES_TAB = self.
|
|
75
|
+
self._PAGES_TAB = self._set_up_tab(
|
|
77
76
|
name="Pages",
|
|
78
77
|
widget=self._pages_tab,
|
|
79
78
|
icon=icons.tabs.pages,
|
|
80
79
|
highlighted_icon=icons.tabs.pages_new_content,
|
|
80
|
+
content_update_key=db.Page,
|
|
81
81
|
)
|
|
82
|
-
self._ASSIGNMENTS_TAB = self.
|
|
82
|
+
self._ASSIGNMENTS_TAB = self._set_up_tab(
|
|
83
83
|
name="Assignments",
|
|
84
84
|
widget=self._assignments_tab,
|
|
85
85
|
icon=icons.tabs.assignments,
|
|
86
86
|
highlighted_icon=icons.tabs.assignments_new_content,
|
|
87
|
+
content_update_key=db.Assignment,
|
|
87
88
|
)
|
|
88
|
-
self._MAIL_TAB = self.
|
|
89
|
+
self._MAIL_TAB = self._set_up_tab(
|
|
89
90
|
name="Mail",
|
|
90
91
|
widget=self._mail_tab,
|
|
91
92
|
icon=icons.tabs.mail,
|
|
92
93
|
highlighted_icon=icons.tabs.mail_new_content,
|
|
94
|
+
content_update_key=db.Message,
|
|
93
95
|
)
|
|
94
96
|
# self._tabs.addTab(QLabel("Not implemented"), "Panopto") # The meme lives on!
|
|
95
97
|
|
|
@@ -97,11 +99,17 @@ class CourseViewer(QWidget):
|
|
|
97
99
|
self._tab_widget.currentChanged.connect(self._tab_changed)
|
|
98
100
|
self._highlight_tabs(sync_receipt)
|
|
99
101
|
|
|
100
|
-
def
|
|
101
|
-
self,
|
|
102
|
+
def _set_up_tab(
|
|
103
|
+
self,
|
|
104
|
+
*,
|
|
105
|
+
widget: QWidget,
|
|
106
|
+
icon: QIcon,
|
|
107
|
+
highlighted_icon: QIcon,
|
|
108
|
+
name: str,
|
|
109
|
+
content_update_key: type,
|
|
102
110
|
) -> int:
|
|
103
111
|
index = self._tab_widget.addTab(widget, icon, name)
|
|
104
|
-
self._tabs[index] = _Tab(icon, highlighted_icon)
|
|
112
|
+
self._tabs[index] = _Tab(icon, highlighted_icon, update_type=content_update_key)
|
|
105
113
|
return index
|
|
106
114
|
|
|
107
115
|
def reload(self, course: db.Course, *, sync_receipt: SyncReceipt) -> None:
|
|
@@ -116,35 +124,19 @@ class CourseViewer(QWidget):
|
|
|
116
124
|
_logger.debug(f"Index = {index}")
|
|
117
125
|
if index != -1:
|
|
118
126
|
_logger.debug(f"Previous tab = {self._previous_tab_index}")
|
|
119
|
-
self.
|
|
127
|
+
self._set_tab_highlight(self._previous_tab_index, False)
|
|
120
128
|
self._previous_tab_index = index
|
|
121
129
|
|
|
122
130
|
def _highlight_tabs(self, sync_receipt: SyncReceipt) -> None:
|
|
123
131
|
updates = sync_receipt.updates_by_course.get(self._course_id, None)
|
|
124
132
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
self._unhighlight_tab(self._PAGES_TAB)
|
|
130
|
-
|
|
131
|
-
if len(updates.updated_assignments) > 0:
|
|
132
|
-
self._highlight_tab(self._ASSIGNMENTS_TAB)
|
|
133
|
-
else:
|
|
134
|
-
self._unhighlight_tab(self._ASSIGNMENTS_TAB)
|
|
135
|
-
|
|
136
|
-
if len(updates.updated_messages) > 0:
|
|
137
|
-
self._highlight_tab(self._MAIL_TAB)
|
|
138
|
-
else:
|
|
139
|
-
self._unhighlight_tab(self._MAIL_TAB)
|
|
140
|
-
else:
|
|
141
|
-
for index in range(0, len(self._tabs)):
|
|
142
|
-
self._unhighlight_tab(index)
|
|
143
|
-
|
|
144
|
-
def _highlight_tab(self, tab_index: int) -> None:
|
|
145
|
-
tab = self._tabs[tab_index]
|
|
146
|
-
self._tab_widget.setTabIcon(tab_index, tab.highlighted_icon)
|
|
133
|
+
for tab_index, tab in enumerate(self._tabs.values()):
|
|
134
|
+
self._set_tab_highlight(
|
|
135
|
+
tab_index, updates is not None and updates[tab.update_type] is not None
|
|
136
|
+
)
|
|
147
137
|
|
|
148
|
-
def
|
|
138
|
+
def _set_tab_highlight(self, tab_index: int, highlighted: bool) -> None:
|
|
149
139
|
tab = self._tabs[tab_index]
|
|
150
|
-
self._tab_widget.setTabIcon(
|
|
140
|
+
self._tab_widget.setTabIcon(
|
|
141
|
+
tab_index, tab.highlighted_icon if highlighted else tab.icon
|
|
142
|
+
)
|
|
@@ -1,28 +1,40 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
from
|
|
3
|
+
from PySide6.QtCore import Qt
|
|
4
|
+
from libqcanvas import db
|
|
5
|
+
from libqcanvas.net.sync.sync_receipt import SyncReceipt
|
|
6
|
+
from libqcanvas.util import as_local
|
|
7
|
+
from PySide6.QtWidgets import (
|
|
8
|
+
QLabel,
|
|
9
|
+
QLayout,
|
|
10
|
+
QDockWidget,
|
|
11
|
+
QMainWindow,
|
|
12
|
+
)
|
|
13
|
+
from typing_extensions import override
|
|
7
14
|
|
|
8
|
-
|
|
15
|
+
import qcanvas.util.ui_tools as ui
|
|
16
|
+
from qcanvas.backend_connectors import FrontendResourceManager
|
|
17
|
+
from .assignment_tree import AssignmentTree
|
|
18
|
+
from qcanvas.ui.qml_components import CommentsPane, AttachmentsPane
|
|
9
19
|
from qcanvas.ui.course_viewer.tabs.content_tab import ContentTab
|
|
10
|
-
from qcanvas.ui.course_viewer.tabs.
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
from qcanvas.ui.course_viewer.tabs.constants import (
|
|
21
|
+
date_strftime_format,
|
|
22
|
+
)
|
|
13
23
|
|
|
14
24
|
_logger = logging.getLogger(__name__)
|
|
15
25
|
|
|
16
26
|
|
|
17
27
|
class AssignmentTab(ContentTab):
|
|
18
|
-
|
|
19
28
|
def __init__(
|
|
20
29
|
self,
|
|
21
30
|
*,
|
|
22
31
|
course: db.Course,
|
|
23
32
|
sync_receipt: SyncReceipt,
|
|
24
|
-
downloader:
|
|
33
|
+
downloader: FrontendResourceManager,
|
|
25
34
|
):
|
|
35
|
+
# must be before super init, otherwise _setup_layout will be called before it is initialised
|
|
36
|
+
self._main_container = QMainWindow()
|
|
37
|
+
|
|
26
38
|
super().__init__(
|
|
27
39
|
explorer=AssignmentTree.create_from_receipt(
|
|
28
40
|
course, sync_receipt=sync_receipt
|
|
@@ -31,38 +43,104 @@ class AssignmentTab(ContentTab):
|
|
|
31
43
|
downloader=downloader,
|
|
32
44
|
)
|
|
33
45
|
|
|
46
|
+
# fixme: can't figure out how to get the panes to have the right size without showing them when nothing is selected
|
|
47
|
+
self._comments_pane = CommentsPane(downloader)
|
|
48
|
+
self._comments_dock = ui.dock_widget(
|
|
49
|
+
title="Comments",
|
|
50
|
+
name="comments_dock",
|
|
51
|
+
widget=self._comments_pane,
|
|
52
|
+
min_size=ui.size(150, 150),
|
|
53
|
+
features=QDockWidget.DockWidgetFeature.DockWidgetMovable,
|
|
54
|
+
hide=False,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
self._submission_files_pane = AttachmentsPane(downloader)
|
|
58
|
+
self._submission_files_dock = ui.dock_widget(
|
|
59
|
+
title="Submission Files",
|
|
60
|
+
name="sub_files_dock",
|
|
61
|
+
widget=self._submission_files_pane,
|
|
62
|
+
min_size=ui.size(150, 150),
|
|
63
|
+
features=QDockWidget.DockWidgetFeature.DockWidgetMovable,
|
|
64
|
+
hide=False,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
self._main_container.setCentralWidget(self._viewer)
|
|
68
|
+
self._main_container.addDockWidget(
|
|
69
|
+
Qt.DockWidgetArea.RightDockWidgetArea, self._submission_files_dock
|
|
70
|
+
)
|
|
71
|
+
self._main_container.addDockWidget(
|
|
72
|
+
Qt.DockWidgetArea.RightDockWidgetArea, self._comments_dock
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
self._main_container.resizeDocks(
|
|
76
|
+
[self._submission_files_dock, self._comments_dock],
|
|
77
|
+
[350, 350],
|
|
78
|
+
Qt.Orientation.Horizontal,
|
|
79
|
+
)
|
|
80
|
+
self._main_container.resizeDocks(
|
|
81
|
+
[self._submission_files_dock],
|
|
82
|
+
[200],
|
|
83
|
+
Qt.Orientation.Vertical,
|
|
84
|
+
)
|
|
85
|
+
|
|
34
86
|
self._due_date_label = QLabel("")
|
|
35
87
|
self._score_label = QLabel("")
|
|
36
|
-
|
|
37
88
|
self.enable_info_grid()
|
|
38
89
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
],
|
|
46
|
-
[
|
|
47
|
-
bold_label("Score:"),
|
|
48
|
-
self._score_label,
|
|
49
|
-
],
|
|
50
|
-
]
|
|
90
|
+
@override
|
|
91
|
+
def _setup_layout(self) -> None:
|
|
92
|
+
super()._setup_layout()
|
|
93
|
+
self.content_grid.replaceWidget(
|
|
94
|
+
self._viewer,
|
|
95
|
+
self._main_container,
|
|
51
96
|
)
|
|
52
97
|
|
|
53
|
-
|
|
54
|
-
|
|
98
|
+
self.content_grid.setColumnStretch(0, 1)
|
|
99
|
+
self.content_grid.setColumnStretch(1, 3)
|
|
55
100
|
|
|
56
|
-
|
|
101
|
+
@override
|
|
102
|
+
def setup_info_grid(self) -> QLayout:
|
|
103
|
+
return ui.form_layout(
|
|
104
|
+
{"Due": self._due_date_label, "Score": self._score_label},
|
|
105
|
+
)
|
|
57
106
|
|
|
107
|
+
# fixme: kind of a misleading name? it's not just updating the info "grid" anymore
|
|
108
|
+
@override
|
|
58
109
|
def update_info_grid(self, assignment: db.Assignment) -> None:
|
|
59
110
|
if assignment.due_date is not None:
|
|
60
|
-
due_text = assignment.due_date.strftime(date_strftime_format)
|
|
111
|
+
due_text = as_local(assignment.due_date).strftime(date_strftime_format)
|
|
61
112
|
else:
|
|
62
|
-
due_text = "
|
|
113
|
+
due_text = "No due date"
|
|
63
114
|
|
|
64
115
|
self._due_date_label.setText(due_text)
|
|
65
116
|
|
|
117
|
+
last_submission = assignment.submissions[-1] if assignment.submissions else None
|
|
118
|
+
submission_score = "-"
|
|
119
|
+
|
|
120
|
+
if last_submission and last_submission.score:
|
|
121
|
+
submission_score = last_submission.score
|
|
122
|
+
|
|
66
123
|
self._score_label.setText(
|
|
67
|
-
f"{
|
|
124
|
+
f"<b>{submission_score}</b>/{assignment.max_score or '?'}"
|
|
68
125
|
)
|
|
126
|
+
|
|
127
|
+
if last_submission and last_submission.attachments:
|
|
128
|
+
self._submission_files_pane.load_files(last_submission.attachments)
|
|
129
|
+
self._submission_files_dock.show()
|
|
130
|
+
else:
|
|
131
|
+
self._submission_files_pane.clear_files()
|
|
132
|
+
self._submission_files_dock.hide()
|
|
133
|
+
|
|
134
|
+
if last_submission and last_submission.comments:
|
|
135
|
+
self._comments_pane.load_comments(last_submission.comments)
|
|
136
|
+
self._comments_dock.show()
|
|
137
|
+
else:
|
|
138
|
+
self._comments_pane.clear_comments()
|
|
139
|
+
self._comments_dock.hide()
|
|
140
|
+
|
|
141
|
+
@override
|
|
142
|
+
def _show_blank(self) -> None:
|
|
143
|
+
super()._show_blank()
|
|
144
|
+
|
|
145
|
+
self._comments_dock.hide()
|
|
146
|
+
self._submission_files_dock.hide()
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from typing import Sequence
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from
|
|
4
|
+
from libqcanvas import db
|
|
5
|
+
from libqcanvas.net.sync.sync_receipt import SyncReceipt
|
|
6
|
+
from PySide6.QtCore import Qt
|
|
7
|
+
from PySide6.QtWidgets import QHeaderView
|
|
8
8
|
|
|
9
9
|
from qcanvas import icons
|
|
10
10
|
from qcanvas.ui.course_viewer.content_tree import ContentTree
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
date_strftime_format = "%A, %Y-%m-%d, %H:%M:%S"
|