qcanvas 1.0.12.dev3__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. qcanvas/app_start/__init__.py +6 -1
  2. qcanvas/icons/__init__.py +55 -6
  3. qcanvas/icons/_icon_type.py +42 -0
  4. qcanvas/icons/_update_icons.py +89 -0
  5. qcanvas/icons/dark/actions/exit.svg +3 -0
  6. qcanvas/icons/dark/actions/mark_all_read.svg +3 -0
  7. qcanvas/icons/dark/actions/open_downloads.svg +3 -0
  8. qcanvas/icons/dark/actions/quick_login.svg +3 -0
  9. qcanvas/icons/dark/actions/sync.svg +3 -0
  10. qcanvas/icons/dark/branding/logo_transparent.svg +303 -0
  11. qcanvas/icons/dark/options/auto_download.svg +3 -0
  12. qcanvas/icons/dark/options/theme.svg +3 -0
  13. qcanvas/icons/dark/tabs/assignments.svg +3 -0
  14. qcanvas/icons/dark/tabs/mail.svg +3 -0
  15. qcanvas/icons/dark/tabs/pages.svg +3 -0
  16. qcanvas/icons/dark/tree_items/assignment.svg +3 -0
  17. qcanvas/icons/dark/tree_items/mail.svg +3 -0
  18. qcanvas/icons/dark/tree_items/module.svg +3 -0
  19. qcanvas/icons/dark/tree_items/page.svg +3 -0
  20. qcanvas/icons/icons.qrc +44 -8
  21. qcanvas/icons/light/actions/exit.svg +3 -0
  22. qcanvas/icons/light/actions/mark_all_read.svg +3 -0
  23. qcanvas/icons/light/actions/open_downloads.svg +3 -0
  24. qcanvas/icons/light/actions/quick_login.svg +3 -0
  25. qcanvas/icons/light/actions/sync.svg +3 -0
  26. qcanvas/icons/light/branding/logo_transparent.svg +304 -0
  27. qcanvas/icons/light/options/auto_download.svg +3 -0
  28. qcanvas/icons/light/options/ignore_old.svg +3 -0
  29. qcanvas/icons/light/options/include_videos.svg +3 -0
  30. qcanvas/icons/light/options/theme.svg +3 -0
  31. qcanvas/icons/light/tabs/assignments.svg +3 -0
  32. qcanvas/icons/light/tabs/mail.svg +3 -0
  33. qcanvas/icons/light/tabs/pages.svg +3 -0
  34. qcanvas/icons/light/tree_items/assignment.svg +3 -0
  35. qcanvas/icons/light/tree_items/mail.svg +3 -0
  36. qcanvas/icons/light/tree_items/module.svg +3 -0
  37. qcanvas/icons/light/tree_items/page.svg +3 -0
  38. qcanvas/icons/rc_icons.py +2165 -355
  39. qcanvas/icons/universal/downloads/download_failed.svg +23 -0
  40. qcanvas/icons/universal/downloads/downloaded.svg +23 -0
  41. qcanvas/icons/universal/downloads/not_downloaded.svg +23 -0
  42. qcanvas/icons/universal/tabs/assignments_new_content.svg +3 -0
  43. qcanvas/icons/universal/tabs/mail_new_content.svg +3 -0
  44. qcanvas/icons/universal/tabs/pages_new_content.svg +3 -0
  45. qcanvas/icons/universal/tree_items/semester.svg +108 -0
  46. qcanvas/run.py +24 -0
  47. qcanvas/ui/course_viewer/content_tree.py +28 -7
  48. qcanvas/ui/course_viewer/course_tree/__init__.py +1 -0
  49. qcanvas/ui/course_viewer/course_tree/_course_icon_generator.py +86 -0
  50. qcanvas/ui/course_viewer/{course_tree.py → course_tree/course_tree.py} +20 -6
  51. qcanvas/ui/course_viewer/course_viewer.py +71 -24
  52. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +15 -14
  53. qcanvas/ui/course_viewer/tabs/file_tab/__init__.py +1 -0
  54. qcanvas/ui/course_viewer/tabs/file_tab/file_tab.py +46 -0
  55. qcanvas/ui/course_viewer/tabs/file_tab/file_tree.py +99 -0
  56. qcanvas/ui/course_viewer/tabs/file_tab/pages_file_tree.py +56 -0
  57. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +11 -11
  58. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +13 -11
  59. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +57 -56
  60. qcanvas/ui/course_viewer/tabs/util.py +10 -0
  61. qcanvas/ui/course_viewer/tree_widget_data_item.py +22 -0
  62. qcanvas/ui/main_ui/course_viewer_container.py +46 -3
  63. qcanvas/ui/main_ui/options/auto_download_resources_option.py +3 -1
  64. qcanvas/ui/main_ui/options/theme_selection_menu.py +2 -0
  65. qcanvas/ui/main_ui/qcanvas_window.py +18 -5
  66. qcanvas/ui/main_ui/status_bar_progress_display.py +17 -8
  67. qcanvas/ui/memory_tree/_tree_memory.py +1 -0
  68. qcanvas/ui/memory_tree/memory_tree_widget.py +2 -2
  69. qcanvas/ui/setup/setup_checker.py +2 -2
  70. qcanvas/ui/setup/setup_dialog.py +145 -66
  71. qcanvas/util/__init__.py +0 -2
  72. qcanvas/util/auto_downloader.py +1 -2
  73. qcanvas/util/file_icons.py +54 -0
  74. qcanvas/util/html_cleaner.py +2 -0
  75. qcanvas/util/layouts.py +5 -2
  76. qcanvas/util/paths.py +15 -26
  77. qcanvas/util/runtime.py +20 -0
  78. qcanvas/util/settings/_client_settings.py +11 -2
  79. qcanvas/util/settings/_mapped_setting.py +6 -1
  80. qcanvas/util/themes/__init__.py +2 -0
  81. qcanvas/util/themes/_colour_scheme_helper.py +38 -0
  82. qcanvas/util/themes/_selected_theme.py +10 -0
  83. qcanvas/util/themes/_theme_changed_event.py +17 -0
  84. qcanvas/util/themes/_theme_changer.py +86 -0
  85. qcanvas/util/ui_tools.py +5 -1
  86. {qcanvas-1.0.12.dev3.dist-info → qcanvas-1.2.0.dist-info}/METADATA +16 -6
  87. qcanvas-1.2.0.dist-info/RECORD +118 -0
  88. qcanvas/icons/file-download-failed.svg +0 -6
  89. qcanvas/icons/file-downloaded.svg +0 -6
  90. qcanvas/icons/file-not-downloaded.svg +0 -6
  91. qcanvas/icons/sync.svg +0 -7
  92. qcanvas/util/themes.py +0 -24
  93. qcanvas-1.0.12.dev3.dist-info/RECORD +0 -68
  94. /qcanvas/icons/{main_icon.svg → universal/branding/main_icon.svg} +0 -0
  95. /qcanvas/icons/{file-unknown.svg → universal/downloads/unknown.svg} +0 -0
  96. {qcanvas-1.0.12.dev3.dist-info → qcanvas-1.2.0.dist-info}/WHEEL +0 -0
  97. {qcanvas-1.0.12.dev3.dist-info → qcanvas-1.2.0.dist-info}/entry_points.txt +0 -0
