Skip to content

Commit

Permalink
Use icons consistently in UI to refer to Root URLs and Groups
Browse files Browse the repository at this point in the history
Resolves #138
  • Loading branch information
davidfstr committed Jan 13, 2024
2 parents 81de08a + 7c4d19d commit d95a888
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 26 deletions.
2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Release Notes ⋮
* Improve suggested source when creating a new group.
* Main Window
* Prevent resizing the window to be too small.
* Use ⚓️ and 📁 icons consistently in the UI to refer to
Root URLs and Groups respectively.

* Serving improvements
* XML files like Atom feeds and RSS feeds are now served correctly,
Expand Down
27 changes: 19 additions & 8 deletions src/crystal/browser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from crystal import __version__ as crystal_version
from crystal.browser.addgroup import AddGroupDialog
from crystal.browser.addrooturl import AddRootUrlDialog
from crystal.browser.entitytree import EntityTree
from crystal.browser.entitytree import EntityTree, ResourceGroupNode, RootResourceNode
from crystal.browser.icons import TREE_NODE_ICONS
from crystal.browser.preferences import PreferencesDialog
from crystal.browser.tasktree import TaskTree
from crystal.model import (
Expand All @@ -19,7 +20,9 @@
from crystal.ui.actions import Action
from crystal.ui.BetterMessageDialog import BetterMessageDialog
from crystal.ui.log_drawer import LogDrawer
from crystal.ui.tree import DEFAULT_FOLDER_ICON_SET
from crystal.util.finderinfo import get_hide_file_extension
from crystal.util.unicode_labels import decorate_label
from crystal.util.wx_bind import bind
from crystal.util.wx_dialog import set_dialog_or_frame_icon_if_appropriate
from crystal.util.xos import (
Expand Down Expand Up @@ -186,39 +189,47 @@ def _create_actions(self) -> None:
# NOTE: Action is bound to self._on_preferences later manually
action_func=None,
enabled=True,
button_label='&Preferences...')
button_label=decorate_label('⚙️', '&Preferences...', ''))

# Entity
self._new_root_url_action = Action(wx.ID_ANY,
'New &Root URL...',
wx.AcceleratorEntry(wx.ACCEL_CTRL, ord('R')),
self._on_add_url,
enabled=(not self._readonly))
enabled=(not self._readonly),
button_bitmap=TREE_NODE_ICONS()['entitytree_root_resource'],
button_label='New &Root URL...')
self._new_group_action = Action(wx.ID_ANY,
'New &Group...',
wx.AcceleratorEntry(wx.ACCEL_CTRL, ord('G')),
self._on_add_group,
enabled=(not self._readonly))
enabled=(not self._readonly),
button_bitmap=dict(DEFAULT_FOLDER_ICON_SET())[wx.TreeItemIcon_Normal],
button_label='New &Group...')
self._forget_action = Action(wx.ID_ANY,
'&Forget',
wx.AcceleratorEntry(wx.ACCEL_CTRL, wx.WXK_BACK),
self._on_forget_entity,
enabled=False)
enabled=False,
button_label=decorate_label('✖️', '&Forget', ''))
self._download_action = Action(wx.ID_ANY,
'&Download',
wx.AcceleratorEntry(wx.ACCEL_CTRL, wx.WXK_RETURN),
self._on_download_entity,
enabled=False)
enabled=False,
button_label=decorate_label('⬇', '&Download', ''))
self._update_membership_action = Action(wx.ID_ANY,
'Update &Membership',
accel=None,
action_func=self._on_update_group_membership,
enabled=False)
enabled=False,
button_label=decorate_label('🔎', 'Update &Membership', ' '))
self._view_action = Action(wx.ID_ANY,
'&View',
wx.AcceleratorEntry(wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('O')),
self._on_view_entity,
enabled=False)
enabled=False,
button_label=decorate_label('👀', '&View', ' '))

