Skip to content

Commit

Permalink
Can mark group as Do Not Download
Browse files Browse the repository at this point in the history
Resolves #72
  • Loading branch information
davidfstr committed Jan 21, 2024
1 parent 7c87e12 commit d28dff1
Show file tree
Hide file tree
Showing 16 changed files with 748 additions and 96 deletions.
2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Release Notes ⋮

* Workflow improvements
* Can now edit the name and source of Root URLs and Groups after creation.
* Can mark resource group as "do not download" to prevent their members
from being downloaded when in an embedded context.

* Serving improvements
* XML files like Atom feeds and RSS feeds are now served correctly,
Expand Down
24 changes: 19 additions & 5 deletions src/crystal/browser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@

class MainWindow:
project: Project
frame: wx.Frame
_frame: wx.Frame
entity_tree: EntityTree
task_tree: TaskTree

Expand Down Expand Up @@ -492,17 +492,30 @@ def _on_new_group(self, event: wx.CommandEvent) -> None:
pass

@fg_affinity
def _on_new_group_dialog_ok(self, name: str, url_pattern: str, source: ResourceGroupSource) -> None:
def _on_new_group_dialog_ok(self,
name: str,
url_pattern: str,
source: ResourceGroupSource,
do_not_download: bool,
) -> None:
# TODO: Validate user input:
# * Is url_pattern empty?
# * Is url_pattern already taken?
ResourceGroup(self.project, name, url_pattern, source)
rg = ResourceGroup(
self.project, name, url_pattern, source,
do_not_download=do_not_download)

@fg_affinity
def _on_edit_group_dialog_ok(self, rg: ResourceGroup, name: str, url_pattern: str, source: ResourceGroupSource) -> None:
def _on_edit_group_dialog_ok(self,
rg: ResourceGroup,
name: str,
url_pattern: str,
source: ResourceGroupSource,
do_not_download: bool,
) -> None:
if url_pattern != rg.url_pattern:
raise ValueError()
(rg.name, rg.source) = (name, source)
(rg.name, rg.source, rg.do_not_download) = (name, source, do_not_download)

# TODO: This update should happen in response to an event
# fired by the entity itself.
Expand Down Expand Up @@ -548,6 +561,7 @@ def _on_edit_entity(self, event) -> None:
initial_url_pattern=rg.url_pattern,
initial_source=rg.source,
initial_name=rg.name,
initial_do_not_download=rg.do_not_download,
is_edit=True)
except CancelLoadUrls:
pass
Expand Down
120 changes: 104 additions & 16 deletions src/crystal/browser/entitytree.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

from crystal.browser.icons import BADGED_TREE_NODE_ICON, TREE_NODE_ICONS
from crystal.browser.icons import (
BADGED_ART_PROVIDER_TREE_NODE_ICON, BADGED_TREE_NODE_ICON, TREE_NODE_ICONS,
)
from crystal.doc.generic import Link
from crystal.doc.html.soup import TEXT_LINK_TYPE_TITLE
from crystal.model import (
Expand Down Expand Up @@ -173,12 +175,23 @@ def root_resource_did_instantiate(self, root_resource: RootResource) -> None:

def resource_group_did_instantiate(self, group: ResourceGroup) -> None:
self.update()

# If new group has do_not_download=False, then some badges related
# to that status may need to be updated
fg_call_later(lambda:
self.root.update_icon_set_of_descendants_in_group(group))

# === Event: Resource Group Did Change Do-Not-Download ===

def resource_group_did_change_do_not_download(self, group: ResourceGroup) -> None:
fg_call_later(lambda:
self.root.update_icon_set_of_descendants_in_group(group))

# === Event: Min Fetch Date Did Change ===

def min_fetch_date_did_change(self) -> None:
fg_call_later(lambda:
self.root.update_icon_set_of_descendants_with_resource(None))
self.root.update_icon_set_of_descendants())

# === Event: Right Click ===