@@ -1,30 +1,68 @@
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
8
9
  from qtpy.QtWidgets import *
9
10
 
11
+ from qcanvas import icons
10
12
  from qcanvas.ui.course_viewer.course_viewer import CourseViewer
13
+ from qcanvas.util import themes
11
14
 
12
15
  _logger = logging.getLogger(__name__)
13
16
 
14
17
 
18
+ class _PlaceholderLogo(QLabel):
19
+ """
20
+ Automatically resizing logo icon for when no course is selected
21
+ """
22
+
23
+ def __init__(self):
24
+ super().__init__()
25
+ self._icon = icons.branding.logo_transparent
26
+ self._old_width = -1
27
+ self._old_height = -1
28
+ self.setAlignment(Qt.AlignmentFlag.AlignCenter)
29
+ self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
30
+ # Because we are using a pixmap for the icon, it will not get updated like a normal QIcon when the theme changes,
31
+ # So we need to update it ourselves
32
+ themes.theme_changed().connect(self._theme_changed)
33
+
34
+ def resizeEvent(self, event) -> None:
35
+ self._update_image()
36
+
37
+ @Slot()
38
+ def _theme_changed(self) -> None:
39
+ self._update_image(force=True)
40
+
41
+ def _update_image(self, force: bool = False) -> None:
42
+ # Calculate the size of the logo as half of the width/height with a max size of 1000x1000
43
+ width = min(floor(self.width() * 0.5), 500)
44
+ height = min(floor(self.height() * 0.5), 500)
45
+
46
+ if force or (width != self._old_width and height != self._old_height):
47
+ self._old_width = width
48
+ self._old_height = height
49
+ self.setPixmap(self._icon.pixmap(width, height))
50
+
51
+
15
52
  class CourseViewerContainer(QStackedWidget):
