Skip to content

Commit 655b580

Browse files
committed
Aliases appear in Entity Tree
1 parent 2b32c25 commit 655b580

File tree

9 files changed

+405
-9
lines changed

9 files changed

+405
-9
lines changed

src/crystal/browser/__init__.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from crystal.browser.preferences import PreferencesDialog
1212
from crystal.browser.tasktree import TaskTree, TaskTreeNode
1313
from crystal.model import (
14-
Project, ProjectReadOnlyError, Resource, ResourceGroup, ResourceGroupSource, RootResource,
14+
Alias, Project, ProjectReadOnlyError, Resource, ResourceGroup, ResourceGroupSource, RootResource,
1515
)
1616
from crystal.progress import (
1717
CancelLoadUrls, CancelSaveAs, DummyOpenProjectProgressListener,
@@ -815,7 +815,8 @@ def _update_entity_pane_empty_state_visibility(self) -> bool:
815815
"""
816816
is_project_empty = (
817817
is_iterable_empty(self.project.root_resources) and
818-
is_iterable_empty(self.project.resource_groups)
818+
is_iterable_empty(self.project.resource_groups) and
819+
is_iterable_empty(self.project.aliases)
819820
)
820821

821822
sizer_index = self._entity_tree_sizer_index # cache
@@ -1498,6 +1499,7 @@ def _on_forget_entity(self, event) -> None:
14981499
def _on_download_entity(self, event) -> None:
14991500
selected_entity = self.entity_tree.selected_entity
15001501
assert selected_entity is not None
1502+
assert not isinstance(selected_entity, Alias)
15011503

15021504
# Show progress dialog in advance if will need to load all project URLs
15031505
if isinstance(selected_entity, ResourceGroup):
@@ -1612,21 +1614,33 @@ def resource_group_did_instantiate(self, group: ResourceGroup) -> None:
16121614
def resource_group_did_forget(self, group: ResourceGroup) -> None:
16131615
self._update_entity_pane_empty_state_visibility()
16141616

1617+
# NOTE: Can't capture to the Entity Tree itself reliably since may not be visible
1618+
@capture_crashes_to_stderr
1619+
@cloak
1620+
def alias_did_instantiate(self, alias: Alias) -> None:
1621+
self._update_entity_pane_empty_state_visibility()
1622+
1623+
# NOTE: Can't capture to the Entity Tree itself reliably since may not be visible
1624+
@capture_crashes_to_stderr
1625+
@cloak
1626+
def alias_did_forget(self, alias: Alias) -> None:
1627+
self._update_entity_pane_empty_state_visibility()
1628+
16151629
def _on_selected_entity_changed(self, event: wx.TreeEvent | None=None) -> None:
16161630
selected_entity = self.entity_tree.selected_entity # cache
16171631

16181632
readonly = self._readonly # cache
16191633
self._edit_action.enabled = (
1620-
isinstance(selected_entity, (ResourceGroup, RootResource)))
1634+
isinstance(selected_entity, (Alias, ResourceGroup, RootResource)))
16211635
self._forget_action.enabled = (
16221636
(not readonly) and
1623-
isinstance(selected_entity, (ResourceGroup, RootResource)))
1637+
isinstance(selected_entity, (Alias, ResourceGroup, RootResource)))
16241638
self._update_members_action.enabled = (
16251639
(not readonly) and
16261640
isinstance(selected_entity, ResourceGroup))
16271641
self._download_action.enabled = (
16281642
(not readonly) and
1629-
selected_entity is not None)
1643+
isinstance(selected_entity, (Resource, ResourceGroup, RootResource)))
16301644
self._view_action.enabled = (
16311645
isinstance(selected_entity, (Resource, RootResource)))
16321646

src/crystal/browser/entitytree.py

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from crystal.doc.generic import Link
88
from crystal.doc.html.soup import TEXT_LINK_TYPE_TITLE
99
from crystal.model import (
10-
Project, ProjectHasTooManyRevisionsError, Resource, ResourceGroup,
10+
Alias, Project, ProjectHasTooManyRevisionsError, Resource, ResourceGroup,
1111
ResourceGroupSource, ResourceRevision, RevisionBodyMissingError,
1212
RevisionDeletedError, RootResource,
1313
)
@@ -306,6 +306,39 @@ def resource_group_did_forget(self, group: ResourceGroup) -> None:
306306
# may need to be updated
307307
self.root.update_icon_set_of_descendants_in_group(group)
308308

309+
# === Events: Alias Lifecycle ===
310+
311+
@capture_crashes_to_self
312+
def alias_did_instantiate(self, alias: Alias) -> None:
313+
selection_was_empty = self.view.selected_node in [None, self.view.root] # capture
314+
315+
self.update()
316+
317+
if selection_was_empty:
318+
# Select the newly-created entity
319+
for child in self.root.children:
320+
if child.entity == alias:
321+
child.view.peer.SelectItem()
322+
break
323+
324+
@capture_crashes_to_self
325+
# TODO: Do not recommend asserting that a listener method will be called
326+
# from any particular thread
327+
@fg_affinity
328+
def alias_did_change(self, alias: Alias) -> None:
329+
# Update the title of the AliasNode
330+
for child in self.root.children:
331+
if isinstance(child, AliasNode) and child.alias == alias:
332+
child.view.title = child.calculate_title()
333+
break
334+
335+
@capture_crashes_to_self
336+
# TODO: Do not recommend asserting that a listener method will be called
337+
# from any particular thread
338+
@fg_affinity
339+
def alias_did_forget(self, alias: Alias) -> None:
340+
self.update()
341+
309342
# === Event: Min Fetch Date Did Change ===
310343

311344
@capture_crashes_to_self
@@ -524,7 +557,7 @@ def _sequence_with_matching_elements_replaced(new_seq, old_seq):
524557
return [old_seq_selfdict.get(x, x) for x in new_seq]
525558

526559

527-
NodeEntity: TypeAlias = Union['RootResource', 'Resource', 'ResourceGroup']
560+
NodeEntity: TypeAlias = Union['RootResource', 'Resource', 'ResourceGroup', 'Alias']
528561

529562

530563
class Node(Bulkhead):
@@ -771,6 +804,9 @@ def update_children(self,
771804
for (index, rg) in enumerate(self._project.resource_groups):
772805
children.append(ResourceGroupNode(rg))
773806

807+
for alias in self._project.aliases:
808+
children.append(AliasNode(alias))
809+
774810
self.set_children(children, progress_listener)
775811

776812

@@ -1013,7 +1049,7 @@ def update_children(self) -> None:
10131049
if self.resource_links:
10141050
for link in self.resource_links:
10151051
url = urljoin(self.resource.url, link.relative_url)
1016-
resource = Resource(self._project, url)
1052+
resource = Resource(self._project, url, _external_ok=True)
10171053
resources_2_links[resource].append(link)
10181054

10191055
linked_root_resources = []
@@ -1556,6 +1592,80 @@ def __hash__(self) -> int:
15561592
return hash(self.resource_group)
15571593

15581594

1595+
class AliasNode(Node):
1596+
"""Node representing an Alias in the entity tree."""
1597+
entity_tooltip = 'alias'
1598+
1599+
ICON = '🔗'
1600+
ICON_TRUNCATION_FIX = ''
1601+
1602+
def __init__(self, alias: Alias) -> None:
1603+
super().__init__(source=None)
1604+
self.alias = alias
1605+
1606+
self.view = NodeView()
1607+
# NOTE: Defer expensive calculation until if/when the icon_set is used
1608+
self.view.icon_set = self.calculate_icon_set
1609+
self.view.title = self.calculate_title()
1610+
self.view.expandable = False
1611+
1612+
# Aliases have no children
1613+
self.children = []
1614+
1615+
# === Properties ===
1616+
1617+
@override
1618+
def calculate_icon_set(self) -> IconSet | None:
1619+
return (
1620+
(wx.TreeItemIcon_Normal, TREE_NODE_ICONS()['entitytree_alias']),
1621+
)
1622+
1623+
@override
1624+
@property
1625+
def icon_tooltip(self) -> str | None:
1626+
return self.entity_tooltip.capitalize()
1627+
1628+
@override
1629+
def calculate_title(self) -> str:
1630+
return self.calculate_title_of(self.alias)
1631+
1632+
@staticmethod
1633+
def calculate_title_of(alias: Alias) -> str:
1634+
target_url_prefix = alias.target_url_prefix
1635+
if alias.target_is_external:
1636+
target_url_prefix = Alias.format_external_url_for_display(target_url_prefix)
1637+
assert alias.source_url_prefix.endswith('/')
1638+
assert target_url_prefix.endswith('/')
1639+
return f'{alias.source_url_prefix}** → {target_url_prefix}**'
1640+
1641+
@override
1642+
@property
1643+
def label_tooltip(self) -> str:
1644+
tooltip = (
1645+
f'Source: {self.alias.source_url_prefix}**\n'
1646+
f'Target: {self.alias.target_url_prefix}**'
1647+
)
1648+
if self.alias.target_is_external:
1649+
tooltip += '\nExternal: Yes (on internet, outside project)'
1650+
return tooltip
1651+
1652+
@override
1653+
@property
1654+
def entity(self) -> Alias:
1655+
return self.alias
1656+
1657+
# === Comparison ===
1658+
1659+
def __eq__(self, other):
1660+
return (
1661+
isinstance(other, AliasNode) and
1662+
self.alias == other.alias
1663+
)
1664+
1665+
def __hash__(self):
1666+
return hash(self.alias)
1667+
1668+
15591669
class GroupedLinkedResourcesNode(_GroupedNode):
15601670
entity_tooltip = 'grouped URLs'
15611671

src/crystal/browser/icons.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def TREE_NODE_ICONS() -> dict[str, wx.Bitmap]:
1313
(icon_name, _load_png_resource(f'treenodeicon_{icon_name}.png'))
1414
for icon_name in [
1515
# Entity Tree Icons
16+
'entitytree_alias',
1617
'entitytree_cluster_embedded',
1718
'entitytree_cluster_offsite',
1819
'entitytree_loading',

src/crystal/model.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1164,8 +1164,17 @@ def _set_entity_title_format(self, value: EntityTitleFormat) -> None:
11641164
def get_display_url(self, url):
11651165
"""
11661166
Returns a displayable version of the provided URL.
1167-
If the URL lies under the configured `default_url_prefix`, that prefix will be stripped.
1167+
1168+
- If the URL is an external URL (pointing to a live resource on the internet),
1169+
it will be formatted with an icon prefix.
1170+
- If the URL is not external and lies under the configured `default_url_prefix`,
1171+
that prefix will be stripped.
11681172
"""
1173+
# Check if this is an external URL first
1174+
if (external_url := Alias.parse_external_url(url)) is not None:
1175+
return Alias.format_external_url_for_display(external_url)
1176+
1177+
# Apply default URL prefix shortening for non-external URLs
11691178
default_url_prefix = self.default_url_prefix
11701179
if default_url_prefix is None:
11711180
return url
@@ -1989,6 +1998,27 @@ def _resource_group_did_forget(self, group: ResourceGroup) -> None:
19891998
if hasattr(lis, 'resource_group_did_forget'):
19901999
run_bulkhead_call(lis.resource_group_did_forget, group) # type: ignore[attr-defined]
19912000

2001+
# === Events: Alias Lifecycle ===
2002+
2003+
# Called when a new Alias is created after the project has loaded
2004+
def _alias_did_instantiate(self, alias: Alias) -> None:
2005+
# Notify normal listeners
2006+
for lis in self.listeners:
2007+
if hasattr(lis, 'alias_did_instantiate'):
2008+
run_bulkhead_call(lis.alias_did_instantiate, alias) # type: ignore[attr-defined]
2009+
2010+
def _alias_did_change(self, alias: Alias) -> None:
2011+
# Notify normal listeners
2012+
for lis in self.listeners:
2013+
if hasattr(lis, 'alias_did_change'):
2014+
run_bulkhead_call(lis.alias_did_change, alias) # type: ignore[attr-defined]
2015+
2016+
def _alias_did_forget(self, alias: Alias) -> None:
2017+
# Notify normal listeners
2018+
for lis in self.listeners:
2019+
if hasattr(lis, 'alias_did_forget'):
2020+
run_bulkhead_call(lis.alias_did_forget, alias) # type: ignore[attr-defined]
2021+
19922022
# === Events: Root Task Lifecycle ===
19932023

19942024
def _root_task_did_change(self, old_root_task: 'RootTask', new_root_task: 'RootTask') -> None:
@@ -4806,6 +4836,10 @@ def __init__(self,
48064836
project._db.commit()
48074837
self._id = c.lastrowid
48084838
project._aliases.append(self)
4839+
4840+
# Notify listeners if not loading
4841+
if not project._loading:
4842+
project._alias_did_instantiate(self)
48094843

48104844
# === Delete ===
48114845

@@ -4825,6 +4859,8 @@ def delete(self) -> None:
48254859
self._id = None
48264860

48274861
self.project._aliases.remove(self)
4862+
4863+
self.project._alias_did_forget(self)
48284864

48294865
# === Properties ===
48304866

@@ -4852,6 +4888,8 @@ def _set_target_url_prefix(self, target_url_prefix: str) -> None:
48524888
self.project._db.commit()
48534889

48544890
self._target_url_prefix = target_url_prefix
4891+
4892+
self.project._alias_did_change(self)
48554893
target_url_prefix = cast(str, property(_get_target_url_prefix, _set_target_url_prefix))
48564894

48574895
def _get_target_is_external(self) -> bool:
@@ -4873,6 +4911,8 @@ def _set_target_is_external(self, target_is_external: bool) -> None:
48734911
self.project._db.commit()
48744912

48754913
self._target_is_external = target_is_external
4914+
4915+
self.project._alias_did_change(self)
48764916
target_is_external = cast(bool, property(_get_target_is_external, _set_target_is_external))
48774917

48784918
# === External URLs ===
@@ -4908,6 +4948,10 @@ def parse_external_url(archive_url: str) -> str | None:
49084948
else:
49094949
return None
49104950

4951+
@staticmethod
4952+
def format_external_url_for_display(external_url: str) -> str:
4953+
return f'🌐 {external_url}'
4954+
49114955
# === Utility ===
49124956

49134957
def __repr__(self):
724 Bytes
Loading
21.1 KB
Loading

src/crystal/tests/index.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
test_do_not_download_groups,
99
test_download,
1010
test_download_body,
11+
test_edit_alias,
1112
test_edit_group,
1213
test_edit_root_url,
1314
test_entitytree,
@@ -20,6 +21,7 @@
2021
test_log_drawer,
2122
test_main_window,
2223
test_menus,
24+
test_new_alias,
2325
test_new_group,
2426
test_new_root_url,
2527
test_open_project,
@@ -70,6 +72,7 @@ def _test_functions_in_module(mod) -> list[Callable]:
7072
_test_functions_in_module(test_do_not_download_groups) +
7173
_test_functions_in_module(test_download) +
7274
_test_functions_in_module(test_download_body) +
75+
_test_functions_in_module(test_edit_alias) +
7376
_test_functions_in_module(test_edit_group) +
7477
_test_functions_in_module(test_edit_root_url) +
7578
_test_functions_in_module(test_entitytree) +
@@ -82,6 +85,7 @@ def _test_functions_in_module(mod) -> list[Callable]:
8285
_test_functions_in_module(test_log_drawer) +
8386
_test_functions_in_module(test_main_window) +
8487
_test_functions_in_module(test_menus) +
88+
_test_functions_in_module(test_new_alias) +
8589
_test_functions_in_module(test_new_group) +
8690
_test_functions_in_module(test_new_root_url) +
8791
_test_functions_in_module(test_open_project) +

0 commit comments

Comments
 (0)