Source code for PyXA.Additions.UI
""".. versionadded:: 0.1.2
Classes for creating and displaying various UI elements.
"""
from datetime import datetime, timedelta
from enum import Enum
from time import sleep
from typing import Any, Callable, Union, Literal
import AppKit
from PyObjCTools import AppHelper
from PyXA import XABase
[docs]
class XASwitch:
"""A switch UI element. Wrapper around AppKit.NSSwitch functionality.
.. versionadded:: 0.1.2
"""
def __init__(
self,
action: Union[Callable[["XASwitch", int, Any], None], None] = None,
args: Union[list[Any], None] = None,
):
"""Initializes a switch object.
:param action: The method to run when the switch's state changes, defaults to None
:type action: Union[Callable[[XASwitch, int, Any], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
.. versionadded:: 0.1.2
"""
self.action = action #: The method to run when the switch's state changes
self.args = (
args or []
) #: The arguments to pass to the action method upon execution
switch = AppKit.NSSwitch.alloc().init()
switch.setFrame_(AppKit.NSMakeRect(0, 0, 100, 32))
switch.setAction_("run:action:")
switch.setTarget_(self)
self.xa_elem = switch
@property
def state(self) -> bool:
"""The current state of the switch."""
return self.xa_elem.state() == AppKit.NSControlStateValueOn
@state.setter
def state(self, state: bool):
if state:
self.xa_elem.setState_(AppKit.NSControlStateValueOn)
else:
self.xa_elem.setState_(AppKit.NSControlStateValueOff)
[docs]
def run_action_(self, slider, _):
"""Runs the action method attached to the switch.
:param switch: The NSSwitch object associated with this object
:type switch: AppKit.NSSwitch
.. versionadded:: 0.1.2
"""
button = AppKit.NSApplication.sharedApplication().currentEvent().buttonNumber()
if callable(self.action):
self.action(self, button, *self.args)
[docs]
def toggle(self):
"""Toggles the switch on or off.
.. versionadded:: 0.1.2
"""
if self.xa_elem.state() == AppKit.NSControlStateValueOn:
self.xa_elem.setState_(AppKit.NSControlStateValueOff)
else:
self.xa_elem.setState_(AppKit.NSControlStateValueOn)
[docs]
class XASlider:
"""A slider UI element. Wrapper around AppKit.NSSlider functionality.
.. versionadded:: 0.1.2
"""
def __init__(
self,
action: Union[Callable[["XASlider", int, Any], None], None] = None,
args: Union[list[Any], None] = None,
value: float = 50.0,
min_value: float = 0,
max_value: float = 100,
):
"""Initializes a new slider object.
:param action: The method to call when the value of the slider changes, defaults to None
:type action: Union[Callable[[XASlider, int, Any], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
:param value: The starting value of the slider, defaults to 50.0
:type value: float, optional
:param min_value: The minimum value of the slider, defaults to 0
:type min_value: float, optional
:param max_value: The maximum value of the slider, defaults to 100
:type max_value: float, optional
.. versionadded:: 0.1.2
"""
self.action = action #: The method to run when the value of the slider changes
self.args = (
args or []
) #: The arguments to pass to the action method upon execution
slider = AppKit.NSSlider.sliderWithTarget_action_(None, "action:")
slider.setMinValue_(min_value)
slider.setMaxValue_(max_value)
slider.setDoubleValue_(value)
slider.setFrameSize_(AppKit.NSMakeSize(100, 25))
slider.setAction_("run:action:")
slider.setTarget_(self)
self.xa_elem = slider
@property
def value(self) -> float:
"""The current value of the slider."""
return self.xa_elem.doubleValue()
@value.setter
def value(self, value: float):
self.xa_elem.setDoubleValue_(value)
@property
def min_value(self) -> float:
"""The minimum value that the slider can be set to."""
return self.xa_elem.minValue()
@min_value.setter
def min_value(self, min_value: float):
self.xa_elem.setMinValue_(min_value)
@property
def max_value(self) -> float:
"""The maximum value that the slider can be set to."""
return self.xa_elem.maxValue()
@max_value.setter
def max_value(self, max_value: float):
self.xa_elem.setMaxValue_(max_value)
[docs]
def run_action_(self, slider, _):
"""Runs the action method associated with the slider.
.. versionadded:: 0.1.2
"""
button = AppKit.NSApplication.sharedApplication().currentEvent().buttonNumber()
if callable(self.action):
self.action(self, button, *self.args)
[docs]
class XASegmentedControl:
"""A segmented control UI element. Wrapper around AppKit.NSSegmentedControl functionality.
.. versionadded:: 0.1.2
"""
def __init__(
self,
segments: Union[list[str], list[XABase.XAImage]],
action: Union[Callable[["XASlider", int, Any], None], None] = None,
args: Union[list[Any], None] = None,
multiselect: bool = False,
):
"""Initializes a new slider object.
:param segments: The labels or images to display as available options
:type segments: Union[list[str], list[XABase.XAImage]]
:param action: The method to call when the value of the slider changes, defaults to None
:type action: Union[Callable[[XASlider, int, Any], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
:param multiselect: Controls whether one or multiple options can be selected, defaults to False
:type multiselect: bool, optional
.. versionadded:: 0.1.2
"""
if not isinstance(segments, list):
raise TypeError(
"Must provided a list of segment names (strings) or images (of the XABase.XAImage class)."
)
all_strings = all([isinstance(segment, str) for segment in segments])
all_images = all(isinstance(segment, XABase.XAImage) for segment in segments)
if not all_strings and not all_images:
raise TypeError("Segments must be either *all* strings or *all* images.")
self.__segments = segments
self.__state = [False for _ in segments]
if multiselect:
multiselect = AppKit.NSSegmentSwitchTrackingSelectAny
else:
multiselect = AppKit.NSSegmentSwitchTrackingSelectOne
if all_strings:
control = AppKit.NSSegmentedControl.segmentedControlWithLabels_trackingMode_target_action_(
segments, multiselect, self, "run:action:"
)
else:
images = [image.xa_elem for image in segments]
control = AppKit.NSSegmentedControl.segmentedControlWithImages_trackingMode_target_action_(
images, multiselect, self, "run:action:"
)
self.__multiselect = multiselect
self.action = action #: The method to run when the value of the slider changes
self.args = (
args or []
) #: The arguments to pass to the action method upon execution
self.xa_elem = control
@property
def selection(self) -> Union[list[str], list[XABase.XAImage]]:
"""The list of currently selected segments.
.. versionadded:: 0.1.2
"""
selected_segments = []
for index, segment in enumerate(self.__segments):
if self.__state[index] is True:
selected_segments.append(segment)
return selected_segments
@selection.setter
def selection(self, selection: Union[list[str], list[XABase.XAImage]]):
self.xa_elem.setSelectedSegment_(-1)
for new_segment in selection:
for index, segment in enumerate(self.__segments):
if new_segment == segment:
self.xa_elem.setSelected_forSegment_(True, index)
[docs]
def run_action_(self, slider, _):
"""Runs the action method associated with the segmented control.
.. versionadded:: 0.1.2
"""
index = self.xa_elem.selectedSegment()
if not self.__multiselect:
self.__state = [False for _ in self.__state]
self.__state[index] = not self.__state[index]
button = AppKit.NSApplication.sharedApplication().currentEvent().buttonNumber()
if callable(self.action):
self.action(self, button, *self.args)
[docs]
class XAHUD:
"""A momentary HUD window that displays a message to the user.
.. versionadded:: 0.1.1
"""
def __init__(
self,
message: str,
font_size: int = 20,
duration: float = 3.0,
background_color: Union["XABase.XAColor", None] = None,
text_color: Union["XABase.XAColor", None] = None,
):
self.message = message #: The HUD's message text
self.font_size = font_size #: The font size of the HUD's message text
self.duration: float = (
duration #: The amount of time to display the HUD for, in seconds
)
self.background_color: XABase.XAColor = background_color or XABase.XAColor(
0, 0, 0, 0.3
) #: The background color of the HUD window
self.text_color: XABase.XAColor = text_color or XABase.XAColor(
1, 1, 1, 1
) #: The text color of the HUD's message text
[docs]
def display(self):
"""Displays the HUD in the center of the screen.
.. versionadded:: 0.1.2
"""
# Adjust text frame bounds according to current font size
text = AppKit.NSText.alloc().initWithFrame_(AppKit.NSMakeRect(0, 0, 500, 50))
text.setString_(self.message)
text.setDrawsBackground_(False)
text.setSelectable_(False)
text.setAlignment_(AppKit.NSTextAlignmentCenter)
text.setTextColor_(self.text_color.xa_elem)
font = AppKit.NSFont.systemFontOfSize_(font_size)
text.setFont_(font)
text.sizeToFit()
# Create a HUD panel, add the text as a subview
panel = AppKit.NSPanel.alloc().initWithContentRect_styleMask_backing_defer_(
text.frame(),
AppKit.NSWindowStyleMaskFullSizeContentView
| AppKit.NSWindowStyleMaskUtilityWindow
| AppKit.NSTitledWindowMask
| AppKit.NSWindowStyleMaskHUDWindow,
AppKit.NSBackingStoreBuffered,
False,
)
panel.setTitlebarAppearsTransparent_(True)
panel.contentView().addSubview_(text)
panel.contentView().setBackgroundColor_(self.background_color.xa_elem)
# Vertically center the text within the panel frame
font_size = text.font().boundingRectForFont().size.height
offset = text.frame().size.height - font_size / 2
new_text_rect = AppKit.NSInsetRect(text.frame(), 0, offset)
text.setFrame_(new_text_rect)
# Center the panel in the display
panel.center()
# Display the HUD for the specified duration
panel.makeKeyAndOrderFront_(panel)
app = AppKit.NSApplication.sharedApplication()
app.setActivationPolicy_(AppKit.NSApplicationActivationPolicyAccessory)
app.activateIgnoringOtherApps_(True)
AppKit.NSRunLoop.currentRunLoop().runUntilDate_(
datetime.now() + timedelta(seconds=self.duration)
)
[docs]
class XAAlert:
"""A class for creating and interacting with an alert dialog window.
.. versionadded:: 0.0.5
"""
def __init__(
self,
title: str = "Alert!",
message: str = "",
buttons=["Ok", "Cancel"],
icon: Union["XABase.XAImage", None] = None,
):
"""Initializes an alert object.
:param title: The title text of the alert
:type title: str
:param message: The detail message text of the alert, defaults to ""
:type message: str, optional
:param buttons: A list specifying the buttons available for the user to click
:type buttons: list[str]
:param icon: The icon displayed in the alert window
:type icon: XABase.XAImage
"""
self.title: str = title #: The title text of the alert
self.message: str = message #: The detail message text of the alert
self.buttons: list[
str
] = buttons #: A list specifying the buttons available for the user to click
self.icon = icon #: The icon displayed in the alert window
self.__return_value = None
def __display(self):
# Actual logic for displaying the alert, run on main thread via display()
alert = AppKit.NSAlert.alloc().init()
alert.setInformativeText_(self.title)
alert.setMessageText_(self.message)
if self.icon is not None:
alert.setIcon_(self.icon.xa_elem)
for button in self.buttons:
alert.addButtonWithTitle_(button)
self.__return_value = alert.runModal()
[docs]
def display(self) -> int:
"""Displays the alert.
:return: A number representing the button that the user selected, if any
:rtype: int
.. versionadded:: 0.0.5
"""
loop_mode = AppKit.NSRunLoop.mainRunLoop().currentMode()
if loop_mode is None:
# Not in run loop; run normally (must be done on main thread)
self.__display()
else:
# In run loop; force run on main thread
AppHelper.callAfter(self.__display)
# Wait for user to click some button
while self.__return_value is None:
sleep(0.01)
return self.__return_value
[docs]
class XANotification:
"""A class for managing and interacting with notifications.
.. versionadded:: 0.0.9
"""
def __init__(
self,
text: str,
title: Union[str, None] = None,
subtitle: Union[str, None] = None,
image: Union["XABase.XAImage", None] = None,
sound_name: Union[str, None] = None,
primary_button_title: Union[str, None] = None,
show_reply_button: bool = False,
click_action: Union[Callable[[], None], None] = None,
primary_action: Union[Callable[[], None], None] = None,
reply_action: Union[Callable[[], None], None] = None,
timeout: float = -1,
):
"""Initializes a notification object.
:param text: The main text of the notification
:type text: str
:param title: The title of the notification, defaults to None
:type title: Union[str, None], optional
:param subtitle: The subtitle of the notification, defaults to None
:type subtitle: Union[str, None], optional
:param image: The content image of the notification, defaults to None
:type image: Union[XABase.XAImage, None], optional
:param sound_name: The sound to play when the notification is displayed, defaults to None
:type sound_name: Union[str, None], optional
:param primary_button_title: The name of the primary action button, defaults to None
:type primary_button_title: str, optional
:param click_action: The method to run when the user clicks the notification
:type click_action: Union[Callable[[XANotification], None], None], optional
:param primary_action: The method to run when the user clicks the primary action button, defaults to None
:type primary_action: Union[Callable[[XANotification], None], None], optional
:param reply_action: The method to run when the user replies to the notification, defaults to None
:type reply_action: Union[Callable[[XANotification, str], None], None], optional
:param timeout: The number of seconds to wait for the user to act on the notification, or -1 to wait infinitely, defaults to -1
:type timeout: float
.. versionadded:: 0.0.9
"""
self.text: str = text #: The main text of the notification
self.title: str = title #: The title of the notification
self.subtitle: str = subtitle #: The subtitle of the notification
self.image: XABase.XAImage = image #: The content image of the notification
self.sound_name = (
sound_name #: The sound to play when the notification is displayed
)
self.primary_button_title: str = (
primary_button_title #: The name of the primary action button
)
self.click_action: Union[
Callable[[XANotification], None], None
] = click_action or (
lambda x: None
) #: The method to run when the user clicks the notification
self.primary_action: Union[
Callable[[XANotification], None], None
] = primary_action or (
lambda x: None
) #: The method to run when the user clicks the primary action button
self.reply_action: Union[
Callable[[XANotification, str], None], None
] = reply_action #: The method to run when the user replies to the notification. Overrides the primary action and replaces the primary action button with a "Reply" button.
self.timeout: float = timeout #: The number of seconds to wait for the user to act on the notification, or -1 to wait infinitely
[docs]
def display(self):
"""Displays the notification.
.. versionadded:: 0.0.9
"""
nc = AppKit.NSUserNotificationCenter.defaultUserNotificationCenter()
parent = self
class NotificationCenterDelegate(AppKit.NSObject):
def userNotificationCenter_didActivateNotification_(self, nc, notification):
activation_type = notification.activationType()
if activation_type == 1:
parent.click_action(parent)
elif activation_type == 2:
parent.primary_action(parent)
elif activation_type == 3:
parent.reply_action(parent, notification.response().string())
nc.removeDeliveredNotification_(notification)
nc.setDelegate_(NotificationCenterDelegate.alloc().init().retain())
notification = AppKit.NSUserNotification.alloc().init()
if isinstance(self.title, str):
notification.setTitle_(self.title)
if isinstance(self.subtitle, str):
notification.setSubtitle_(self.subtitle)
notification.setInformativeText_(self.text)
if isinstance(self.image, XABase.XAImage):
notification.setContentImage_(self.image.xa_elem)
if isinstance(self.sound_name, str):
notification.setSoundName_(self.sound_name)
if isinstance(self.primary_button_title, str):
notification.setActionButtonTitle_(self.primary_button_title)
self.__dismiss = False
else:
notification.setHasActionButton_(False)
if callable(self.reply_action):
notification.setHasReplyButton_(True)
self.__dismiss = False
notification.setDeliveryDate_(datetime.now())
nc.scheduleNotification_(notification)
# Wait for notification to be delivered
while notification not in nc.deliveredNotifications():
sleep(0.01)
# Wait for user to do some action on the notification
while notification in nc.deliveredNotifications():
AppKit.NSRunLoop.mainRunLoop().runUntilDate_(
datetime.now() + timedelta(seconds=0.01)
)
[docs]
class XAMenuBar:
def __init__(self):
"""Creates a new menu bar object for interacting with the system menu bar.
:Example:
>>> import PyXA
>>> menubar = PyXA.XAMenuBar()
>>> menu_icon = PyXA.XAImage.symbol("sparkles")
>>> menu = menubar.new_menu(icon=menu_icon)
>>>
>>> button_item = menu.new_item("Click me", action=lambda item, button: print(item.text))
>>> url_item = menu.new_url_item("www.github.com")
>>> image_item = menu.new_image_item("/Users/steven/Desktop/Screenshot 2022-12-06 at 21.40.37.png", tooltip="An image")
>>> switch_item = menu.new_switch_item(label="Toggle", action=lambda switch, button: print(switch.state))
>>> segmented_control_item = menu.new_segmented_control_item(["A", "B", "C"], multiselect=True, action=lambda control, button: print(control.selection))
>>> segmented_control_item = menu.new_segmented_control_item(["1", "2", "3"], action=lambda control, button: print(control.selection))
>>> slider_item = menu.new_slider_item(action=lambda slider, button: print(slider.value))
>>> menu.new_separator()
>>>
>>> menubar.display()
.. versionadded:: 0.0.9
"""
self.menus = {} #: The menus to be displayed in the status bar, keyed by ID
app = AppKit.NSApplication.sharedApplication()
detector = self
def get_all_subitems(menu):
items = []
for item_key, item in menu.items.items():
items.append(item)
items.extend(get_all_subitems(item))
return items
class MyApplicationAppDelegate(AppKit.NSObject):
def menu_willHighlightItem_(self, menu, item):
if hasattr(item, "submenu") and item.submenu() is not None:
self.action_(item)
def menuWillOpen_(self, selected_menu):
button = app.currentEvent().buttonNumber()
for menu_key, menu in detector.menus.items():
if menu.xa_elem == selected_menu:
menu._run_action(button)
def action_(self, menu_item):
button = app.currentEvent().buttonNumber()
if app.currentEvent().type() == AppKit.NSEventTypeMouseMoved:
button = -1
for menu_key, menu in detector.menus.items():
subitems = get_all_subitems(menu)
for subitem in subitems:
if subitem.xa_elem == menu_item:
subitem._run_action(button)
break
# popover = AppKit.NSPopover.alloc().init()
# popover.setContentSize_((100, 200))
# # popover.setBehavior_(AppKit.NSPopoverBehaviorTransient)
# view_controller = AppKit.NSViewController.alloc().init()
# view_controller.setView_(AppKit.NSView.alloc().init())
# popover.setContentViewController_(view_controller)
# popover.showRelativeToRect_ofView_preferredEdge_(menu._status_item.button().bounds(), menu._status_item.button(), AppKit.NSRectEdgeMinY)
self.__delegate = MyApplicationAppDelegate.alloc().init().retain()
app.setDelegate_(self.__delegate)
app.setActivationPolicy_(AppKit.NSApplicationActivationPolicyAccessory)
[docs]
def display(self):
"""Displays the custom menus on the menu bar.
:Example:
>>> import PyXA
>>> menu_bar = PyXA.XAMenuBar()
>>> menu_bar.new_menu("🔥", tooltip="Fire")
>>> menu_bar.new_menu("💧", tooltip="Water")
>>> menu_bar.display()
.. versionadded:: 0.0.9
"""
for menu in self.menus:
# Add a 'Quit' item to the bottom of each menu
menu = self.menus[menu]
item = AppKit.NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
"Quit", "terminate:", ""
)
menu.xa_elem.addItem_(item)
menu.xa_elem.setDelegate_(self.__delegate)
for item in menu.xa_elem.itemArray():
view = item.view()
if view is None:
continue
menu_width = max(menu.xa_elem.size().width, 100)
view_height = view.frame().size.height
view.setFrameSize_((menu_width, view_height))
if isinstance(view, AppKit.NSImageView):
view.image().setSize_(
AppKit.NSMakeSize(menu_width * 0.9, view_height * 0.95)
)
for subview in view.subviews():
if isinstance(subview, AppKit.NSSwitch):
subview_height = subview.frame().size.height
subview.sizeToFit()
subview.setFrame_(
AppKit.NSMakeRect(
menu_width * 0.95 - subview.frame().size.width,
0,
subview.frame().size.width,
subview_height,
)
)
elif not isinstance(subview, AppKit.NSText):
subview_y = subview.frame().origin.y
subview_height = subview.frame().size.height
subview.setFrame_(
AppKit.NSMakeRect(
(menu_width - menu_width * 0.9) / 2,
subview_y,
menu_width * 0.9,
subview_height,
)
)
try:
if len(self.menus) > 0:
AppHelper.installMachInterrupt()
AppHelper.runEventLoop()
except Exception as e:
print(e)
[docs]
class XAMenuBarMenu:
def __init__(
self,
content: Union[str, int, float, XABase.XAObject],
icon: Union[XABase.XAImage, None] = None,
tooltip: Union[str, None] = None,
icon_dimensions: tuple[int, int] = (30, 30),
action: Callable[["XAMenuBarMenu", None], None] = None,
id: Union[str, None] = None,
index: int = -1,
):
"""Initializes a new menu to be displayed in the system menu bar.
:param content: The content of the menu (the string or icon shown in the menubar)
:type content: str
:param icon: The icon associated with the menu
:type icon: XABase.XAImage
:param tooltip: The tooltip to display on hovering over the menu, defaults to None
:type tooltip: Union[str, None], optional
:param icon_dimensions: The width and height of the image, in pixels, defaults to (30, 30)
:type icon_dimensions: int, optional
:param action: The method, if any, to associate with the menu (the method called when the menu is opened), defaults to None
:type action: Callable[[XAMenuBarMenu, None], None], optional
:param id: A unique identifier for the menu, or None to use the title, defaults to None
:type id: Union[str, None], optional
:param index: The position to insert the menu in the list of menus, defaults to -1
:type index: int, optional
.. versionadded:: 0.1.1
"""
self.__content = content
self.__icon = icon
self.__icon_dimensions = icon_dimensions
self.__tooltip = tooltip
self.action = action #: The method to call when the menu is opened
self.id = id or str(content) #: The unique identifier for the menu
self.items = {} #: The menu items, keyed by their IDs
self.index = index
# Create a new status bar item
self.__status_bar = AppKit.NSStatusBar.systemStatusBar()
self._status_item = self.__status_bar.statusItemWithLength_(
AppKit.NSVariableStatusItemLength
).retain()
# Add an image to the status bar item, if necessary
if icon is not None:
self.icon = icon
self.content = content
# Add a tooltip to the status bar item, if necessary
if isinstance(tooltip, str):
self.tooltip = tooltip
# Create a new menu and associate it to the status bar item
# Disable auto-enabling items so that users have the option to disable them
self.xa_elem = AppKit.NSMenu.alloc().init()
self.xa_elem.setAutoenablesItems_(False)
self._status_item.setMenu_(self.xa_elem)
@property
def content(self) -> str:
"""The content of the menu."""
return self.__content
@content.setter
def content(self, content: str):
self.__content = content
if isinstance(content, str):
self._status_item.setTitle_(content)
elif isinstance(content, XABase.XAImage):
self.__icon = content
img = content.xa_elem.copy()
img.setScalesWhenResized_(True)
img.setSize_(self.__icon_dimensions)
self._status_item.button().setImage_(img)
@property
def icon(self) -> "XABase.XAImage":
"""The image associated with the menu."""
return self.__icon
@icon.setter
def icon(self, icon: "XABase.XAImage"):
self.__icon = icon
img = icon.xa_elem.copy()
img.setScalesWhenResized_(True)
img.setSize_(self.__icon_dimensions)
self._status_item.button().setImage_(img)
@property
def icon_dimensions(self) -> tuple[int, int]:
"""The width and height of the menu's image, in pixels."""
return self.__icon_dimensions
@icon_dimensions.setter
def icon_dimensions(self, icon_dimensions: tuple[int, int]):
self.__icon_dimensions = icon_dimensions
size = AppKit.NSSizeFromCGSize(icon_dimensions)
self._status_item.button().image().setSize_(size)
@property
def tooltip(self) -> str:
"""The tooltip that appears when hovering over the menu."""
return self.__tooltip
@tooltip.setter
def tooltip(self, tooltip: str):
self.__tooltip = tooltip
self._status_item.setToolTip_(tooltip)
[docs]
def new_item(
self,
content: Union[str, None] = None,
icon: Union["XABase.XAImage", None] = None,
action: Union[Callable[[], None], None] = None,
args: Union[list[Any], None] = None,
icon_dimensions: tuple[int, int] = (20, 20),
id: Union[str, None] = None,
index: int = -1,
label: Union[str, None] = None,
tooltip: Union[str, None] = None,
multiselect: bool = False,
) -> "XAMenuBarMenuItem":
"""Creates a new menu item and adds it to this menu at the current insertion point.
:param content: The title text of the item, defaults to None
:type content: Union[str, None], optional
:param icon: The image for the item, defaults to None
:type icon: Union[XABase.XAImage, None], optional
:param action: The method to call when the item is clicked, defaults to None
:type action: Union[Callable[[], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
:param icon_dimensions: The width and height of the image, in pixels, defaults to (20, 20)
:type icon_dimensions: tuple[int, int], optional
:param id: A unique identifier for the item, defaults to None
:type id: Union[str, None], optional
:param index: The position to insert the item in the list of menu items, defaults to -1
:type index: int, optional
:param label: The label to the left of the item's content
:type label: Union[str, None], optional
:param tooltip: The tooltip text for the menu item, defaults to None
:type tooltip: Union[str, None], optional
:param multiselect: Whether multiple options in a segmented control item can be selected at once, defaults to False
:type multiselect: bool, optional
:return: The newly created menu item object
:rtype: XAMenuBarMenuItem
:Example:
>>> import PyXA
>>> menu_bar = PyXA.XAMenuBar()
>>>
>>> img1 = PyXA.XAColor.red().make_swatch(10, 10)
>>> img2 = PyXA.XAImage("https://avatars.githubusercontent.com/u/7865925?v=4")
>>> img3 = PyXA.XAImage.symbol("flame.circle")
>>>
>>> m1 = menu_bar.new_menu("Menu 1")
>>> m1.new_item("Item 1", lambda _: print("Action 1"), [], img1, (100, 100))
>>> m1.new_item("Item 2", lambda _: print("Action 2"), [], img2, (100, 100))
>>>
>>> m2 = menu_bar.new_menu("Menu 2")
>>> m2.new_item("Item 1", lambda _: print("Action 3"), image=img3, icon_dimensions=(50, 50))
>>>
>>> menu_bar.display()
.. versionadded:: 0.1.1
"""
# If no ID provided, use the title, then make it unique
content = content or ""
id = id or str(content)
while id in self.items:
id += "_"
if index == -1:
index = self.xa_elem.numberOfItems()
if content == "separator":
self.items[id] = XASeparatorMenuItem(self)
elif content == "slider":
self.items[id] = XASliderMenuItem(self, tooltip, action, args)
elif content == "switch":
self.items[id] = XASwitchMenuItem(self, label, tooltip, action, args)
elif content == "header":
self.items[id] = XAHeaderMenuItem(self, label)
elif isinstance(content, list):
self.items[id] = XASegmentedControlMenuItem(
self, content, tooltip, action, args, multiselect
)
elif isinstance(content, XABase.XAImage):
self.items[id] = XAImageMenuItem(self, content, tooltip)
elif isinstance(content, XABase.XAURL) or isinstance(content, XABase.XAPath):
self.items[id] = XAURLMenuItem(
self, content, label, icon, icon_dimensions, tooltip, action, args
)
elif (
isinstance(content, str)
or isinstance(content, int)
or isinstance(content, float)
or isinstance(content, bool)
):
self.items[id] = XATextMenuItem(
self, str(content), icon, icon_dimensions, tooltip, action, args
)
else:
self.items[id] = XAMenuBarMenuItem(
self,
content,
icon,
action,
args,
icon_dimensions,
id,
index,
label,
tooltip,
)
self.xa_elem.insertItem_atIndex_(self.items[id].xa_elem, index)
return self.items[id]
[docs]
def new_url_item(
self,
url: Union[str, XABase.XAURL, XABase.XAPath],
label: Union[str, None] = None,
icon: Union[XABase.XAImage, None] = None,
icon_dimensions: tuple[int, int] = (20, 20),
tooltip: Union[str, None] = None,
action: Union[Callable[["XAURLMenuItem"], None], None] = None,
args: Union[list[Any], None] = None,
index: int = -1,
) -> "XAURLMenuItem":
"""Creates a new URL menu item at the specified index or the current insertion point.
:param url: The URL or file path that the item will link to
:type url: Union[str, XABase.XAURL, XABase.XAPath]
:param label: The label for the item, or None to use the full URL as the label
:type label: Union[str, None]
:param icon: The icon to display to the left of the label, defaults to None
:type icon: Union[XABase.XAImage, None], optional
:param icon_dimensions: The dimensions of the icon, defaults to (20, 20)
:type icon_dimensions: tuple[int, int], optional
:param tooltip: The tooltip text to display when the cursor hovers over this item, defaults to None
:type tooltip: Union[str, None], optional
:param action: The method to call when the user clicks this item, or None to open the URL using standard methods, defaults to None
:type action: Union[Callable[[XAURLMenuItem], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
:param index: The position to insert the separator in the list of menu items, defaults to -1
:type index: int, optional
:return: The newly created URL menu item
:rtype: XAURLMenuItem
.. versionadded:: 0.1.2
"""
# Convert string URL to XAURL
if isinstance(url, str):
url = XABase.XAURL(url)
return self.new_item(
url, icon, action, args, icon_dimensions, None, index, label, tooltip
)
[docs]
def new_image_item(
self,
image: Union[str, XABase.XAImage],
tooltip: Union[str, None] = None,
index: int = -1,
) -> "XAImageMenuItem":
"""Creates a new image menu item at the specified index or the current insertion point.
:param image: An image object or the path to an image file to display in the menu
:type image: Union[str, XABase.XAImage]
:param tooltip: The tooltip text to display when the cursor hovers over the image, defaults to None
:type tooltip: Union[str, None], optional
:param index: The position to insert the separator in the list of menu items, defaults to -1
:type index: int, optional
:return: The newly created image menu item
:rtype: XAImageMenuItem
.. versionadded:: 0.1.2
"""
# Convert image path to XAImage
if isinstance(image, str):
image = XABase.XAImage(image)
return self.new_item(image, tooltip=tooltip, index=index)
[docs]
def new_segmented_control_item(
self,
segments: Union[list[str], list[XABase.XAImage]],
tooltip: Union[str, None] = None,
action: Union[Callable[[XASlider], None], None] = None,
args: Union[list[Any], None] = None,
multiselect: bool = False,
index: int = -1,
) -> "XASegmentedControlMenuItem":
"""Creates a new segmented control menu item at the specified index or the current insertion point.
:param segments: The strings or images to display as button options
:type segments: Union[list[str], list[XABase.XAImage]]
:param tooltip: The tooltip to display when the cursor hovers over the segmented control, defaults to None
:type tooltip: Union[str, None], optional
:param action: The method to call when the user changes the state of the segmented control (when the user activates/deactivates a button), defaults to None
:type action: Union[Callable[[XASlider], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
:param multiselect: Whether multiple options can be selected at a time, defaults to False
:type multiselect: bool optional
:param index: The position to insert the separator in the list of menu items, defaults to -1
:type index: int, optional
:return: The newly created segmented control menu item
:rtype: XASegmentedControlMenuItem
.. versionadded:: 0.1.2
"""
return self.new_item(
segments,
tooltip=tooltip,
action=action,
args=args,
multiselect=multiselect,
index=index,
)
[docs]
def new_switch_item(
self,
label: Union[str, None] = None,
tooltip: Union[str, None] = None,
action: Union[Callable[[XASlider], None], None] = None,
args: Union[list[Any], None] = None,
index: int = -1,
) -> "XASwitchMenuItem":
"""Creates a new switch menu item at the specified index or the current insertion point.
:param label: The label text to display left of the switch, defaults to None
:type label: Union[str, None], optional
:param tooltip: The tooltip text to display when the cursor hovers over the switch, defaults to None
:type tooltip: Union[str, None], optional
:param action: The method to call when the user changes the switch's state, defaults to None
:type action: Union[Callable[[XASlider], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
:param index: The position to insert the separator in the list of menu items, defaults to -1
:type index: int, optional
:return: The newly created switch menu item
:rtype: XASwitchMenuItem
.. versionadded:: 0.1.2
"""
return self.new_item(
"switch",
label=label,
tooltip=tooltip,
action=action,
args=args,
index=index,
)
[docs]
def new_slider_item(
self,
tooltip: Union[str, None] = None,
action: Union[Callable[[XASlider], None], None] = None,
args: Union[list[Any], None] = None,
index: int = -1,
) -> "XASliderMenuItem":
"""Creates a new slider menu item at the specified index or the current insertion point.
:param tooltip: The tooltip text to display when the cursor hovers over the item, defaults to None
:type tooltip: Union[str, None], optional
:param action: The method to call when the user changes the slider's value, defaults to None
:type action: Union[Callable[[XASlider], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
:param index: The position to insert the separator in the list of menu items, defaults to -1
:type index: int, optional
:return: The newly created slider menu item
:rtype: XASliderMenuItem
.. versionadded:: 0.1.2
"""
return self.new_item(
"slider", tooltip=tooltip, action=action, args=args, index=index
)
[docs]
def new_header(self, text: str, index: int = -1):
return self.new_item("header", label=text, index=index)
[docs]
def new_separator(
self, id: Union[str, None] = None, index: int = -1
) -> "XAMenuBarMenuItem":
"""Adds a separator to the menu at the current insertion point.
:param id: A unique identifier for the separator, defaults to None
:type id: Union[str, None], optional
:param index: The position to insert the separator in the list of menu items, defaults to -1
:type index: int, optional
:return: The newly created separator menu item object
:rtype: XAMenuBarMenuItem
.. versionadded:: 0.1.1
"""
return self.new_item("separator", index=index)
def _run_action(self, button: int):
"""Runs the action associated with this menu.
.. versionadded:: 0.1.1
"""
if callable(self.action):
self.action(self, button)
[docs]
def remove_item(self, id):
"""Removes an item from this menu.
:param id: The ID of the item to remove
:type id: str
.. versionadded:: 0.1.1
"""
item = self.items.pop(id)
self.xa_elem.removeItem_(item.xa_elem)
[docs]
def delete(self):
"""Deletes the menu.
.. versionadded:: 0.1.2
"""
self.__status_bar.removeStatusItem_(self._status_item)
[docs]
class XAMenuBarMenuItem(XAMenuBarMenu):
def __init__(
self,
parent: XAMenuBarMenu,
content: Union[str, int, float, XABase.XAImage, XABase.XAURL, XABase.XAPath],
icon: Union["XABase.XAImage", None] = None,
action: Union[Callable[["XAMenuBarMenuItem", Any], None], None] = None,
args: Union[list[Any], None] = None,
tooltip: Union[str, None] = None,
):
"""Initializes an item of a menu.
:param parent: The menu which owns this menu item
:type parent: XAMenuBarMenu
:param content: The content of the item
:type content: Union[str, int, float, XABase.XAImage, XABase.XAURL, XABase.XAPath]
:param icon: The image for the item, defaults to None
:type icon: Union[XABase.XAImage, None], optional
:param action: The method to associate with the item (the method called when the item is clicked), defaults to None
:type action: Callable[[XAMenuBarMenuItem], None]
:param args: The arguments to call the method with, defaults to None
:type args: Union[list[Any], None], optional
:param tooltip: The tooltip text for the menu item, defaults to None
:type tooltip: Union[str, None], optional
.. versionadded:: 0.1.1
"""
self.parent = parent #: The parent menu or menu item of this item
self.args = (
args or []
) #: The arguments to pass to the action method upon execution
self.action = None #: The method to call when this menu item is clicked
self.items = {}
if isinstance(self.xa_elem, AppKit.NSView):
# Menu item with subviews has been initialized by subclass
new_elem = AppKit.NSMenuItem.alloc().init()
new_elem.setView_(self.xa_elem)
self.xa_elem = new_elem
if tooltip is not None:
self.xa_elem.setToolTip_(tooltip)
if self.action is None:
self.action = action
@property
def tooltip(self) -> str:
"""The tooltip text displayed when the cursor hovers over the menu item."""
return self.xa_elem.toolTip()
@tooltip.setter
def tooltip(self, tooltip: str):
self.xa_elem.setToolTip_(tooltip)
def _run_action(self, button: int):
"""Runs the action associated with this menu item.
.. versionadded:: 0.1.1
"""
if callable(self.action):
self.action(self, button, *self.args)
[docs]
def new_item(
self,
content: Union[str, None] = None,
icon: Union["XABase.XAImage", None] = None,
action: Union[Callable[[], None], None] = None,
args: Union[list[Any], None] = None,
icon_dimensions: tuple[int, int] = (20, 20),
id: Union[str, None] = None,
index: int = -1,
label: Union[str, None] = None,
tooltip: Union[str, None] = None,
multiselect: bool = False,
) -> "XAMenuBarMenuItem":
"""Creates a new menu item and places it in a submenu of this item.
This will create a new submenu as needed, or will append to the existing submenu if one is already available on this item.
:param content: The title text of the item, defaults to None
:type content: Union[str, None], optional
:param icon: The image for the item, defaults to None
:type icon: Union[XABase.XAImage, None], optional
:param action: The method to call when the item is clicked, defaults to None
:type action: Union[Callable[[], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
:param icon_dimensions: The width and height of the image, in pixels, defaults to (20, 20)
:type icon_dimensions: tuple[int, int], optional
:param id: A unique identifier for the item, defaults to None
:type id: Union[str, None], optional
:param index: The position to insert the item in the list of menu items, defaults to -1
:type index: int, optional
:param label: The label to the left of the item's content
:type label: Union[str, None], optional
:param tooltip: The tooltip text for the menu item, defaults to None
:type tooltip: Union[str, None], optional
:param multiselect: Controls whether one or multiple options can be selected in a segmented control menu item, defaults to False
:type multiselect: bool, optional
:return: The newly created menu item object
:rtype: XAMenuBarMenuItem
:Example:
>>> import PyXA
>>> menu_bar = PyXA.XAMenuBar()
>>> m1 = menu_bar.new_menu("Menu 1")
>>> i1 = m1.new_item("Item 1")
>>> i2 = i1.new_subitem("Item 1.1")
>>> i3 = i2.new_subitem("Item 1.1.1")
>>> i4 = i3.new_subitem("Item 1.1.1.1")
>>> menu_bar.display()
.. versionadded:: 0.1.2
"""
# If no ID provided, use the title, then make it unique
content = content or ""
id = id or str(content)
while id in self.items:
id += "_"
# Create a new submenu, if necessary
submenu = self.xa_elem.submenu()
if submenu is None:
submenu = AppKit.NSMenu.alloc().init()
if index == -1:
index = submenu.numberOfItems()
subitem = None
if content == "separator":
subitem = XASeparatorMenuItem(self)
elif content == "slider":
subitem = XASliderMenuItem(self, tooltip, action, args)
elif content == "switch":
subitem = XASwitchMenuItem(self, label, tooltip, action, args)
elif isinstance(content, list):
subitem = XASegmentedControlMenuItem(
self, content, tooltip, action, args, multiselect
)
elif isinstance(content, XABase.XAImage):
subitem = XAImageMenuItem(self, content, tooltip)
elif isinstance(content, XABase.XAURL) or isinstance(content, XABase.XAPath):
subitem = XAURLMenuItem(
self, content, label, icon, icon_dimensions, tooltip, action, args
)
elif (
isinstance(content, str)
or isinstance(content, int)
or isinstance(content, float)
or isinstance(content, bool)
):
subitem = XATextMenuItem(
self, str(content), icon, icon_dimensions, tooltip, action, args
)
else:
subitem = XAMenuBarMenuItem(
self,
content,
icon,
action,
args,
icon_dimensions,
id,
index,
label,
tooltip,
)
submenu.insertItem_atIndex_(subitem.xa_elem, index)
self.xa_elem.menu().setSubmenu_forItem_(submenu, self.xa_elem)
self.items[id] = subitem
return self.items[id]
[docs]
def remove_item(self, id: str):
"""Removes a subitem from this item's submenu.
:param id: The ID of the subitem to remove
:type id: str
.. versionadded:: 0.1.1
"""
item = self.items.pop(id)
self.xa_elem.submenu().removeItem_(item.xa_elem)
[docs]
def delete(self):
"""Deletes the item.
.. versionadded:: 0.1.2
"""
if isinstance(self.parent, XAMenuBarMenu):
self.parent.xa_elem.removeItem_(self.xa_elem)
else:
self.parent.xa_elem.submenu().removeItem_(self.xa_elem)
[docs]
class XASliderMenuItem(XAMenuBarMenuItem):
"""A menu item containing a slider.
.. versionadded:: 0.1.2
"""
def __init__(
self,
parent: XAMenuBarMenu,
tooltip: Union[str, None] = None,
action: Union[Callable[["XASlider", Any], None], None] = None,
args: Union[list[Any], None] = None,
value: float = 50,
min_value: float = 0,
max_value: float = 100,
):
"""Initializes a new slider menu item.
:param parent: The menu containing this item
:type parent: XAMenuBarMenu
:param tooltip: The tooltip text for this item, defaults to None
:type tooltip: Union[str, None], optional
:param action: The method to call when the slider's value changes, defaults to None
:type action: Union[Callable[[XASlider, Any], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
:param value: The starting value of the slider, defaults to 50
:type value: float, optional
:param min_value: The minimum value of the slider, defaults to 0
:type min_value: float, optional
:param max_value: The maximum value of the slider, defaults to 100
:type max_value: float, optional
.. versionadded:: 0.1.2
"""
slider_view = AppKit.NSView.alloc().initWithFrame_(
AppKit.NSMakeRect(0, 0, 100, 30)
)
self.slider = XASlider(action, args, value, min_value, max_value)
slider_view.addSubview_(self.slider.xa_elem)
self.xa_elem = slider_view
super().__init__(parent, None)
@property
def value(self) -> float:
return self.slider.value
@value.setter
def value(self, value: float):
self.slider.value = value
[docs]
class XASegmentedControlMenuItem(XAMenuBarMenuItem):
"""A menu item containing a segmented control.
.. versionadded:: 0.1.2
"""
def __init__(
self,
parent: XAMenuBarMenu,
segments: Union[list[str], list[XABase.XAImage]],
tooltip: Union[str, None] = None,
action: Union[Callable[["XASlider", Any], None], None] = None,
args: Union[list[Any], None] = None,
multiselect: bool = False,
):
"""Initializes a new segmented control menu item.
:param parent: The menu containing this item
:type parent: XAMenuBarMenu
:param segments: The strings or images to display as button options
:type segments: Union[list[str], list[XABase.XAImage]]
:param tooltip: The tooltip text for this item, defaults to None
:type tooltip: Union[str, None], optional
:param action: The method to call when the slider's value changes, defaults to None
:type action: Union[Callable[[XASlider, Any], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
:param multiselect: Whether multiple options can be selected at once, defaults to False
:type multiselect: bool, optional
.. versionadded:: 0.1.2
"""
self.xa_elem = AppKit.NSView.alloc().initWithFrame_(
AppKit.NSMakeRect(0, 0, 100, 25)
)
self.segmented_control = XASegmentedControl(segments, action, args, multiselect)
self.xa_elem.addSubview_(self.segmented_control.xa_elem)
super().__init__(parent, None)
[docs]
class XAImageMenuItem(XAMenuBarMenuItem):
"""A menu item containing an image.
.. versionadded:: 0.1.2
"""
def __init__(
self,
parent: XAMenuBarMenu,
image: Union[str, int, float, XABase.XAImage, XABase.XAURL, XABase.XAPath],
tooltip: Union[str, None] = None,
):
"""Initializes an image menu item.
:param parent: The menu containing this item
:type parent: XAMenuBarMenu
:param image: The image to display in the menu
:type image: Union[str, int, float, XABase.XAImage, XABase.XAURL, XABase.XAPath]
.. versionadded:: 0.1.2
"""
self.xa_elem = AppKit.NSImageView.alloc().initWithFrame_(
AppKit.NSMakeRect(0, 0, 200, image.size[1] * 200 / image.size[0])
)
rounded_img = AppKit.NSImage.alloc().initWithSize_(image.xa_elem.size())
rounded_img.setScalesWhenResized_(True)
rounded_img.lockFocus()
ctx = AppKit.NSGraphicsContext.currentContext()
ctx.setImageInterpolation_(AppKit.NSImageInterpolationHigh)
image_frame = AppKit.NSMakeRect(0, 0, *image.xa_elem.size())
clip_path = AppKit.NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(
image_frame, 50, 50
)
clip_path.setWindingRule_(AppKit.NSWindingRuleEvenOdd)
clip_path.addClip()
image.xa_elem.drawAtPoint_fromRect_operation_fraction_(
AppKit.NSZeroPoint, image_frame, AppKit.NSCompositingOperationSourceOver, 1
)
rounded_img.unlockFocus()
self.xa_elem.setImage_(rounded_img)
super().__init__(parent, image)
[docs]
class XASwitchMenuItem(XAMenuBarMenuItem):
"""A menu item containing a labelled switch.
.. versionadded:: 0.1.2
"""
def __init__(
self,
parent: XAMenuBarMenu,
label: Union[str, None] = None,
tooltip: Union[str, None] = None,
action: Union[Callable[["XASwitch", int, Any], None], None] = None,
args: Union[list[Any], None] = None,
):
"""Initializes a new switch menu item.
:param parent: The menu containing this item
:type parent: XAMenuBarMenu
:param label: The label displayed to the left of the switch, defaults to None
:type label: Union[str, None], optional
:param action: The method to call when the switch changes state, defaults to None
:type action: Union[Callable[[XASwitch, int, Any], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
.. versionadded:: 0.1.2
"""
self.xa_elem = AppKit.NSView.alloc().initWithFrame_(
AppKit.NSMakeRect(0, 0, 100, 30)
)
if label is not None:
self.__text = AppKit.NSText.alloc().initWithFrame_(
AppKit.NSMakeRect(9, -7, 151, 32)
)
self.__text.setString_(label)
self.__text.setDrawsBackground_(False)
font = AppKit.NSFont.menuBarFontOfSize_(0)
self.__text.setFont_(font)
self.xa_elem.addSubview_(self.__text)
self.switch = XASwitch(action, args)
self.xa_elem.addSubview_(self.switch.xa_elem)
self.xa_elem.setWantsLayer_(True)
self.xa_elem.setCanDrawSubviewsIntoLayer_(True)
self.xa_elem.layer().setBackgroundColor_(XABase.XAColor(1, 0, 0).xa_elem)
super().__init__(parent, None)
@property
def label(self) -> str:
"""The label for this switch menu item."""
return self.__text.string()
@label.setter
def label(self, label: str):
self.__text.setString_(label)
[docs]
class XASeparatorMenuItem(XAMenuBarMenuItem):
"""A menu item containing a separator.
.. versionadded:: 0.1.2
"""
def __init__(self, parent: XAMenuBarMenu):
"""Initializes a separator menu item.
:param parent: The menu containing this item
:type parent: XAMenuBarMenu
.. versionadded:: 0.1.2
"""
self.xa_elem = AppKit.NSMenuItem.separatorItem()
super().__init__(parent, None)
[docs]
class XATextMenuItem(XAMenuBarMenuItem):
"""A menu item containing text.
.. versionadded:: 0.1.2
"""
def __init__(
self,
parent: XAMenuBarMenu,
text: str,
icon: Union["XABase.XAImage", None] = None,
icon_dimensions: tuple[int, int] = (20, 20),
tooltip: Union[str, None] = None,
action: Union[Callable[["XAURLMenuItem", int, Any], None], None] = None,
args: Union[list[Any], None] = None,
):
"""Initializes a text menu item.
:param parent: The menu containing this item
:type parent: XAMenuBarMenu
:param text: The text string to display
:type text: str
:param icon: The image for the item, defaults to None
:type icon: Union[XABase.XAImage, None], optional
:param icon_dimensions: The width and height of the icon, in pixels, defaults to (20, 20)
:type icon_dimensions: tuple[int, int], optional
:param action: The method to call when the user clicks the menu item, defaults to None
:type action: Union[Callable[[XAURLMenuItem, int, Any], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
.. versionadded:: 0.1.2
"""
self.__text = text
self.xa_elem = AppKit.NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
text, "action:", ""
)
if isinstance(icon, XABase.XAImage):
img = icon.xa_elem.copy()
img.setScalesWhenResized_(True)
img.setSize_(icon_dimensions)
self.xa_elem.setImage_(img)
super().__init__(parent, None, action=action, args=args)
@property
def text(self) -> str:
return self.__text
@text.setter
def text(self, text: str):
self.__text = text
self.xa_elem.setTitle_(text)
@property
def icon(self) -> "XABase.XAImage":
"""The image associated with the menu item."""
img_obj = self.xa_elem.image()
if img_obj is None:
return None
return self.__icon
@icon.setter
def icon(self, icon: "XABase.XAImage"):
self.__icon = icon
img = icon.xa_elem.copy()
img.setScalesWhenResized_(True)
img.setSize_(self.__icon_dimensions)
self.xa_elem.setImage_(img)
@property
def icon_dimensions(self) -> tuple[int, int]:
"""The width and height of the menu item's image, in pixels."""
return self.__icon_dimensions
@icon_dimensions.setter
def icon_dimensions(self, icon_dimensions: tuple[int, int]):
self.__icon_dimensions = icon_dimensions
size = AppKit.NSSizeFromCGSize(icon_dimensions)
self.xa_elem.image().setSize_(size)
@property
def indent(self) -> int:
"""The level of indentation of the menu item, from 0 to 15."""
return self.__indent
@indent.setter
def indent(self, indent: int):
self.__indent = indent
self.xa_elem.setIndentationLevel_(indent)
@property
def enabled(self) -> int:
"""Whether the menu item is enabled (vs. appearing grayed out)."""
return self.__enabled
@enabled.setter
def enabled(self, enabled: bool):
self.__enabled = enabled
self.xa_elem.setEnabled_(enabled)
def _run_action(self, button: int):
"""Runs the action associated with this menu item.
.. versionadded:: 0.1.1
"""
if callable(self.action):
self.action(self, button, *self.args)
[docs]
def new_subitem(
self,
content: Union[str, None] = None,
icon: Union["XABase.XAImage", None] = None,
action: Union[Callable[[], None], None] = None,
args: Union[list[Any], None] = None,
icon_dimensions: tuple[int, int] = (20, 20),
id: Union[str, None] = None,
index: int = -1,
label: Union[str, None] = None,
tooltip: Union[str, None] = None,
) -> "XAMenuBarMenuItem":
"""Creates a new menu item and places it in a submenu of this item.
This will create a new submenu as needed, or will append to the existing submenu if one is already available on this item.
:param content: The title text of the item, defaults to None
:type content: Union[str, None], optional
:param icon: The image for the item, defaults to None
:type icon: Union[XABase.XAImage, None], optional
:param action: The method to call when the item is clicked, defaults to None
:type action: Union[Callable[[], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
:param icon_dimensions: The width and height of the image, in pixels, defaults to (20, 20)
:type icon_dimensions: tuple[int, int], optional
:param id: A unique identifier for the item, defaults to None
:type id: Union[str, None], optional
:param index: The position to insert the item in the list of menu items, defaults to -1
:type index: int, optional
:param label: The label to the left of the item's content
:type label: Union[str, None], optional
:param tooltip: The tooltip text for the menu item, defaults to None
:type tooltip: Union[str, None], optional
:return: The newly created menu item object
:rtype: XAMenuBarMenuItem
:Example:
>>> import PyXA
>>> menu_bar = PyXA.XAMenuBar()
>>> m1 = menu_bar.new_menu("Menu 1")
>>> i1 = m1.new_item("Item 1")
>>> i2 = i1.new_subitem("Item 1.1")
>>> i3 = i2.new_subitem("Item 1.1.1")
>>> i4 = i3.new_subitem("Item 1.1.1.1")
>>> menu_bar.display()
.. versionadded:: 0.1.1
"""
# If no ID provided, use the title, then make it unique
content = content or ""
id = id or str(content)
while id in self.items:
id += "_"
# Create a new submenu, if necessary
submenu = self.xa_elem.submenu()
if submenu is None:
submenu = AppKit.NSMenu.alloc().init()
if index == -1:
index = submenu.numberOfItems()
subitem = None
if content == "separator":
subitem = XASeparatorMenuItem(self)
elif content == "slider":
subitem = XASliderMenuItem(self, action, tooltip, args)
elif content == "switch":
subitem = XASwitchMenuItem(self, label, tooltip, action, args)
elif isinstance(content, XABase.XAImage):
subitem = XAImageMenuItem(self, content, tooltip)
elif isinstance(content, XABase.XAURL) or isinstance(content, XABase.XAPath):
subitem = XAURLMenuItem(
self, content, label, icon, icon_dimensions, tooltip, action, args
)
elif (
isinstance(content, str)
or isinstance(content, int)
or isinstance(content, float)
or isinstance(content, bool)
):
subitem = XATextMenuItem(
self, str(content), icon, icon_dimensions, tooltip, action, args
)
else:
subitem = XAMenuBarMenuItem(
self,
content,
icon,
action,
args,
icon_dimensions,
id,
index,
label,
tooltip,
)
submenu.insertItem_atIndex_(subitem.xa_elem, index)
self.xa_elem.menu().setSubmenu_forItem_(submenu, self.xa_elem)
self.items[id] = subitem
return self.items[id]
[docs]
def remove_subitem(self, id: str):
"""Removes a subitem from this item's submenu.
:param id: The ID of the subitem to remove
:type id: str
.. versionadded:: 0.1.1
"""
item = self.items.pop(id)
self.xa_elem.submenu().removeItem_(item.xa_elem)
[docs]
class XAURLMenuItem(XATextMenuItem):
"""A menu item containing a URL or path.
.. versionadded:: 0.1.2
"""
def __init__(
self,
parent: XAMenuBarMenu,
url: Union[XABase.XAURL, XABase.XAPath],
label: Union[str, None] = None,
icon: Union["XABase.XAImage", None] = None,
icon_dimensions: tuple[int, int] = (20, 20),
tooltip: Union[str, None] = None,
action: Union[Callable[["XAURLMenuItem", int, Any], None], None] = None,
args: Union[list[Any], None] = None,
):
"""Initializes a URL menu item.
:param parent: The menu containing this item
:type parent: XAMenuBarMenu
:param url: The URL or path to display
:type url: Union[XABase.XAURL, XABase.XAPath]
:param label: The text displayed for the item, or None to use the raw URL/path, defaults to None
:type label: Union[str, None], optional
:param icon: The image for the item, defaults to None
:type icon: Union[XABase.XAImage, None], optional
:param icon_dimensions: The width and height of the icon, in pixels, defaults to (20, 20)
:type icon_dimensions: tuple[int, int], optional
:param action: The method to call when the user clicks the menu item, defaults to None
:type action: Union[Callable[[XAURLMenuItem, int, Any], None], None], optional
:param args: The arguments to pass to the action method upon execution, defaults to None
:type args: Union[list[Any], None], optional
.. versionadded:: 0.1.2
"""
self.__location = url
self.xa_elem = AppKit.NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
label or str(url.xa_elem), "action:", ""
)
if action is None:
action = lambda item, button, *args: url.open() if button != -1 else None
super().__init__(
parent,
label or str(url.xa_elem),
icon=icon,
icon_dimensions=icon_dimensions,
tooltip=tooltip,
action=action,
args=args,
)
@property
def location(self) -> Union[XABase.XAURL, XABase.XAPath]:
"""The URL or path that the menu item refers to."""
return self.__location
@location.setter
def location(self, location: Union[XABase.XAURL, XABase.XAPath]):
self.__location = location
self.xa_elem.setTitle_(self.label or str(location.xa_elem))
@property
def label(self) -> str:
return self.xa_elem.title()
@label.setter
def label(self, label: Union[str, None]):
self.xa_elem.setTitle_(label or str(self.content.xa_elem))
[docs]
class XAHeaderMenuItem(XAMenuBarMenuItem):
"""A section header within a menu.
.. versionadded:: 0.2.0
"""
def __init__(self, parent: XAMenuBarMenu, label: str):
"""Initializes a URL menu item.
:param parent: The menu containing this item
:type parent: XAMenuBarMenu
:param url: The URL or path to display
:param label: The text displayed for the item
:type label: str
.. versionadded:: 0.2.0
"""
self.xa_elem = AppKit.NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
label, None, ""
)
font = AppKit.NSFont.systemFontOfSize_weight_(10, AppKit.NSFontWeightSemibold)
self.xa_elem.setFont_(font)
self.xa_elem.setEnabled_(False)
super().__init__(parent, None)