16
53
  def __init__(self, downloader: ResourceManager):
17
54
  super().__init__()
18
55
  self._course_viewers: dict[str, CourseViewer] = {}
19
56
  self._downloader = downloader
20
57
  self._last_course_id: Optional[str] = None
58
+ self._selected_course: Optional[db.Course] = None
21
59
  self._last_sync_receipt: SyncReceipt = empty_receipt()
22
- self._placeholder = QLabel("No Course Selected")
23
- self._placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
60
+ self._placeholder = _PlaceholderLogo()
24
61
  self.addWidget(self._placeholder)
25
62
 
26
63
  def show_blank(self) -> None:
27
64
  self._last_course_id = None
65
+ self._selected_course = None
28
66
  self.setCurrentWidget(self._placeholder)
29
67
 
30
68
  def load_course(self, course: db.Course) -> None:
@@ -40,6 +78,7 @@ class CourseViewerContainer(QStackedWidget):
40
78
  viewer = self._course_viewers[course.id]
41
79
 
42
80
  self.setCurrentWidget(viewer)
81
+ self._selected_course = course
43
82
  self._last_course_id = course.id
44
83
 
45
84
  async def reload_all(
@@ -50,3 +89,7 @@ class CourseViewerContainer(QStackedWidget):
50
89
  if course.id in self._course_viewers:
51
90
  viewer = self._course_viewers[course.id]
52
91
  viewer.reload(course, sync_receipt=sync_receipt)
92
+
93
+ @property
94
+ def selected_course(self) -> Optional[db.Course]:
95
+ return self._selected_course
@@ -5,6 +5,7 @@ from qtpy.QtCore import Slot
5
5
  from qtpy.QtGui import QAction
6
6
  from qtpy.QtWidgets import QMenu
7
7
 
8
+ from qcanvas import icons
8
9
  from qcanvas.util import settings
9
10
 
10
11
  _logger = logging.getLogger(__name__)
@@ -36,6 +37,7 @@ class _EnableVideoDownloadOption(QAction):
36
37
 
37
38
  class AutoDownloadResourcesMenu(QMenu):
38
39
  def __init__(self, parent: Optional[QMenu] = None):
39
- super().__init__("Download new resources", parent)
40
+ super().__init__("Auto download resources", parent)
40
41
  self.addAction(_EnableAutoDownloadOption(self))
41
42
  self.addAction(_EnableVideoDownloadOption(self))
43
+ self.setIcon(icons.options.auto_download)
@@ -4,6 +4,7 @@ from qtpy.QtCore import Slot
4
4
  from qtpy.QtGui import QAction, QActionGroup
5
5
  from qtpy.QtWidgets import QMenu
6
6
 
7
+ from qcanvas import icons
7
8
  from qcanvas.util import settings, themes
8
9
 
9
10
  _logger = logging.getLogger(__name__)
@@ -38,6 +39,7 @@ class ThemeSelectionMenu(QMenu):
38
39
  actions = [auto_theme, light_theme, dark_theme, native_theme]
39
40
 
40
41
  self.addActions(actions)
42
+ self.setIcon(icons.options.theme)
41
43
 
42
44
  for theme in actions:
43
45
  action_group.addAction(theme)
@@ -9,7 +9,7 @@ from qcanvas_backend.database.data_monolith import DataMonolith
9
9
  from qcanvas_backend.net.sync.sync_receipt import SyncReceipt, empty_receipt
10
10
  from qcanvas_backend.qcanvas import QCanvas
11
11
  from qtpy.QtCore import QUrl, Signal, Slot
12
- from qtpy.QtGui import QDesktopServices, QIcon, QKeySequence
12
+ from qtpy.QtGui import QDesktopServices, QKeySequence
13
13
  from qtpy.QtWidgets import *
14
14
 
15
15
  from qcanvas import icons
@@ -23,7 +23,7 @@ from qcanvas.ui.main_ui.options.quick_sync_option import QuickSyncOption
23
23
  from qcanvas.ui.main_ui.options.sync_on_start_option import SyncOnStartOption
24
24
  from qcanvas.ui.main_ui.options.theme_selection_menu import ThemeSelectionMenu
25
25
  from qcanvas.ui.main_ui.status_bar_progress_display import StatusBarProgressDisplay
26
- from qcanvas.util import paths, settings, auto_downloader
26
+ from qcanvas.util import auto_downloader, paths, settings
27
27
  from qcanvas.util.qurl_util import file_url
28
28
  from qcanvas.util.ui_tools import create_qaction
29
29
 
@@ -37,7 +37,7 @@ class QCanvasWindow(QMainWindow):
37
37
  super().__init__()
38
38
 
39
39
  self.setWindowTitle("QCanvas")
40
- self.setWindowIcon(QIcon(icons.main_icon))
40
+ self.setWindowIcon(icons.branding.main_icon)
41
41
 
42
42
  self._operation_semaphore = BoundedSemaphore()
43
43
  self._data: Optional[DataMonolith] = None
@@ -74,6 +74,7 @@ class QCanvasWindow(QMainWindow):
74
74
  shortcut=QKeySequence("Ctrl+S"),
75
75
  triggered=self._synchronise_requested,
76
76
  parent=app_menu,
77
+ icon=icons.actions.sync,
77
78
  )
