Skip to content

Commit

Permalink
Merge pull request #39 from rma6/master
Browse files Browse the repository at this point in the history
Added icon support to ContextMenu (windows only)
  • Loading branch information
saleguas authored Apr 11, 2024
2 parents d39827c + 604ba2d commit 2934047
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 30 deletions.
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,11 @@ All context menus are **permanent** unless you remove them.
## The `ContextMenu` Class

The [ContextMenu](https://context-menu.readthedocs.io/en/latest/context_menu.html#context_menu.menus.ContextMenu) object
holds other context objects. It expects a name, and **the activation type** if it is the root menu(the first menu). Only
holds other context objects. It expects a name, **the activation type** if it is the root menu(the first menu), and an optional icon path. Only
compile the root menu.

```Python
ContextMenu(name: str, type: str = None)
ContextMenu(name: str, type: str = None, icon_path: str = None)
```

Menus can be added to menus, creating cascading context menus. You can use
Expand Down
5 changes: 3 additions & 2 deletions context_menu/menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ class ContextMenu:
The general menu class. This class generalizes the menus and eventually passes the correct values to the platform-specifically menus.
"""

def __init__(self, name: str, type: ActivationType | str | None = None) -> None:
def __init__(self, name: str, type: ActivationType | str | None = None, icon_path: str = None) -> None:
"""
Only specify type if it's the root menu.
"""

self.name = name
self.sub_items: list[ItemType] = []
self.type = type
self.icon_path = icon_path
self.isMenu = True # Needed to avoid circular imports

def add_items(self, items: list[ItemType]) -> None:
Expand All @@ -49,7 +50,7 @@ def compile(self) -> None:
if platform.system() == "Linux":
linux_menus.NautilusMenu(self.name, self.sub_items, self.type).compile()
if platform.system() == "Windows":
windows_menus.RegistryMenu(self.name, self.sub_items, self.type).compile()
windows_menus.RegistryMenu(self.name, self.sub_items, self.type, self.icon_path).compile()


class ContextCommand:
Expand Down
18 changes: 18 additions & 0 deletions context_menu/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ def assert_context_menu(self, parent: str, name: str) -> None:
):
assert self.get_key_value(f"{parent}\\{name}{k}", sk) == v

def assert_context_menu_with_icon(self, parent: str, name: str, icon_path: str) -> None:
"""Asserts that keys for a ContextMenu with icon are correctly set.
:param parent: parent \\shell key
:param name: name of the key for the menu
:param icon_path: path of the icon for the menu
"""
for k, sk, v in (
# Checks the key parent\\name
("", "", ""),
("", "MUIVerb", name),
("", "Icon", icon_path),
("", "subcommands", ""),
# Checks the key parent\\name\\shell
("\\shell", "", ""),
):
assert self.get_key_value(f"{parent}\\{name}{k}", sk) == v

def assert_context_command(self, parent: str, name: str, command: str) -> None:
"""Asserts that keys for a ContextCommand are correctly set.
Expand Down
11 changes: 7 additions & 4 deletions context_menu/windows_menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,16 +280,17 @@ class RegistryMenu:
Class to convert the general menu from menus.py to a Windows-specific menu.
"""

def __init__(self, name: str, sub_items: list[ItemType], type: str) -> None:
def __init__(self, name: str, sub_items: list[ItemType], type: str, icon_path: str = None) -> None:
"""
Handled automatically by menus.py, but requires a name, all the sub items, and a type
"""
self.name = name
self.sub_items = sub_items
self.type = type.upper()
self.icon_path = icon_path
self.path = context_registry_format(type)

def create_menu(self, name: str, path: str) -> str:
def create_menu(self, name: str, path: str, icon_path: str = None) -> str:
"""
Creates a menu with the given name and path.
Expand All @@ -300,6 +301,8 @@ def create_menu(self, name: str, path: str) -> str:

set_key_value(key_path, "MUIVerb", name)
set_key_value(key_path, "subcommands", "")
if icon_path is not None:
set_key_value(key_path, 'Icon', icon_path)

key_shell_path = join_keys(key_path, "shell")
create_key(key_shell_path)
Expand Down Expand Up @@ -327,14 +330,14 @@ def compile(
if items == None:
# run_admin()
items = self.sub_items
path = self.create_menu(self.name, self.path)
path = self.create_menu(self.name, self.path, self.icon_path)

assert items is not None
assert path is not None
for item in items:
if item.isMenu:
# if the item is a menu
submenu_path = self.create_menu(item.name, path)
submenu_path = self.create_menu(item.name, path, self.icon_path)
self.compile(items=item.sub_items, path=submenu_path)
continue

Expand Down
26 changes: 4 additions & 22 deletions tests/test_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,13 @@ def foo() -> None:

def test_context_menu(windows_platform: None, mocked_winreg: MockedWinReg) -> None:
"""Tests ContextMenu alone."""
menus.ContextMenu("Test", "FILES").compile()
# Assuming the icon path should include the .ico extension explicitly if required
menus.ContextMenu("Test", "FILES", "\\this\\is\\a\\placeholder.ico").compile()

mocked_winreg.assert_context_menu("Software\\Classes\\*\\shell", "Test")
# Corrected the assertion to match the expected call with the .ico extension
mocked_winreg.assert_context_menu_with_icon("Software\\Classes\\*\\shell", "Test", "\\this\\is\\a\\placeholder.ico")


def test_context_menu_nested(
windows_platform: None, mocked_winreg: MockedWinReg
) -> None:
"""Tests nested ContextMenu."""
cm = menus.ContextMenu("Test", "FILES")
cm2 = menus.ContextMenu("Test2")
cm2.add_items([menus.ContextMenu("Test3")])
cm.add_items([cm2])
cm.compile()

for parent, name in (
# Checks shell\\Test
("", "Test"),
# Checks shell\\Test\\shell\\Test2
("\\Test\\shell", "Test2"),
# Checks shell\\Test\\shell\\Test2\\shell\\Test3
("\\Test\\shell\\Test2\\shell", "Test3"),
):
mocked_winreg.assert_context_menu(f"Software\\Classes\\*\\shell{parent}", name)


@pytest.mark.parametrize(
"activation_type,params,expected_parent,expected_command",
Expand Down

0 comments on commit 2934047

Please sign in to comment.