qcanvas 0.0.5.6a0__py3-none-any.whl → 1.0.3.post0__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.

Files changed (114) hide show
  1. qcanvas/app_start/__init__.py +47 -0
  2. qcanvas/backend_connectors/__init__.py +2 -0
  3. qcanvas/backend_connectors/frontend_resource_manager.py +63 -0
  4. qcanvas/backend_connectors/qcanvas_task_master.py +28 -0
  5. qcanvas/icons/__init__.py +6 -0
  6. qcanvas/icons/file-download-failed.svg +6 -0
  7. qcanvas/icons/file-downloaded.svg +6 -0
  8. qcanvas/icons/file-not-downloaded.svg +6 -0
  9. qcanvas/icons/file-unknown.svg +6 -0
  10. qcanvas/icons/icons.qrc +4 -0
  11. qcanvas/icons/main_icon.svg +7 -7
  12. qcanvas/icons/rc_icons.py +580 -214
  13. qcanvas/icons/sync.svg +7 -0
  14. qcanvas/run.py +29 -0
  15. qcanvas/ui/course_viewer/__init__.py +2 -0
  16. qcanvas/ui/course_viewer/content_tree.py +123 -0
  17. qcanvas/ui/course_viewer/course_tree.py +93 -0
  18. qcanvas/ui/course_viewer/course_viewer.py +62 -0
  19. qcanvas/ui/course_viewer/tabs/__init__.py +3 -0
  20. qcanvas/ui/course_viewer/tabs/assignment_tab/__init__.py +1 -0
  21. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tab.py +168 -0
  22. qcanvas/ui/course_viewer/tabs/assignment_tab/assignment_tree.py +104 -0
  23. qcanvas/ui/course_viewer/tabs/content_tab.py +96 -0
  24. qcanvas/ui/course_viewer/tabs/mail_tab/__init__.py +1 -0
  25. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tab.py +68 -0
  26. qcanvas/ui/course_viewer/tabs/mail_tab/mail_tree.py +70 -0
  27. qcanvas/ui/course_viewer/tabs/page_tab/__init__.py +1 -0
  28. qcanvas/ui/course_viewer/tabs/page_tab/page_tab.py +36 -0
  29. qcanvas/ui/course_viewer/tabs/page_tab/page_tree.py +74 -0
  30. qcanvas/ui/course_viewer/tabs/resource_rich_browser.py +176 -0
  31. qcanvas/ui/course_viewer/tabs/util.py +1 -0
  32. qcanvas/ui/main_ui/course_viewer_container.py +52 -0
  33. qcanvas/ui/main_ui/options/__init__.py +3 -0
  34. qcanvas/ui/main_ui/options/quick_sync_option.py +25 -0
  35. qcanvas/ui/main_ui/options/sync_on_start_option.py +25 -0
  36. qcanvas/ui/main_ui/qcanvas_window.py +192 -0
  37. qcanvas/ui/main_ui/status_bar_progress_display.py +153 -0
  38. qcanvas/ui/memory_tree/__init__.py +2 -0
  39. qcanvas/ui/memory_tree/_tree_memory.py +66 -0
  40. qcanvas/ui/memory_tree/memory_tree_widget.py +133 -0
  41. qcanvas/ui/memory_tree/memory_tree_widget_item.py +19 -0
  42. qcanvas/ui/setup/__init__.py +2 -0
  43. qcanvas/ui/setup/setup_checker.py +17 -0
  44. qcanvas/ui/setup/setup_dialog.py +212 -0
  45. qcanvas/util/__init__.py +2 -0
  46. qcanvas/util/basic_fonts.py +12 -0
  47. qcanvas/util/fe_resource_manager.py +23 -0
  48. qcanvas/util/html_cleaner.py +25 -0
  49. qcanvas/util/layouts.py +52 -0
  50. qcanvas/util/logs.py +6 -0
  51. qcanvas/util/paths.py +41 -0
  52. qcanvas/util/settings/__init__.py +9 -0
  53. qcanvas/util/settings/_client_settings.py +29 -0
  54. qcanvas/util/settings/_mapped_setting.py +63 -0
  55. qcanvas/util/settings/_ui_settings.py +34 -0
  56. qcanvas/util/ui_tools.py +41 -0
  57. qcanvas/util/url_checker.py +13 -0
  58. qcanvas-1.0.3.post0.dist-info/METADATA +61 -0
  59. qcanvas-1.0.3.post0.dist-info/RECORD +64 -0
  60. {qcanvas-0.0.5.6a0.dist-info → qcanvas-1.0.3.post0.dist-info}/WHEEL +1 -1
  61. qcanvas-1.0.3.post0.dist-info/entry_points.txt +3 -0
  62. qcanvas/__main__.py +0 -155
  63. qcanvas/db/__init__.py +0 -5
  64. qcanvas/db/database.py +0 -337
  65. qcanvas/db/db_converter_helper.py +0 -81
  66. qcanvas/net/canvas/__init__.py +0 -2
  67. qcanvas/net/canvas/canvas_client.py +0 -209
  68. qcanvas/net/canvas/legacy_canvas_types.py +0 -124
  69. qcanvas/net/custom_httpx_async_transport.py +0 -34
  70. qcanvas/net/self_authenticating.py +0 -108
  71. qcanvas/queries/__init__.py +0 -4
  72. qcanvas/queries/all_courses.gql +0 -7
  73. qcanvas/queries/all_courses.py +0 -108
  74. qcanvas/queries/canvas_course_data.gql +0 -51
  75. qcanvas/queries/canvas_course_data.py +0 -143
  76. qcanvas/ui/container_item.py +0 -11
  77. qcanvas/ui/main_ui.py +0 -249
  78. qcanvas/ui/menu_bar/__init__.py +0 -0
  79. qcanvas/ui/menu_bar/grouping_preferences_menu.py +0 -61
  80. qcanvas/ui/menu_bar/theme_selection_menu.py +0 -39
  81. qcanvas/ui/setup_dialog.py +0 -190
  82. qcanvas/ui/status_bar_reporter.py +0 -40
  83. qcanvas/ui/viewer/__init__.py +0 -0
  84. qcanvas/ui/viewer/course_list.py +0 -96
  85. qcanvas/ui/viewer/file_list.py +0 -195
  86. qcanvas/ui/viewer/file_view_tab.py +0 -62
  87. qcanvas/ui/viewer/page_list_viewer.py +0 -150
  88. qcanvas/util/app_settings.py +0 -98
  89. qcanvas/util/constants.py +0 -5
  90. qcanvas/util/course_indexer/__init__.py +0 -1
  91. qcanvas/util/course_indexer/conversion_helpers.py +0 -78
  92. qcanvas/util/course_indexer/data_manager.py +0 -447
  93. qcanvas/util/course_indexer/resource_helpers.py +0 -191
  94. qcanvas/util/download_pool.py +0 -58
  95. qcanvas/util/helpers/__init__.py +0 -0
  96. qcanvas/util/helpers/canvas_sanitiser.py +0 -47
  97. qcanvas/util/helpers/file_icon_helper.py +0 -34
  98. qcanvas/util/helpers/qaction_helper.py +0 -25
  99. qcanvas/util/helpers/theme_helper.py +0 -45
  100. qcanvas/util/link_scanner/__init__.py +0 -2
  101. qcanvas/util/link_scanner/canvas_link_scanner.py +0 -41
  102. qcanvas/util/link_scanner/canvas_media_object_scanner.py +0 -60
  103. qcanvas/util/link_scanner/dropbox_scanner.py +0 -68
  104. qcanvas/util/link_scanner/resource_scanner.py +0 -69
  105. qcanvas/util/progress_reporter.py +0 -101
  106. qcanvas/util/self_updater.py +0 -55
  107. qcanvas/util/task_pool.py +0 -253
  108. qcanvas/util/tree_util/__init__.py +0 -3
  109. qcanvas/util/tree_util/expanding_tree.py +0 -165
  110. qcanvas/util/tree_util/model_helpers.py +0 -36
  111. qcanvas/util/tree_util/tree_model.py +0 -85
  112. qcanvas-0.0.5.6a0.dist-info/METADATA +0 -21
  113. qcanvas-0.0.5.6a0.dist-info/RECORD +0 -61
  114. /qcanvas/{net → ui/main_ui}/__init__.py +0 -0