78
79
 
79
80
  create_qaction(
@@ -81,17 +82,22 @@ class QCanvasWindow(QMainWindow):
81
82
  shortcut=QKeySequence("Ctrl+D"),
82
83
  triggered=self._open_downloads_folder,
83
84
  parent=app_menu,
85
+ icon=icons.actions.open_downloads,
84
86
  )
85
87
 
86
88
  create_qaction(
87
- name="Quick canvas login",
89
+ name="Open Canvas in browser",
88
90
  shortcut=QKeySequence("Ctrl+O"),
89
91
  triggered=self._open_quick_auth_in_browser,
90
92
  parent=app_menu,
93
+ icon=icons.actions.quick_login,
91
94
  )
92
95
 
93
96
  create_qaction(
94
- name="Mark all as seen", triggered=self._clear_new_items, parent=app_menu
97
+ name="Mark all as seen",
98
+ triggered=self._clear_new_items,
99
+ parent=app_menu,
100
+ icon=icons.actions.mark_all_read,
95
101
  )
96
102
 
97
103
  create_qaction(
@@ -99,6 +105,7 @@ class QCanvasWindow(QMainWindow):
99
105
  shortcut=QKeySequence("Ctrl+Q"),
100
106
  triggered=lambda: self.close(),
101
107
  parent=app_menu,
108
+ icon=icons.actions.exit,
102
109
  )
103
110
 
104
111
  options_menu = menu_bar.addMenu("Options")
@@ -229,6 +236,12 @@ class QCanvasWindow(QMainWindow):
229
236
  @Slot()
230
237
  def _open_downloads_folder(self) -> None:
231
238
  directory = self._qcanvas.resource_manager.downloads_folder
239
+
240
+ if self._course_viewer_container.selected_course is not None:
241
+ directory /= self._qcanvas.resource_manager.course_folder_name(
242
+ self._course_viewer_container.selected_course
243
+ )
244
+
232
245
  directory.mkdir(parents=True, exist_ok=True)
233
246
 
234
247
  QDesktopServices.openUrl(file_url(directory))
@@ -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
- self._show_multiple_tasks_progress(list(self._tasks.values()))
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(self, tasks: list[_TaskProgress]) -> None:
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(f"{len(tasks)} tasks in progress")
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(self, tasks: list[_TaskProgress]) -> _TaskProgress:
103
- # Used to represent 0..1 progress as 0..multiplier
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
 