# HACK: Gather all actions indirectly by inspecting fields
self._actions = [a for a in self.__dict__.values() if isinstance(a, Action)]
Expand Down
16 changes: 14 additions & 2 deletions src/crystal/browser/addgroup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# TODO: Consider extracting functions shared between dialogs to own module
from crystal.browser.addrooturl import AddRootUrlDialog, fields_hide_hint_when_focused
from crystal.browser.entitytree import ResourceGroupNode, RootResourceNode
from crystal.model import Project, ResourceGroup, ResourceGroupSource
from crystal.progress import CancelLoadUrls
from crystal.util.unicode_labels import decorate_label
from crystal.util.wx_bind import bind
from crystal.util.wx_dialog import position_dialog_initially
from crystal.util.wx_static_box_sizer import wrap_static_box_sizer_child
Expand Down Expand Up @@ -146,9 +148,19 @@ def _create_fields(self,
name='cr-add-group-dialog__source-field')
self.source_choice_box.Append('none', None)
for rr in self._project.root_resources:
self.source_choice_box.Append(rr.display_name, rr)
self.source_choice_box.Append(
decorate_label(
RootResourceNode.ICON,
RootResourceNode.calculate_title_of(rr),
RootResourceNode.ICON_TRUNCATION_FIX),
rr)
for rg in self._project.resource_groups:
self.source_choice_box.Append(rg.display_name, rg)
self.source_choice_box.Append(
decorate_label(
ResourceGroupNode.ICON,
ResourceGroupNode.calculate_title_of(rg),
ResourceGroupNode.ICON_TRUNCATION_FIX),
rg)
self.source_choice_box.SetSelection(0)
if initial_source is not None:
for i in range(self.source_choice_box.GetCount()):
Expand Down
30 changes: 22 additions & 8 deletions src/crystal/browser/entitytree.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,9 @@ def on_selection_deleted(self,


class RootResourceNode(_ResourceNode):
ICON = '⚓️'
ICON_TRUNCATION_FIX = ''

def __init__(self,
root_resource: RootResource,
*, source: DeferrableResourceGroupSource,
Expand All @@ -811,10 +814,14 @@ def _entity_tooltip(self) -> str:
return 'root URL'

def calculate_title(self) -> str:
project = self.root_resource.project
display_url = project.get_display_url(self.root_resource.url)
if self.root_resource.name != '':
return '%s - %s' % (display_url, self.root_resource.name)
return self.calculate_title_of(self.root_resource)

@staticmethod
def calculate_title_of(root_resource: RootResource) -> str:
project = root_resource.project
display_url = project.get_display_url(root_resource.url)
if root_resource.name != '':
return '%s - %s' % (display_url, root_resource.name)
else:
return '%s' % (display_url,)

Expand Down Expand Up @@ -950,6 +957,9 @@ def __hash__(self):


class ResourceGroupNode(Node):
ICON = '📁'
ICON_TRUNCATION_FIX = ' '

_MAX_VISIBLE_CHILDREN = 100
_MORE_CHILDREN_TO_SHOW = 20

Expand All @@ -973,10 +983,14 @@ def icon_tooltip(self) -> Optional[str]:
return 'Group'

def calculate_title(self) -> str:
project = self.resource_group.project
display_url = project.get_display_url(self.resource_group.url_pattern)
if self.resource_group.name != '':
return '%s - %s' % (display_url, self.resource_group.name)
return self.calculate_title_of(self.resource_group)

@staticmethod
def calculate_title_of(resource_group: ResourceGroup) -> str:
project = resource_group.project
display_url = project.get_display_url(resource_group.url_pattern)
if resource_group.name != '':
return '%s - %s' % (display_url, resource_group.name)
else:
return '%s' % (display_url,)

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 @@ -80,4 +80,28 @@ def _add_badge_to_background(background: wx.Bitmap, badge: wx.Bitmap) -> wx.Bitm
return background_plus_badge


def add_transparent_left_border(original: wx.Bitmap, thickness: int) -> wx.Bitmap:
if thickness == 0:
return original

bordered = wx.Bitmap()
success = bordered.Create(
original.Width + thickness,
original.Height,
original.Depth)
if not success:
raise Exception('Failed to create a wx.Bitmap')

dc = wx.MemoryDC(bordered)
dc.Clear() # fill bitmap with white
dc.DrawBitmap(
original,
x=thickness,
y=0,
useMask=True)
dc.SelectObject(wx.NullBitmap) # commit changes to bordered
bordered.SetMaskColour(wx.Colour(255, 255, 255)) # replace white with transparent
return bordered


# ------------------------------------------------------------------------------
39 changes: 34 additions & 5 deletions src/crystal/tests/util/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
)
from crystal.util.xos import is_mac_os
import os.path
import re
import sys
import tempfile
import traceback
Expand Down Expand Up @@ -364,6 +365,7 @@ async def cancel(self) -> None:

class AddGroupDialog:
_NONE_SOURCE_TITLE = 'none'
_SOURCE_TITLE_RE = re.compile(r'^(?:[^a-zA-Z0-9]+ )?(.*?)(?: - (.*?))? *$')

name_field: wx.TextCtrl
pattern_field: wx.TextCtrl
Expand Down Expand Up @@ -412,14 +414,41 @@ def _get_source(self) -> Optional[str]:
selection_ci = self.source_field.GetSelection()
if selection_ci == wx.NOT_FOUND:
return None
source_name = self.source_field.GetString(selection_ci)
if source_name == self._NONE_SOURCE_TITLE:
source_title = self.source_field.GetString(selection_ci)
if source_title == self._NONE_SOURCE_TITLE:
return None
return source_name
m = self._SOURCE_TITLE_RE.fullmatch(source_title)
assert m is not None
source_name = m.group(2)
if source_name is not None:
return source_name
else:
# If the referenced source has no name, allow referring to it by its display URL
cur_source_display_url = m.group(1)
assert cur_source_display_url is not None
return cur_source_display_url

def _set_source(self, source_name: Optional[str]) -> None:
if source_name is None:
source_name = self._NONE_SOURCE_TITLE
selection_ci = self.source_field.GetStrings().index(source_name)
selection_ci = 0
else:
for (selection_ci, source_title) in enumerate(self.source_field.GetStrings()):
if selection_ci == 0:
continue
m = self._SOURCE_TITLE_RE.fullmatch(source_title)
assert m is not None
cur_source_name = m.group(2)
if cur_source_name is not None:
if cur_source_name == source_name:
break
else:
# If the referenced source has no name, allow referring to it by its display URL
cur_source_display_url = m.group(1)
assert cur_source_display_url is not None
if cur_source_display_url == source_name:
break
else:
raise ValueError(f'Source not found: {source_name}')
self.source_field.SetSelection(selection_ci)
source = property(_get_source, _set_source)

Expand Down
15 changes: 14 additions & 1 deletion src/crystal/ui/actions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from crystal.browser.icons import add_transparent_left_border
from crystal.util.wx_bind import bind
from crystal.util.xos import is_mac_os
from crystal.util.xos import is_mac_os, is_windows
from typing import Callable, List, Optional
import wx

Expand All @@ -21,12 +22,14 @@ def __init__(self,
accel: Optional[wx.AcceleratorEntry]=None,
action_func: Optional[Callable[[wx.CommandEvent], None]]=None,
enabled: bool=True,
button_bitmap: Optional[wx.Bitmap]=None,
button_label: str=''):
self._menuitem_id = menuitem_id
self._label = label
self._accel = accel
self._action_func = action_func
self._enabled = enabled
self._button_bitmap = button_bitmap
self._button_label = button_label

self._menuitems = [] # type: List[wx.MenuItem]
Expand Down Expand Up @@ -78,6 +81,16 @@ def create_button(self, *args, **kwargs) -> wx.Button:
button_label = wx.Control.RemoveMnemonics(button_label) # reinterpret

button = wx.Button(*args, label=button_label, **kwargs)
if self._button_bitmap is not None:
button.SetBitmap(add_transparent_left_border(
self._button_bitmap,
# Alter left margin for bitmap to be reasonable on Windows.
# Initially it is 0.
8+6 if is_windows() else 0
))
if is_windows():
# Decrease bitmap-to-text spacing on Windows to be reasonable
button.SetBitmapMargins((-6, 0))
# (NOTE: Do NOT set accelerator for the button because it does not
# work on consistently on macOS. Any action added as a button
# should also be added as a menuitem too, and we CAN set an
Expand Down
4 changes: 2 additions & 2 deletions src/crystal/ui/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
_DEFAULT_TREE_ICON_SIZE = (16,16)

_DEFAULT_FOLDER_ICON_SET_CACHED = None
def _DEFAULT_FOLDER_ICON_SET() -> IconSet:
def DEFAULT_FOLDER_ICON_SET() -> IconSet:
global _DEFAULT_FOLDER_ICON_SET_CACHED # necessary to write to a module global
if not _DEFAULT_FOLDER_ICON_SET_CACHED:
_DEFAULT_FOLDER_ICON_SET_CACHED = (
Expand Down Expand Up @@ -258,7 +258,7 @@ def _set_icon_set(self,
effective_value = (
value
if value is not None else (
_DEFAULT_FOLDER_ICON_SET()
DEFAULT_FOLDER_ICON_SET()
if self.expandable
else _DEFAULT_FILE_ICON_SET()
)
Expand Down
15 changes: 15 additions & 0 deletions src/crystal/util/unicode_labels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from crystal.util.xos import is_windows, windows_major_version


def decorate_label(icon: str, message: str, truncation_fix: str) -> str:
if truncation_fix not in ('', ' ', ' '):
raise ValueError()
if is_windows():
if windows_major_version() == 7:
# Windows 7: Cannot display Unicode icon at all
return f'{message}'
else: # windows_major_version() >= 8
# Windows 8, 10: Can display some Unicode icons.
# Some icons require trailing spaces to avoid truncating the message
return f'{icon} {message}{truncation_fix}'
return f'{icon} {message}'

0 comments on commit d95a888

Please sign in to comment.