Expand Down Expand Up @@ -362,6 +375,12 @@ def update_title_of_descendants(self) -> None:
"""
self._call_on_descendants('update_title')

def update_icon_set_of_descendants(self) -> None:
"""
Updates the icon set of this node's descendants, usually due to a project change.
"""
self.update_icon_set_of_descendants_with_resource(None)

def update_icon_set_of_descendants_with_resource(self, resource: Optional[Resource]) -> None:
"""
Updates the icon set of this node's descendants, usually due to a project change.
Expand All @@ -375,6 +394,21 @@ def update_icon_set_of_descendants_with_resource(self, resource: Optional[Resour
for child in self.children:
child.update_icon_set_of_descendants_with_resource(resource)

def update_icon_set_of_descendants_in_group(self, group: ResourceGroup) -> None:
"""
Updates the icon set of this node's descendants, usually due to a project change.
Only update if the entity's resource is in the specified group.
"""
if isinstance(self.entity, (RootResource, Resource)):
if self.entity.resource in group:
self.update_icon_set()
elif isinstance(self.entity, ResourceGroup):
if self.entity == group:
self.update_icon_set()
for child in self.children:
child.update_icon_set_of_descendants_in_group(group)

def _call_on_descendants(self, method_name) -> None:
getattr(self, method_name)()
for child in self.children:
Expand Down Expand Up @@ -538,6 +572,19 @@ def _status_badge_name(self, *, force_recalculate: bool=False) -> Optional[str]:
return self._status_badge_name_value

def _calculate_status_badge_name(self) -> Optional[str]:
is_dnd_url = False
url = self.resource.url # cache
for rg in self.resource.project.resource_groups:
if rg.contains_url(url):
if rg.do_not_download:
is_dnd_url = True
else:
is_dnd_url = False
break

if is_dnd_url:
return 'prohibition'

freshest_rr = self.resource.default_revision(stale_ok=True)
if freshest_rr is None:
# Not downloaded
Expand Down Expand Up @@ -570,6 +617,8 @@ def _status_badge_tooltip(self) -> str:
return 'Stale'
elif status_badge_name == 'warning':
return 'Error downloading'
elif status_badge_name == 'prohibition':
return 'Ignored'
else:
raise AssertionError('Unknown resource status badge: ' + status_badge_name)

Expand Down Expand Up @@ -958,19 +1007,63 @@ def __hash__(self):
return hash(self._children_tuple)


class ResourceGroupNode(Node):
class _GroupedNode(Node): # abstract
entity_tooltip: str # abstract

ICON = '📁'
ICON_TRUNCATION_FIX = ' '

def __init__(self,
resource_group: ResourceGroup,
*, source: DeferrableResourceGroupSource,
) -> None:
self.resource_group = resource_group
super().__init__(source=source)

# === Properties ===

def calculate_icon_set(self) -> Optional[IconSet]:
return (
(wx.TreeItemIcon_Normal, BADGED_ART_PROVIDER_TREE_NODE_ICON(
wx.ART_FOLDER,
self._status_badge_name()
)),
(wx.TreeItemIcon_Expanded, BADGED_ART_PROVIDER_TREE_NODE_ICON(
wx.ART_FOLDER_OPEN,
self._status_badge_name()
)),
)

def _status_badge_name(self) -> Optional[str]:
if self.resource_group.do_not_download:
return 'prohibition'
else:
return None

@property
def icon_tooltip(self) -> Optional[str]:
status_badge_name = self._status_badge_name()
if status_badge_name is None:
return f'{self.entity_tooltip.capitalize()}'
elif status_badge_name == 'prohibition':
return f'Ignored {self.entity_tooltip}'
else:
raise AssertionError()


class ResourceGroupNode(_GroupedNode):
entity_tooltip = 'group'

_MAX_VISIBLE_CHILDREN = 100
_MORE_CHILDREN_TO_SHOW = 20

def __init__(self, resource_group: ResourceGroup) -> None:
self.resource_group = resource_group
super().__init__(source=lambda: resource_group.source)
super().__init__(resource_group, source=lambda: resource_group.source)
self._max_visible_children = self._MAX_VISIBLE_CHILDREN

self.view = NodeView()
# NOTE: Defer expensive calculation until if/when the icon_set is used
self.view.icon_set = self.calculate_icon_set
self.view.title = self.calculate_title()
self.view.expandable = True

Expand All @@ -980,10 +1073,6 @@ def __init__(self, resource_group: ResourceGroup) -> None:

# === Properties ===

@property
def icon_tooltip(self) -> Optional[str]:
return 'Group'

def calculate_title(self) -> str:
return self.calculate_title_of(self.resource_group)

Expand Down Expand Up @@ -1100,17 +1189,20 @@ def __hash__(self) -> int:
return hash(self.resource_group)


class GroupedLinkedResourcesNode(Node):
class GroupedLinkedResourcesNode(_GroupedNode):
entity_tooltip = 'grouped URLs'

def __init__(self,
resource_group: ResourceGroup,
root_rsrc_nodes: List[RootResourceNode],
linked_rsrc_nodes: List[LinkedResourceNode],
source: ResourceGroupSource,
) -> None:
self.resource_group = resource_group
super().__init__(source=source)
super().__init__(resource_group, source=source)

self.view = NodeView()
# NOTE: Defer expensive calculation until if/when the icon_set is used
self.view.icon_set = self.calculate_icon_set
#self.view.title = ... (set below)
self.view.expandable = True

Expand All @@ -1121,10 +1213,6 @@ def __init__(self,

# === Properties ===

@property
def icon_tooltip(self) -> Optional[str]:
return 'Grouped URLs'

def calculate_title(self) -> str:
project = self.resource_group.project
display_url_pattern = project.get_display_url(self.resource_group.url_pattern)
Expand Down
24 changes: 24 additions & 0 deletions src/crystal/browser/icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,27 @@ def TREE_NODE_ICONS() -> Dict[str, wx.Bitmap]:
])


@cache
def ART_PROVIDER_TREE_NODE_ICONS() -> Dict[int, wx.Bitmap]:
# HACK: Uses private API
from crystal.ui.tree import _DEFAULT_TREE_ICON_SIZE

return OrderedDict([
(art_id, wx.ArtProvider.GetBitmap(art_id, wx.ART_OTHER, _DEFAULT_TREE_ICON_SIZE))
for art_id in [
wx.ART_FOLDER,
wx.ART_FOLDER_OPEN,
]
])


@cache
def BADGES() -> Dict[str, wx.Bitmap]:
return OrderedDict([
(icon_name, _load_png_resource(f'badge_{icon_name}.png'))
for icon_name in [
'new',
'prohibition',
'stale',
'warning',
]
Expand All @@ -66,6 +81,15 @@ def BADGED_TREE_NODE_ICON(tree_node_icon_name: str, badge_name: Optional[str]) -
badge=BADGES()[badge_name])


@cache
def BADGED_ART_PROVIDER_TREE_NODE_ICON(art_id: int, badge_name: Optional[str]) -> wx.Bitmap:
if badge_name is None:
return ART_PROVIDER_TREE_NODE_ICONS()[art_id]
return _add_badge_to_background(
background=ART_PROVIDER_TREE_NODE_ICONS()[art_id],
badge=BADGES()[badge_name])


def _add_badge_to_background(background: wx.Bitmap, badge: wx.Bitmap) -> wx.Bitmap:
background_plus_badge = background.GetSubBitmap(
wx.Rect(0, 0, background.Width, background.Height)) # copy
Expand Down
Loading

0 comments on commit d28dff1

Please sign in to comment.