@@ -54,6 +54,7 @@ class TreeMemory:
54
54
  def set_expanded(self, node_id: str, expanded: bool) -> None:
55
55
  contains = node_id in self._state.collapsed_items
56
56
 
57
+ # fixme when using a slow usb stick, this can momentarily block the event loop.
57
58
  if expanded and contains:
58
59
  self._state.collapsed_items.remove(node_id)
59
60
  self._state.save()
@@ -17,7 +17,7 @@ class MemoryTreeWidget(QTreeWidget):
17
17
  parent: Optional[QWidget] = None,
18
18
  ):
19
19
  super().__init__(parent)
20
- self._id_map: dict[str, MemoryTreeWidgetItem] = {}
20
+ self._id_map: dict[str, QTreeWidgetItem] = {}
21
21
  self._memory = TreeMemory(tree_name)
22
22
  self._suppress_expansion_signals = False
23
23
  self._suppress_selection_signal = False
@@ -100,7 +100,7 @@ class MemoryTreeWidget(QTreeWidget):
100
100
  while len(widget_stack) > 0:
101
101
  item = widget_stack.pop()
102
102
 
103
- if isinstance(item, MemoryTreeWidgetItem):
103
+ if hasattr(item, "id"):
104
104
  if item.id in self._id_map or item.id in map_updates:
105
105
  raise ValueError(f"Item with ID {item.id} is already in the tree")
106
106
 
@@ -1,13 +1,13 @@
1
1
  import logging
2
2
 
3
3
  import qcanvas.util.settings as settings
4
- from qcanvas.util import is_url
4
+ from qcanvas.util.url_checker import is_url
5
5
 
6
6
  _logger = logging.getLogger(__name__)
7
7
 
8
8
 
9
9
  def needs_setup() -> bool:
10
- if not is_url(settings.client.panopto_url):
10
+ if not settings.client.panopto_disabled and not is_url(settings.client.panopto_url):
11
11
  return True
12
12
  elif not is_url(settings.client.canvas_url):
13
13
  return True
@@ -1,18 +1,19 @@
1
1
  import logging
2
2
  from threading import Semaphore
3
+ from typing import Optional
3
4
 
4
5
  from qasync import asyncSlot
5
6
  from qcanvas_api_clients.canvas import CanvasClient, CanvasClientConfig
6
7
  from qcanvas_api_clients.panopto import PanoptoClient, PanoptoClientConfig
7
8
  from qcanvas_api_clients.util.request_exceptions import ConfigInvalidError
8
- from qtpy.QtCore import QUrl, Signal, Slot
9
+ from qtpy.QtCore import Qt, QUrl, Signal, Slot
9
10
  from qtpy.QtGui import QDesktopServices, QIcon
10
11
  from qtpy.QtWidgets import *
11
12
 
12
13
  import qcanvas.util.settings as settings
13
14
  from qcanvas import icons
14
- from qcanvas.util import is_url
15
- from qcanvas.util.layouts import grid_layout_widget, layout
15
+ from qcanvas.util.layouts import GridItem, grid_layout_widget, layout
16
+ from qcanvas.util.url_checker import is_url
16
17
 
17
18
  _logger = logging.getLogger(__name__)
18
19
 
@@ -21,6 +22,68 @@ _tutorial_url = (
21
22
  )
22
23
 
23
24
 