@@ -1,101 +0,0 @@
1
- import itertools
2
- from abc import ABC, abstractmethod
3
- from typing import Any
4
-
5
-
6
- class ProgressSection:
7
- """
8
- Convenience class for reporting on a section of a larger task
9
- """
10
-
11
- def __init__(self, section_name: str, total_progress: int, reporter: "ProgressReporter"):
12
- self.total_progress = total_progress
13
- self.reporter = reporter
14
- self._counter = itertools.count(1) # Start at 1 instead of 0
15
- self.reporter.section_started(section_name, total_progress)
16
-
17
- def increment_progress(self, *args):
18
- self.reporter.progress(next(self._counter), self.total_progress)
19
-
20
-
21
- class ProgressReporter(ABC):
22
- """
23
- A progress reporter is passed to a function which then uses it to report the progress for whatever task it performs to another place.
24
- Only 1 section should be active at a time.
25
- """
26
-
27
- @abstractmethod
28
- def section_started(self, section_name: str, total_progress: int) -> None:
29
- """
30
- Signals that a new section of a task has started
31
- Parameters
32
- ----------
33
- section_name
34
- The name of the new task section
35
- """
36
- ...
37
-
38
- @abstractmethod
39
- def progress(self, current_progress: int, total_progress: int) -> None:
40
- """
41
- Updates the current progress of the current task
42
- Parameters
43
- ----------
44
- current_progress
45
- The current amount of progress, e.g. the number of bytes of a file that have been downloaded
46
- total_progress
47
- The total or final amount of progress, e.g. the size of a download
48
- """
49
- ...
50
-
51
- @abstractmethod
52
- def finished(self) -> None:
53
- """
54
- Signals that the task is finished and there is nothing left to do
55
- """
56
- ...
57
-
58
- @abstractmethod
59
- def errored(self, context: Any) -> None:
60
- """
61
- Signals that the task could not be completed
62
- Parameters
63
- ----------
64
- context
65
- Any information about why the task failed
66
- """
67
- ...
68
-
69
- def section(self, section_name: str, total_progress: int) -> ProgressSection:
70
- """
71
- Creates a ProgressSection for this reporter, for reporting on a section of a larger task
72
- Parameters
73
- ----------
74
- section_name
75
- The name of the section
76
- total_progress
77
- The total or final amount of progress, e.g. the size of a download
78
- Returns
79
- -------
80
- ProgressSection
81
- The created section
82
- """
83
- return ProgressSection(section_name, total_progress, self)
84
-
85
-
86
- class _NoopReporter(ProgressReporter):
87
-
88
- def section_started(self, section_name: str, total_progress: int) -> None:
89
- pass
90
-
91
- def progress(self, current_progress: int, total: int) -> None:
92
- pass
93
-
94
- def finished(self) -> None:
95
- pass
96
-
97
- def errored(self, context: Any) -> None:
98
- pass
99
-
100
-
101
- noop_reporter = _NoopReporter()
@@ -1,55 +0,0 @@
1
- import asyncio
2
- import json
3
- import os
4
- from importlib.metadata import version
5
-
6
- import httpx
7
- from packaging.version import Version
8
-
9
- from qcanvas.util.constants import package_name
10
-
11
- # When true, signals that the program should be restarted when it closes next
12
- restart_flag = False
13
-
14
-
15
- async def do_update() -> None:
16
- """
17
- Updates the qcanvas package and sets the restart flag.
18
- The restart flag is passed back to the launcher script as a return code.
19
- """
20
- global restart_flag
21
- await asyncio.to_thread(os.system, f"pip install --upgrade {package_name}")
22
- restart_flag = True
23
-
24
-
25
- async def get_newer_version() -> tuple[Version | None, Version | None] | None:
26
- """
27
- Check for a newer version of qcanvas
28
- Returns
29
- -------
30
- tuple
31
- A tuple, where the first item is the latest version and the second item is the installed version. If the installed
32
- version is up-to-date, then the first item is None
33
- """
34
- latest_version = await get_latest_version()
35
- installed_version = Version(version(package_name))
36
-
37
- print(f"latest = {latest_version}, installed = {installed_version}")
38
-
39
- if installed_version < latest_version:
40
- return latest_version, installed_version
41
- else:
42
- return None, installed_version
43
-
44
-
45
- async def get_latest_version() -> Version:
46
- """
47
- Retrieves the latest version of the package from pypi
48
- Returns
49
- -------
50
- Version
51
- The latest version of qcanvas on pypi
52
- """
53
- async with httpx.AsyncClient() as client:
54
- data = json.loads((await client.get(f"https://pypi.org/pypi/{package_name}/json")).text)
55
- return Version(data["info"]["version"])
qcanvas/util/task_pool.py DELETED
@@ -1,253 +0,0 @@
1
- import asyncio
2
- import logging
3
- from typing import TypeVar, Generic, Callable
4
-
5
- T = TypeVar("T")
6
-
7
-
8
- class TaskPool(Generic[T]):
9
- """
10
- A TaskPool is a utility that receives submitted tasks which have an identity and which should be executed no more
11
- than once. It can be configured to wait for the task to finish when started, wait for the task to finish when it
12
- is in progress, and record the result of the task when it finishes and return it when a task with the same ID is
13
- submitted again.
14
- """
15
-
16
- _logger = logging.getLogger(__name__)
17
-
18
- def __init__(self, remember_result: bool = True, wait_if_in_progress: bool = True,
19
- wait_if_just_started: bool = True, restart_if_finished: bool = False):
20
- """
21
- Parameters
22
- ----------
23
- remember_result : bool
24
- Whether to store the result of submitted tasks
25
-
26
- wait_if_just_started : bool
27
- Whether to await a newly started task or return immediately
28
-
29
- wait_if_in_progress : bool
30
- Whether to await a task that is in progress or return immediately
31
- """
32
-
33
- if not wait_if_in_progress and remember_result:
34
- raise ValueError("Can't remember result without waiting")
35
-
36
- self._results: dict[object, asyncio.Event | T | None] = {}
37
- self._semaphore = asyncio.Semaphore()
38
- self._remember_result: bool = remember_result
39
- self._wait_if_in_progress: bool = wait_if_in_progress
40
- self._wait_if_just_started: bool = wait_if_just_started
41
- self._restart_if_finished: bool = restart_if_finished
42
-
43
- def add_values(self, results: dict[object, T]) -> None:
44
- """
45
- Adds the specified values from the dictionary as stored results.
46
-
47
- Parameters
48
- ----------
49
- results
50
- The results to store, where the key is the ID and the value is the result
51
-
52
- Returns
53
- -------
54
- None
55
- """
56
- self._results.update(**results)
57
-
58
- async def submit(self, task_id: object, func, **kwargs) -> T | None:
59
- """
60
- Submits a task and executes it. Depending on configuration, waits for it and returns the result of it.
61
-
62
- Parameters
63
- ----------
64
- task_id: object
65
- The identity of the task. E.g. "task_get_file_1" or "task_get_file_2"
66
-
67
- func : lambda
68
- The function to execute.
69
- Should be provided in the form of a lambda which executes some async function without using the await keyword.
70
- E.g::
71
- # Right
72
- taskpool.submit(..., func=lambda: some_async_function(some_val))
73
- # Wrong
74
- taskpool.submit(..., func=lambda: async some_async_function(some_val))
75
- # ^^^^^
76
- kwargs
77
- Extra arguments for the lambda
78
- Returns
79
- -------
80
- T | None
81
- None, if the configuration states that we should not wait for the function to finish or not to remember the result.
82
- Returns the result of the function otherwise.
83
- """
84
- sem = self._semaphore
85
- await sem.acquire()
86
-
87
- if task_id in self._results.keys():
88
- if not self._wait_if_in_progress and isinstance(self._results[task_id], asyncio.Event):
89
- self._logger.debug("Task %s in progress but configured to not wait, returning None.", task_id)
90
- sem.release()
91
- return None
92
-
93
- if not isinstance(self._results[task_id], asyncio.Event):
94
- if self._restart_if_finished:
95
- self._logger.debug("Task %s already finished but configured to restart if finished, restarting.",
96
- task_id)
97
- # start_task releases the semaphore, no need to do it here
98
- return await self._start_task(task_id, func, **kwargs)
99
-
100
- self._logger.debug("Task %s already finished, returning.", task_id)
101
- sem.release()
102
- return self._results[task_id]
103
-
104
- self._logger.debug("Task %s in progress. Waiting.", task_id)
105
-
106
- event: asyncio.Event = self._results[task_id]
107
- sem.release()
108
-
109
- await event.wait()
110
-
111
- self._logger.debug("Finished waiting for %s.", task_id)
112
-
113
- return self._results[task_id]
114
- else:
115
- # start_task releases the semaphore, no need to do it here
116
- return await self._start_task(task_id, func, **kwargs)
117
-
118
- async def _start_task(self, task_id: object, func, **kwargs):
119
- """
120
- Starts a task and **releases the results list semaphore**
121
-
122
- Parameters
123
- ----------
124
- task
125
- The task to start
126
- task_id
127
- The ID of the task
128
- event
129
- The event that the task is attached to
130
-
131
- Returns
132
- -------
133
- T
134
- The result of the task
135
- """
136
- sem = self._semaphore
137
-
138
- self._logger.debug("Task %s started.", task_id)
139
-
140
- event = asyncio.Event()
141
- self._results[task_id] = event
142
- sem.release()
143
-
144
- if not self._wait_if_just_started:
145
- # noinspection PyAsyncCall
146
- asyncio.create_task(self._handle_task(func, task_id, event, func_args=kwargs))
147
- return None
148
-
149
- return await self._handle_task(func, task_id, event, func_args=kwargs)
150
-
151
- async def _handle_task(self, func: Callable, task_id: object, event: asyncio.Event, func_args: dict) -> T:
152
- """
153
- Handles the specified task
154
-
155
- Parameters
156
- ----------
157
- func
158
- The task to handle
159
- task_id
160
- The ID of the task
161
- event
162
- The event that the task is attached to
163
-
164
- Returns
165
- -------
166
- T
167
- The result of the task
168
- """
169
- sem = self._semaphore
170
-
171
- result = await func(**func_args)
172
-
173
- if isinstance(result, asyncio.Event):
174
- self._logger.warning("Result was of type asyncio.Event, this will break things!")
175
-
176
- async with sem:
177
- self._logger.debug("Task %s finished", task_id)
178
- # Record this task as done, storing the result if configured to
179
- self._results[task_id] = result if self._remember_result else None
180
- event.set()
181
-
182
- return result
183
-
184
- async def wait_if_in_progress(self, task_id: object):
185
- """
186
- Waits for a task if it is in progress. Returns immediately otherwise.
187
-
188
- Parameters
189
- ----------
190
- task_id
191
- The task id to wait for
192
- """
193
- sem = self._semaphore
194
-
195
- await sem.acquire()
196
-
197
- if task_id in self._results and isinstance(self._results[task_id], asyncio.Event):
198
- event: asyncio.Event = self._results[task_id]
199
- sem.release()
200
-
201
- await event.wait()
202
-
203
- def clear(self) -> None:
204
- """
205
- Deletes the stored results
206
-
207
- Returns
208
- -------
209
- None
210
- """
211
- self._results.clear()
212
-
213
- def results(self) -> list[T]:
214
- """
215
- Gets the results of all currently completed tasks
216
-
217
- Returns
218
- -------
219
- list[T]
220
- The results of all currently completed tasks, excluding Nones
221
- """
222
-
223
- def filter_func(it):
224
- return not isinstance(it, asyncio.Event) and it is not None
225
-
226
- return list(filter(filter_func, self._results.values()))
227
-
228
- def get_completed_result_or_nothing(self, task_id: object, default: T | None = None) -> T | None:
229
- """
230
- Returns the result of an already completed task, or nothing at all if result with that id exists or is still in progress
231
-
232
- Returns
233
- -------
234
- object
235
- The result of the task
236
-
237
- Raises
238
- ------
239
- ValueError
240
- If the task is still in progress
241
-
242
- KeyError
243
- If the task has not been started yet or does not exist
244
- """
245
- if task_id in self._results:
246
- result = self._results[task_id]
247
-
248
- if isinstance(result, asyncio.Event):
249
- return None
250
-
251
- return result
252
- else:
253
- return None
@@ -1,3 +0,0 @@
1
- from .expanding_tree import ExpandingTreeView
2
- from .model_helpers import HasColumnData, HasParent, HasChildren, HasText
3
- from .tree_model import TreeModel
@@ -1,165 +0,0 @@
1
- from typing import Any, TypeVar, Sequence, Optional
2
-
3
- from PySide6.QtCore import QModelIndex, Slot, QItemSelectionModel
4
- from PySide6.QtWidgets import QTreeView, QWidget
5
-
6
- from .model_helpers import HasChildren, HasParent
7
- from .tree_model import TreeModel
8
-
9
- T = TypeVar("T")
10
-
11
-
12
- class ExpandingTreeView(QTreeView):
13
- """
14
- Provides a way to retain the collapsed or expanded state of tree items after a model reset. Expects the tree's
15
- model to implement `AbstractItemModelHasListRoot` and any children to implement `HasChildren`. Also provides
16
- some methods that can be used to get a model index for an arbitrary object in a tree. These will NOT work when
17
- duplicate objects are present in the tree. Does NOT support multi-selection.
18
- """
19
-
20
- def __init__(self, parent: Optional[QWidget] = None):
21
- """
22
- Constructor
23
- :param session_factory: The session that will be used to update an item's expanded/collapsed state when it is changed
24
- :param parent: The parent of this treeview
25
- """
26
- super().__init__(parent)
27
- self._connect_expanded_listeners()
28
-
29
- def reexpand(self) -> None:
30
- """
31
- Re-expands all items in the tree that have children based on their saved collapsed/expanded state.
32
- """
33
- self._disconnect_expanded_listeners()
34
-
35
- try:
36
- root: Sequence[Any] = self.model().get_root()
37
-
38
- # Go through every item in the root and reexpand it and any children it has
39
- for index, item in enumerate(root):
40
- if isinstance(item, HasChildren):
41
- self._reexpand_recur(item, self.model().index(index, 0, QModelIndex()))
42
- finally:
43
- self._connect_expanded_listeners()
44
-
45
- def _connect_expanded_listeners(self):
46
- self.expanded.connect(self.tree_item_expanded)
47
- self.collapsed.connect(self.tree_item_collapsed)
48
-
49
- def _disconnect_expanded_listeners(self):
50
- self.expanded.disconnect(self.tree_item_expanded)
51
- self.collapsed.disconnect(self.tree_item_collapsed)
52
-
53
- def _reexpand_recur(self, item: HasChildren, index: QModelIndex) -> None:
54
- """
55
- Internal function to expand an item in the tree. Recurs to any children it has.
56
- :param item: The object to expand in the tree
57
- :param index: The index of that object
58
- """
59
- # Sanity check
60
- if not isinstance(item, HasChildren):
61
- raise TypeError("item must be an instance of HasChildren")
62
-
63
- # Expand this item
64
- self.setExpanded(index, not item.collapsed)
65
-
66
- for child_index, child in enumerate(item.get_children()):
67
- # Check if the child item can also have children and be collapsed. Also check that it actually has
68
- # any children.
69
- if isinstance(child, HasChildren) and len(child.get_children()) > 0:
70
- # Expand it and any children it has
71
- self._reexpand_recur(child, self.model().index(child_index, 0, index))
72
-
73
- @Slot()
74
- def tree_item_expanded(self, index: QModelIndex) -> None:
75
- """
76
- Slot that is connected to the tree's `expanded` signal
77
- :param index: The index that has been expanded
78
- """
79
- item = self.model().get_item(index)
80
-
81
- # Update the item's state
82
- if isinstance(item, HasChildren):
83
- item.collapsed = False
84
-
85
- @Slot()
86
- def tree_item_collapsed(self, index: QModelIndex) -> None:
87
- """
88
- Slot that is connected to the tree's `collapsed` signal
89
- :param index: The index that was collapsed
90
- """
91
- item = self.model().get_item(index)
92
-
93
- # Update the item's state
94
- if isinstance(item, HasChildren):
95
- item.collapsed = True
96
-
97
- def get_path_of_indexes_for_item(self, item: Any) -> list[int]:
98
- """
99
- Gets the path of numerical indexes for an arbitrary object in a tree.
100
- For example, second child of the first item would be [0, 1].
101
-
102
- :param item: The item to find the path for. The item must belong to the tree
103
- :return: The path to the item as a list of indexes to follow
104
- """
105
- path: list[int] = []
106
-
107
- # Keep going until we reach the root
108
- while isinstance(item, HasParent) and item.parent is not None:
109
- # Add the index of the item to the path and then go to the parent of that child until there is no parent
110
- path.insert(0, item.index_of_self)
111
- item = item.parent
112
-
113
- # Insert the index of the item belonging to the root list to the path
114
- path.insert(0, self.model().get_root().index(item))
115
-
116
- return path
117
-
118
- def select_object(self, item: Any) -> None:
119
- """
120
- Selects an arbitrary item that belongs to the tree. Will not work for branches that have duplicate items.
121
- :param item: The item to select. Must belong to the tree
122
- """
123
- self.select_item_by_path(self.get_path_of_indexes_for_item(item))
124
-
125
- def select_item_by_path(self, item_path: list) -> None:
126
- """
127
- Selects an item based on its path
128
- :param item_path: The path of the item
129
- """
130
- selection_model = self.selectionModel()
131
-
132
- # Convert the path to a model index
133
- # Clear previous selection and select the whole row
134
- selection_model.select(self.get_model_index_for_path(item_path),
135
- QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows)
136
-
137
- def get_model_index_for_path(self, item_path: list[int]) -> QModelIndex:
138
- """
139
- Converts an item's path into a QModelIndex
140
- :param item_path: The path of the item to convert
141
- :return: The path as a QModelIndex
142
- """
143
- model: TreeModel[Any] = self.model()
144
- # Get the first index
145
- item_index = item_path.pop(0)
146
- # Create the base QModelIndex
147
- model_index = model.index(item_index, 0, QModelIndex())
148
- # Get the children of the first item
149
- children = model.get_root()[item_index]
150
-
151
- while len(item_path) > 0:
152
- # Get the next item
153
- item_index = item_path.pop(0)
154
- # Update the QModelIndex
155
- model_index = model.index(item_index, 0, model_index)
156
-
157
- if len(item_path) > 0:
158
- # Sanity check
159
- if not isinstance(children, HasChildren):
160
- raise TypeError("Parent object does not have any children but a child was expected")
161
-
162
- # Go to the children of this child next
163
- children = children.children[item_index]
164
-
165
- return model_index
@@ -1,36 +0,0 @@
1
- from typing import Sequence, Any
2
-
3
-
4
- class HasColumnData:
5
- def get_column_data(self, column: int, role: int) -> str | None:
6
- raise NotImplementedError()
7
-
8
-
9
- class HasText:
10
- @property
11
- def text(self) -> str:
12
- raise NotImplementedError()
13
-
14
-
15
- class HasParent:
16
- @property
17
- def parent(self) -> Any:
18
- raise NotImplementedError()
19
-
20
- @property
21
- def index_of_self(self) -> int:
22
- raise NotImplementedError()
23
-
24
-
25
- class HasChildren:
26
- @property
27
- def collapsed(self) -> bool:
28
- raise NotImplementedError()
29
-
30
- @collapsed.setter
31
- def collapsed(self, value: bool):
32
- raise NotImplementedError()
33
-
34
- @property
35
- def children(self) -> Sequence[HasColumnData]:
36
- raise NotImplementedError()
@@ -1,85 +0,0 @@
1
- from typing import Any, Sequence, TypeVar, Generic, Optional
2
-
3
- from PySide6.QtCore import QAbstractItemModel, QModelIndex, QPersistentModelIndex
4
- from PySide6.QtWidgets import QWidget
5
-
6
- from .model_helpers import HasColumnData, HasParent, HasChildren
7
-
8
- T = TypeVar("T")
9
-
10
-
11
- # todo comment this
12
- class TreeModel(QAbstractItemModel, Generic[T]):
13
- def __init__(self, parent: Optional[QWidget] = None):
14
- super().__init__(parent)
15
- self.root: Sequence[T] = []
16
-
17
- def get_item(self, index: QModelIndex | QPersistentModelIndex) -> object:
18
- if index.isValid():
19
- return index.internalPointer()
20
-
21
- return self.root
22
-
23
- def data(self, index: QModelIndex | QPersistentModelIndex, role: int = ...) -> Any:
24
- if not index.isValid():
25
- return None
26
-
27
- item = self.get_item(index)
28
-
29
- if isinstance(item, HasColumnData):
30
- return item.get_column_data(index.column(), role)
31
- else:
32
- return None
33
-
34
- def index(self, row: int, column: int, parent: QModelIndex | QPersistentModelIndex = ...) -> QModelIndex:
35
- if parent.isValid() and parent.column() != 0:
36
- return QModelIndex()
37
-
38
- parent_item = self.get_item(parent)
39
-
40
- if isinstance(parent_item, HasChildren):
41
- parent_list = parent_item.children
42
- elif isinstance(parent_item, Sequence):
43
- parent_list = parent_item
44
- else:
45
- raise TypeError(
46
- f"Expected parent of item to be Sequence or HasChildren, actually {parent_item.__class__}")
47
-
48
- if row > len(parent_list) or row < 0:
49
- return QModelIndex()
50
-
51
- return self.createIndex(row, column, parent_list[row])
52
-
53
- def parent(self, child: QModelIndex | QPersistentModelIndex = QModelIndex()) -> QModelIndex:
54
- if not child.isValid():
55
- return QModelIndex()
56
-
57
- child_item = self.get_item(child)
58
-
59
- # We don't need to handle the root items here because... they are the root and have no parents
60
-
61
- if isinstance(child_item, HasParent) and child_item.parent is not None:
62
- return self.createIndex(child_item.index_of_self, 0, child_item.parent)
63
-
64
- return QModelIndex()
65
-
66
- def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
67
- if parent.isValid() and parent.column() > 0:
68
- return 0
69
-
70
- parent_item = self.get_item(parent)
71
-
72
- if isinstance(parent_item, HasChildren):
73
- return len(parent_item.children)
74
- elif isinstance(parent_item, Sequence):
75
- return len(parent_item)
76
- else:
77
- return 0
78
-
79
- def get_root(self) -> Sequence[T]:
80
- """
81
- Returns the list of root level items (i.e. items which have no parents) for the tree
82
- :return: List of root level items (i.e. items which have no parents) for the tree
83
- :return: List of root level items (i.e. items which have no parents) for the tree
84
- """
85
- return self.root