66from crystal .app_preferences import app_prefs
77from crystal .browser .entitytree import EntityTree
88from crystal .browser .icons import TREE_NODE_ICONS
9+ from crystal .browser .new_alias import NewAliasDialog
910from crystal .browser .new_group import NewGroupDialog
1011from crystal .browser .new_root_url import ChangePrefixCommand , NewRootUrlDialog
1112from crystal .browser .preferences import PreferencesDialog
1213from crystal .browser .tasktree import TaskTree , TaskTreeNode
1314from crystal .model import (
14- Project , ProjectReadOnlyError , Resource , ResourceGroup , ResourceGroupSource , RootResource ,
15+ Alias , Project , ProjectReadOnlyError , Resource , ResourceGroup , ResourceGroupSource , RootResource ,
1516)
1617from crystal .progress import (
1718 CancelLoadUrls , CancelSaveAs , DummyOpenProjectProgressListener ,
@@ -316,6 +317,11 @@ def _create_actions(self) -> None:
316317 enabled = (not self ._readonly ),
317318 button_bitmap = dict (DEFAULT_FOLDER_ICON_SET ())[wx .TreeItemIcon_Normal ],
318319 button_label = 'New &Group...' )
320+ self ._new_alias_action = Action (wx .ID_ANY ,
321+ 'New &Alias...' ,
322+ wx .AcceleratorEntry (wx .ACCEL_CTRL | wx .ACCEL_SHIFT , ord ('A' )),
323+ self ._on_new_alias ,
324+ enabled = (not self ._readonly ))
319325 self ._edit_action = Action (wx .ID_ANY ,
320326 self ._EDIT_ACTION_LABEL if not self ._readonly else self ._GET_INFO_ACTION_LABEL ,
321327 accel = wx .AcceleratorEntry (wx .ACCEL_NORMAL , wx .WXK_RETURN ),
@@ -438,6 +444,7 @@ def _create_menu_bar(self, raw_frame: wx.Frame) -> wx.MenuBar:
438444 entity_menu .AppendSeparator ()
439445 self ._new_root_url_action .append_menuitem_to (entity_menu )
440446 self ._new_group_action .append_menuitem_to (entity_menu )
447+ self ._new_alias_action .append_menuitem_to (entity_menu )
441448 self ._edit_action .append_menuitem_to (entity_menu )
442449 self ._forget_action .append_menuitem_to (entity_menu )
443450 entity_menu .AppendSeparator ()
@@ -815,7 +822,8 @@ def _update_entity_pane_empty_state_visibility(self) -> bool:
815822 """
816823 is_project_empty = (
817824 is_iterable_empty (self .project .root_resources ) and
818- is_iterable_empty (self .project .resource_groups )
825+ is_iterable_empty (self .project .resource_groups ) and
826+ is_iterable_empty (self .project .aliases )
819827 )
820828
821829 sizer_index = self ._entity_tree_sizer_index # cache
@@ -1412,16 +1420,56 @@ def _on_edit_group_dialog_ok(self,
14121420
14131421 assert download_immediately == False
14141422
1415- def _saving_source_would_create_cycle (self , rg : ResourceGroup , source : ResourceGroupSource ) -> bool :
1416- ancestor_source = source # type: ResourceGroupSource
1417- while ancestor_source is not None :
1418- if ancestor_source == rg :
1419- return True
1420- if isinstance (ancestor_source , ResourceGroup ):
1421- ancestor_source = ancestor_source .source # reinterpret
1422- else :
1423- ancestor_source = None
1424- return False
1423+ # === Entity Pane: New/Edit Alias ===
1424+
1425+ def _on_new_alias (self , event : wx .CommandEvent ) -> None :
1426+ restore_menuitems = self ._disable_menus_during_showwindowmodal ()
1427+ NewAliasDialog (
1428+ self ._frame ,
1429+ self ._on_new_alias_dialog_ok ,
1430+ alias_exists_func = self ._alias_exists ,
1431+ on_close = restore_menuitems ,
1432+ )
1433+
1434+ def _alias_exists (self , source_url_prefix : str ) -> bool :
1435+ return self .project .get_alias (source_url_prefix ) is not None
1436+
1437+ @fg_affinity
1438+ def _on_new_alias_dialog_ok (self ,
1439+ source_url_prefix : str ,
1440+ target_url_prefix : str ,
1441+ target_is_external : bool ,
1442+ ) -> None :
1443+ if source_url_prefix == '' :
1444+ raise ValueError ('Invalid blank source URL prefix' )
1445+ if target_url_prefix == '' :
1446+ raise ValueError ('Invalid blank target URL prefix' )
1447+
1448+ try :
1449+ alias = Alias (self .project , source_url_prefix , target_url_prefix ,
1450+ target_is_external = target_is_external )
1451+ except Alias .AlreadyExists :
1452+ raise ValueError ('Invalid duplicate source URL prefix' )
1453+
1454+ @fg_affinity
1455+ def _on_edit_alias_dialog_ok (self ,
1456+ alias : Alias ,
1457+ source_url_prefix : str ,
1458+ target_url_prefix : str ,
1459+ target_is_external : bool ,
1460+ ) -> None :
1461+ if source_url_prefix != alias .source_url_prefix :
1462+ raise ValueError (
1463+ 'Attempted to change source_url_prefix of existing Alias. '
1464+ 'Source URL prefix cannot be changed after alias is created.'
1465+ )
1466+
1467+ if target_url_prefix == '' :
1468+ raise ValueError ('Invalid blank target URL prefix' )
1469+
1470+ # NOTE: Entity tree update will be triggered by property setters
1471+ alias .target_url_prefix = target_url_prefix
1472+ alias .target_is_external = target_is_external
14251473
14261474 # === Entity Pane: Other Commands ===
14271475
@@ -1486,9 +1534,38 @@ def _on_edit_entity(self, event) -> None:
14861534 raise
14871535 except CancelLoadUrls :
14881536 pass
1537+ elif isinstance (selected_entity , Alias ):
1538+ alias = selected_entity
1539+ restore_menuitems = self ._disable_menus_during_showwindowmodal ()
1540+ try :
1541+ NewAliasDialog (
1542+ self ._frame ,
1543+ partial (self ._on_edit_alias_dialog_ok , alias ),
1544+ alias_exists_func = self ._alias_exists ,
1545+ initial_source_url_prefix = alias .source_url_prefix ,
1546+ initial_target_url_prefix = alias .target_url_prefix ,
1547+ initial_target_is_external = alias .target_is_external ,
1548+ is_edit = True ,
1549+ readonly = self ._readonly ,
1550+ on_close = restore_menuitems ,
1551+ )
1552+ except :
1553+ restore_menuitems ()
1554+ raise
14891555 else :
14901556 raise AssertionError ()
14911557
1558+ def _saving_source_would_create_cycle (self , rg : ResourceGroup , source : ResourceGroupSource ) -> bool :
1559+ ancestor_source = source # type: ResourceGroupSource
1560+ while ancestor_source is not None :
1561+ if ancestor_source == rg :
1562+ return True
1563+ if isinstance (ancestor_source , ResourceGroup ):
1564+ ancestor_source = ancestor_source .source # reinterpret
1565+ else :
1566+ ancestor_source = None
1567+ return False
1568+
14921569 def _on_forget_entity (self , event ) -> None :
14931570 selected_entity = self .entity_tree .selected_entity
14941571 assert selected_entity is not None
@@ -1498,6 +1575,7 @@ def _on_forget_entity(self, event) -> None:
14981575 def _on_download_entity (self , event ) -> None :
14991576 selected_entity = self .entity_tree .selected_entity
15001577 assert selected_entity is not None
1578+ assert not isinstance (selected_entity , Alias )
15011579
15021580 # Show progress dialog in advance if will need to load all project URLs
15031581 if isinstance (selected_entity , ResourceGroup ):
@@ -1612,21 +1690,33 @@ def resource_group_did_instantiate(self, group: ResourceGroup) -> None:
16121690 def resource_group_did_forget (self , group : ResourceGroup ) -> None :
16131691 self ._update_entity_pane_empty_state_visibility ()
16141692
1693+ # NOTE: Can't capture to the Entity Tree itself reliably since may not be visible
1694+ @capture_crashes_to_stderr
1695+ @cloak
1696+ def alias_did_instantiate (self , alias : Alias ) -> None :
1697+ self ._update_entity_pane_empty_state_visibility ()
1698+
1699+ # NOTE: Can't capture to the Entity Tree itself reliably since may not be visible
1700+ @capture_crashes_to_stderr
1701+ @cloak
1702+ def alias_did_forget (self , alias : Alias ) -> None :
1703+ self ._update_entity_pane_empty_state_visibility ()
1704+
16151705 def _on_selected_entity_changed (self , event : wx .TreeEvent | None = None ) -> None :
16161706 selected_entity = self .entity_tree .selected_entity # cache
16171707
16181708 readonly = self ._readonly # cache
16191709 self ._edit_action .enabled = (
1620- isinstance (selected_entity , (ResourceGroup , RootResource )))
1710+ isinstance (selected_entity , (Alias , ResourceGroup , RootResource )))
16211711 self ._forget_action .enabled = (
16221712 (not readonly ) and
1623- isinstance (selected_entity , (ResourceGroup , RootResource )))
1713+ isinstance (selected_entity , (Alias , ResourceGroup , RootResource )))
16241714 self ._update_members_action .enabled = (
16251715 (not readonly ) and
16261716 isinstance (selected_entity , ResourceGroup ))
16271717 self ._download_action .enabled = (
16281718 (not readonly ) and
1629- selected_entity is not None )
1719+ isinstance ( selected_entity , ( Resource , ResourceGroup , RootResource )) )
16301720 self ._view_action .enabled = (
16311721 isinstance (selected_entity , (Resource , RootResource )))
16321722
0 commit comments