25
+ class _InputRow:
26
+ def __init__(
27
+ self,
28
+ *,
29
+ label: str,
30
+ initial_value: str,
31
+ placeholder_text: Optional[str] = None,
32
+ is_password: bool = False,
33
+ ):
34
+ self._label = QLabel(label)
35
+ self._input = QLineEdit(initial_value)
36
+
37
+ if placeholder_text is not None:
38
+ self._input.setPlaceholderText(placeholder_text)
39
+
40
+ if is_password:
41
+ self._input.setEchoMode(QLineEdit.EchoMode.Password)
42
+
43
+ def set_error(self, message: Optional[str]) -> None:
44
+ self._input.setStyleSheet("QLineEdit { border: 1px solid red }")
45
+ self._input.setToolTip(message)
46
+
47
+ def clear_error(self) -> None:
48
+ self._input.setStyleSheet(None)
49
+ self._input.setToolTip(None)
50
+
51
+ def grid_row(self) -> list[QWidget]:
52
+ return [self._label, self._input]
53
+
54
+ def disable(self) -> None:
55
+ self._input.setEnabled(False)
56
+
57
+ @property
58
+ def enabled(self) -> bool:
59
+ return self._input.isEnabled()
60
+
61
+ @enabled.setter
62
+ def enabled(self, value: bool) -> None:
63
+ self._input.setEnabled(value)
64
+
65
+ @property
66
+ def text(self) -> str:
67
+ return self._input.text().strip()
68
+
69
+ @property
70
+ def url_text(self) -> str:
71
+ url = self.text
72
+
73
+ if not url.startswith("http"):
74
+ return "https://" + url
75
+ else:
76
+ return url
77
+
78
+ @property
79
+ def is_valid_url(self) -> bool:
80
+ return is_url(self.url_text)
81
+
82
+ @property
83
+ def is_empty(self) -> bool:
84
+ return len(self.text) == 0
85
+
86
+
24
87
  class SetupDialog(QDialog):
25
88
  closed = Signal()
26
89
 
@@ -30,41 +93,61 @@ class SetupDialog(QDialog):
30
93
  self.setWindowTitle("Configure QCanvas")
31
94
  self.setMinimumSize(550, 200)
32
95
  self.resize(550, 200)
33
- self.setWindowIcon(QIcon(icons.main_icon))
96
+ self.setWindowIcon(QIcon(icons.branding.main_icon))
34
97
 
35
98
  self._semaphore = Semaphore()
36
- self._canvas_url_box = QLineEdit(settings.client.canvas_url)
37
- self._canvas_url_box.setPlaceholderText("https://instance.canvas.com")
38
- self._canvas_api_key_box = QLineEdit(settings.client.canvas_api_key)
39
- self._canvas_api_key_box.setEchoMode(QLineEdit.EchoMode.Password)
40
- self._panopto_url_box = QLineEdit(settings.client.panopto_url)
41
- self._panopto_url_box.setPlaceholderText("https://instance.panopto.com")
99
+
100
+ self._canvas_url_box = _InputRow(
101
+ label="Canvas URL",
102
+ initial_value=settings.client.canvas_url,
103
+ placeholder_text="https://instance.canvas.com",
104
+ )
105
+ self._panopto_url_box = _InputRow(
106
+ label="Panopto URL",
107
+ initial_value=settings.client.panopto_url,
108
+ placeholder_text="https://instance.panopto.com",
109
+ )
110
+ self._canvas_api_key_box = _InputRow(
111
+ label="Canvas API Key",
112
+ initial_value=settings.client.canvas_api_key,
113
+ is_password=True,
114
+ )
115
+ self._disable_panopto_checkbox = QCheckBox("Continue without Panopto")
116
+ self._disable_panopto_checkbox.checkStateChanged.connect(
117
+ self._disable_panopto_check_changed
118
+ )
42
119
  self._button_box = self._setup_button_box()
43
- self._button_box.accepted.connect(self._accepted)
44
- self._button_box.helpRequested.connect(self._help_requested)
45
120
  self._waiting_indicator = self._setup_progress_bar()
46
- self._status_bar = QStatusBar()
47
121
 
48
122
  self.setLayout(
49
123
  layout(
50
124
  QVBoxLayout,
51
125
  grid_layout_widget(
52
126
  [
53
- [QLabel("Canvas URL"), self._canvas_url_box],
54
- [QLabel("Canvas API Key"), self._canvas_api_key_box],
55
- [QLabel("Panopto URL"), self._panopto_url_box],
127
+ self._canvas_url_box.grid_row(),
128
+ self._canvas_api_key_box.grid_row(),
129
+ self._panopto_url_box.grid_row(),
130
+ [
131
+ GridItem(
132
+ self._disable_panopto_checkbox,
133
+ col_span=2,
134
+ alignment=Qt.AlignmentFlag.AlignRight,
135
+ )
136
+ ],
56
137
  ]
57
138
  ),
58
139
  self._waiting_indicator,
59
140
  self._button_box,
60
- self._status_bar,
61
141
  )
62
142
  )
63
143
 
64
144
  def _setup_button_box(self) -> QDialogButtonBox:
65
145
  box = QDialogButtonBox()
66
146
  box.addButton(QDialogButtonBox.StandardButton.Ok)
67
- box.addButton("Get a Canvas API key", QDialogButtonBox.ButtonRole.HelpRole)
147
+ box.addButton("Get a Canvas API Key", QDialogButtonBox.ButtonRole.HelpRole)
148
+
149
+ box.accepted.connect(self._verify_settings)
150
+ box.helpRequested.connect(self._help_requested)
68
151
  return box
69
152
 
70
153
  def _setup_progress_bar(self) -> QProgressBar:
@@ -81,32 +164,33 @@ class SetupDialog(QDialog):
81
164
  widget.setSizePolicy(size_policy)
82
165
 
83
166
  @asyncSlot()
84
- async def _accepted(self) -> None:
167
+ async def _verify_settings(self) -> None:
85
168
  if self._semaphore.acquire(False):
86
169
  try:
87
170
  self._clear_errors()
88
171
 
89
- if not self._all_inputs_valid():
90
- self._status_bar.showMessage("Invalid input!", 5000)
172
+ if not self._check_all_inputs():
91
173
  return
92
174
 
93
175
  self._waiting_indicator.setVisible(True)
94
- self._status_bar.showMessage("Checking configuration...")
95
176
 
96
177
  canvas_config = CanvasClientConfig(
97
- api_token=self._canvas_api_key_box.text().strip(),
98
- canvas_url=self._get_url(self._canvas_url_box),
178
+ api_token=self._canvas_api_key_box.text,
179
+ canvas_url=self._canvas_url_box.url_text,
99
180
  )
100
181
 
101
182
  if not await self._check_canvas_config(canvas_config):
102
183
  return
103
184
 
104
- if not await self._check_panopto_config(canvas_config):
105
- self._show_panopto_help()
106
- return
185
+ if self._panopto_enabled:
186
+ if not await self._check_panopto_config(canvas_config):
187
+ self._show_panopto_help()
188
+ return
107
189
  except Exception as e:
108
- self._status_bar.showMessage(f"An error occurred: {e}", 5000)
109
190
  _logger.warning("Checking config failed", exc_info=e)
191
+
192
+ error_box = QErrorMessage(self)
193
+ error_box.showMessage(f"Checking config failed: {e}")
110
194
  finally:
111
195
  self._waiting_indicator.setVisible(False)
112
196
  self._semaphore.release()
@@ -117,59 +201,40 @@ class SetupDialog(QDialog):
117
201
  _logger.debug("Validation already in progress")
118
202
 
119
203
  def _clear_errors(self) -> None:
120
- for line_edit in [
121
- self._canvas_url_box,
122
- self._panopto_url_box,
123
- self._canvas_api_key_box,
124
- ]:
125
- self._status_bar.clearMessage()
126
- line_edit.setStyleSheet(None)
127
- line_edit.setToolTip(None)
128
-
129
- def _all_inputs_valid(self) -> bool:
204
+ self._canvas_url_box.clear_error()
205
+ self._panopto_url_box.clear_error()
206
+ self._canvas_api_key_box.clear_error()
207
+
208
+ def _check_all_inputs(self) -> bool:
130
209
  all_valid = True
131
210
 
132
- if not is_url(self._get_url(self._canvas_url_box)):
211
+ if not self._canvas_url_box.is_valid_url:
133
212
  all_valid = False
134
- self._show_error(self._canvas_url_box, "Canvas URL is invalid")
135
- if len(self._canvas_api_key_box.text().strip()) == 0:
213
+ self._canvas_url_box.set_error("Canvas URL is invalid")
214
+
215
+ if self._canvas_api_key_box.is_empty:
136
216
  all_valid = False
137
- self._show_error(self._canvas_api_key_box, "Canvas API key is empty")
138
- if not is_url(self._get_url(self._panopto_url_box)):
217
+ self._canvas_api_key_box.set_error("Canvas API key is empty")
218
+
219
+ if self._panopto_enabled and not self._panopto_url_box.is_valid_url:
139
220
  all_valid = False
140
- self._show_error(self._panopto_url_box, "Panopto URL is invalid")
221
+ self._panopto_url_box.set_error("Panopto URL is invalid")
141
222
 
142
223
  return all_valid
143
224
 
144
- def _get_url(self, line_edit: QLineEdit) -> str:
145
- url = line_edit.text().strip()
146
-
147
- if not url.startswith("http"):
148
- return "https://" + url
149
- else:
150
- return url
151
-
152
225
  async def _check_canvas_config(self, canvas_config: CanvasClientConfig) -> bool:
153
226
  try:
154
227
  await CanvasClient.verify_config(canvas_config)
155
228
  return True
156
229
  except ConfigInvalidError:
157
- self._show_error(self._canvas_api_key_box, "Canvas API key is invalid")
230
+ self._canvas_api_key_box.set_error("Canvas API key is invalid")
158
231
  return False
159
232
 
160
- def _show_error(self, line_edit: QLineEdit, text: str) -> None:
161
- line_edit.setToolTip(text)
162
- self._waiting_indicator.hide()
163
- self._highlight_line_edit(line_edit)
164
-
165
- def _highlight_line_edit(self, line_edit: QLineEdit) -> None:
166
- line_edit.setStyleSheet("QLineEdit { border: 1px solid red }")
167
-
168
233
  async def _check_panopto_config(self, canvas_config: CanvasClientConfig) -> bool:
169
234
  client = CanvasClient(canvas_config)
170
235
  try:
171
236
  await PanoptoClient.verify_config(
172
- PanoptoClientConfig(panopto_url=self._get_url(self._panopto_url_box)),
237
+ PanoptoClientConfig(panopto_url=self._panopto_url_box.url_text),
173
238
  client,
174
239
  )
175
240
  return True
@@ -194,15 +259,21 @@ class SetupDialog(QDialog):
194
259
 
195
260
  @Slot()
196
261
  def _open_panopto_login(self) -> None:
197
- url = QUrl(self._get_url(self._panopto_url_box))
262
+ url = QUrl(self._panopto_url_box.url_text)
198
263
  url.setPath("/Panopto/Pages/Auth/Login.aspx")
199
264
  url.setQuery("instance=Canvas&AllowBounce=true")
200
265
  QDesktopServices.openUrl(url)
201
266
 
202
267
  def _save_and_close(self) -> None:
203
- settings.client.canvas_url = self._get_url(self._canvas_url_box)
204
- settings.client.panopto_url = self._get_url(self._panopto_url_box)
205
- settings.client.canvas_api_key = self._canvas_api_key_box.text().strip()
268
+ settings.client.canvas_url = self._canvas_url_box.url_text
269
+
270
+ if self._panopto_enabled:
271
+ settings.client.panopto_url = self._panopto_url_box.url_text
272
+ else:
273
+ settings.client.panopto_disabled = True
274
+
275
+ settings.client.canvas_api_key = self._canvas_api_key_box.text
276
+
206
277
  self.closed.emit()
207
278
  self.close()
208
279
 
@@ -223,3 +294,11 @@ class SetupDialog(QDialog):
223
294
  @Slot()
224
295
  def _open_tutorial(self) -> None:
225
296
  QDesktopServices.openUrl(QUrl(_tutorial_url))
297
+
298
+ @Slot(Qt.CheckState)
299
+ def _disable_panopto_check_changed(self, state: Qt.CheckState) -> None:
300
+ self._panopto_url_box.enabled = state == Qt.CheckState.Unchecked
301
+
302
+ @property
303
+ def _panopto_enabled(self) -> bool:
304
+ return self._disable_panopto_checkbox.checkState() == Qt.CheckState.Unchecked
qcanvas/util/__init__.py CHANGED
@@ -1,2 +0,0 @@
1
- # todo remove this
2
- from .url_checker import is_url
@@ -4,8 +4,7 @@ import logging
4
4
  import qcanvas_backend.database.types as db
5
5
  from qcanvas_backend.net.resources.download.resource_manager import ResourceManager
6
6
  from qcanvas_backend.net.sync.sync_receipt import SyncReceipt
7
- from qtpy.QtWidgets import QMessageBox
8
- from qtpy.QtWidgets import QWidget
7
+ from qtpy.QtWidgets import QMessageBox, QWidget
9
8
 
10
9
  from qcanvas.util import settings
11
10