+""".. versionadded:: 0.0.1
+
+General classes and methods applicable to any PyXA object.
+"""
+
+import importlib
+import math
+import os
+import random
+import sys
+import tempfile
+import threading
+import time
+from datetime import datetime, timedelta
+from enum import Enum
+from pprint import pprint
+from typing import Any, Callable, Literal, Self, Union
+
+import AppKit
+import Quartz
+import requests
+import ScriptingBridge
+from bs4 import BeautifulSoup, element
+from PyObjCTools import AppHelper
+
+from PyXA.XAErrors import ApplicationNotFoundError, InvalidPredicateError
+from PyXA.XAProtocols import XACanOpenPath, XAClipboardCodable, XAPathLike
+
+from .apps import application_classes
+
+
+[docs]def OSType(s: str):
+
return int.from_bytes(s.encode("UTF-8"), "big")
+
+[docs]def unOSType(i: int):
+
return i.to_bytes((i.bit_length() + 7) // 8, 'big').decode()
+
+VERSION = "0.1.2" #: The installed version of PyXA
+supported_applications: list[str] = list(application_classes.keys()) #: A list of names of supported scriptable applications
+
+workspace = None
+
+###############
+### General ###
+###############
+[docs]class XAObject():
+
"""A general class for PyXA scripting objects.
+
+
.. seealso:: :class:`XABaseScriptable.XASBObject`
+
+
.. versionadded:: 0.0.1
+
"""
+
_xa_sevt = None
+
_xa_estr = None
+
_xa_wksp = None
+
+
[docs] def __init__(self, properties: dict = None):
+
"""Instantiates a PyXA scripting object.
+
+
:param properties: A dictionary of properties to assign to this object.
+
:type properties: dict, optional
+
+
.. versionchanged:: 0.0.3
+
Removed on-the-fly creation of class attributes. All objects should concretely define their properties.
+
+
.. versionadded:: 0.0.1
+
"""
+
if properties is not None:
+
self.xa_prnt = properties.get("parent", None)
+
self.xa_elem = properties.get("element", None)
+
self.xa_scel = properties.get("scriptable_element", None)
+
self.xa_aref = properties.get("appref", None)
+
+
@property
+
def xa_wksp(self):
+
return workspace
+
+
@property
+
def xa_sevt(self):
+
if XAObject._xa_sevt is None:
+
XAObject._xa_sevt = Application("System Events")
+
return XAObject._xa_sevt
+
+
@property
+
def xa_estr(self):
+
if XAObject._xa_estr is None:
+
import EventKit
+
XAObject._xa_estr = self._exec_suppresed(EventKit.EKEventStore.alloc().init)
+
return XAObject._xa_estr
+
+
def _exec_suppresed(self, f: Callable[..., Any], *args: Any) -> Any:
+
"""Silences unwanted and otherwise unavoidable warning messages.
+
+
Taken from: https://stackoverflow.com/a/3946828
+
+
:param f: The function to execute
+
:type f: Callable[...]
+
:param args: The parameters to pass to the specified function
+
:type args: Any
+
:raises error: Any exception that occurs while trying to run the specified function
+
:return: The value returned by the specified function upon execution
+
:rtype: Any
+
+
.. versionadded:: 0.0.2
+
"""
+
error = None
+
value = None
+
+
old_stderr = os.dup(sys.stderr.fileno())
+
fd = os.open('/dev/null', os.O_CREAT | os.O_WRONLY)
+
os.dup2(fd, sys.stderr.fileno())
+
try:
+
value = f(*args)
+
except Exception as e:
+
error = e
+
os.dup2(old_stderr, sys.stderr.fileno())
+
+
if error is not None:
+
raise error
+
return value
+
+
def _new_element(self, obj: 'AppKit.NSObject', obj_class: type = 'XAObject', *args: list[Any]) -> 'XAObject':
+
"""Wrapper for creating a new PyXA object.
+
+
:param folder_obj: The Objective-C representation of an object.
+
:type folder_obj: NSObject
+
:return: The PyXA representation of the object.
+
:rtype: XAObject
+
+
.. versionchannged:: 0.1.2
+
+
Now returns `None` if no object is provided or if the object itself is `None`.
+
+
.. versionadded:: 0.0.1
+
"""
+
if obj is None:
+
return None
+
+
properties = {
+
"parent": self,
+
"element": obj,
+
"appref": getattr(self, "xa_aref", None),
+
}
+
return obj_class(properties, *args)
+
+
def _spawn_thread(self, function: Callable[..., Any], args: Union[list[Any], None] = None, kwargs: Union[list[Any], None] = None, daemon: bool = True) -> threading.Thread:
+
"""Spawns a new thread running the specified function.
+
+
:param function: The function to run in the new thread
+
:type function: Callable[..., Any]
+
:param args: Arguments to pass to the function
+
:type args: list[Any]
+
:param kwargs: Keyword arguments to pass to the function
+
:type kwargs: list[Any]
+
:param daemon: Whether the thread should be a daemon thread, defaults to True
+
:type daemon: bool, optional
+
:return: The thread object
+
:rtype: threading.Thread
+
+
.. versionadded:: 0.0.9
+
"""
+
new_thread = threading.Thread(target=function, args=args or [], kwargs=kwargs or {}, daemon=daemon)
+
new_thread.start()
+
return new_thread
+
+
[docs] def has_element(self) -> bool:
+
"""Whether this object has an AppleScript/JXA/Objective-C scripting element associated with it.
+
+
:return: True if this object's element attribute is set, False otherwise.
+
:rtype: bool
+
+
.. deprecated:: 0.0.9
+
+
Perform this check manually instead.
+
+
.. versionadded:: 0.0.1
+
"""
+
return self.xa_elem is not None
+
+
[docs] def has_element_properties(self) -> bool:
+
"""Whether the scripting element associated with this object has properties attached to it.
+
+
:return: True if this object's properties attribute is set, False otherwise.
+
:rtype: bool
+
+
.. deprecated:: 0.0.8
+
All elements now have a properties dictionary, even if it is empty.
+
+
.. versionadded:: 0.0.1
+
"""
+
return self.element_properties != None
+
+
[docs] def set_element(self, element: 'XAObject') -> 'XAObject':
+
"""Sets the element attribute to the supplied element and updates the properties attribute accordingly.
+
+
:param element: The new scripting element to reference via the element attribute.
+
:type element: XAObject
+
:return: A reference to this PyXA object.
+
:rtype: XAObject
+
+
.. deprecated:: 0.0.9
+
+
Set the element attribute directly instead.
+
+
.. versionadded:: 0.0.1
+
"""
+
self.xa_elem = element
+
return self
+
+
[docs] def set_properties(self, properties: dict) -> 'XAObject':
+
"""Updates the value of multiple properties of the scripting element associated with this object.
+
+
:param properties: A dictionary defining zero or more property names and updated values as key-value pairs.
+
:type properties: dict
+
:return: A reference to this PyXA object.
+
:rtype: XAObject
+
+
.. versionadded:: 0.0.1
+
"""
+
property_dict = {}
+
for key in properties:
+
parts = key.split("_")
+
titled_parts = [part.title() for part in parts[1:]]
+
property_name = parts[0] + "".join(titled_parts)
+
property_dict[property_name] = properties[key]
+
self.xa_elem.setValuesForKeysWithDictionary_(property_dict)
+
return self
+
+
[docs] def set_property(self, property_name: str, value: Any) -> 'XAObject':
+
"""Updates the value of a single property of the scripting element associated with this object.
+
+
:param property: The name of the property to assign a new value to.
+
:type property: str
+
:param value: The value to assign to the specified property.
+
:type value: Any
+
:return: A reference to this PyXA object.
+
:rtype: XAObject
+
+
.. versionadded:: 0.0.1
+
"""
+
if "_" in property_name:
+
parts = property_name.split("_")
+
titled_parts = [part.title() for part in parts[1:]]
+
property_name = parts[0] + "".join(titled_parts)
+
self.xa_elem.setValue_forKey_(value, property_name)
+
return self
+
+
[docs] def __eq__(self, other: 'XAObject'):
+
if other is None:
+
return False
+
+
if hasattr(self.xa_elem, "get"):
+
return self.xa_elem.get() == other.xa_elem.get()
+
+
if isinstance(other, list) or isinstance(other.xa_elem, AppKit.NSArray):
+
return len(self.xa_elem) == len(other.xa_elem) and all([x == y for x, y in zip(self.xa_elem, other.xa_elem)])
+
+
return self.xa_elem == other.xa_elem
+
+
+
+
+[docs]class XAList(XAObject):
+
"""A wrapper around NSArray and NSMutableArray objects enabling fast enumeration and lazy evaluation of Objective-C objects.
+
+
.. versionadded:: 0.0.3
+
"""
+
[docs] def __init__(self, properties: dict, object_class: type = None, filter: Union[dict, None] = None):
+
"""Creates an efficient wrapper object around a list of scriptable elements.
+
+
:param properties: PyXA properties passed to this object for utility purposes
+
:type properties: dict
+
:param object_class: _description_, defaults to None
+
:type object_class: type, optional
+
:param filter: A dictionary of properties and values to filter items by, defaults to None
+
:type filter: Union[dict, None], optional
+
+
.. versionchanged:: 0.0.8
+
The filter property is deprecated and will be removed in a future version. Use the :func:`filter` method instead.
+
+
.. versionadded:: 0.0.3
+
"""
+
super().__init__(properties)
+
self.xa_ocls = object_class
+
+
if not isinstance(self.xa_elem, AppKit.NSArray):
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(self.xa_elem)
+
+
if filter is not None:
+
self.xa_elem = XAPredicate().from_dict(filter).evaluate(self.xa_elem)
+
+
[docs] def by_property(self, property: str, value: Any) -> XAObject:
+
"""Retrieves the first element whose property value matches the given value, if one exists.
+
+
:param property: The property to match
+
:type property: str
+
:param value: The value to match
+
:type value: Any
+
:return: The matching element, if one is found
+
:rtype: XAObject
+
+
:Example:
+
+
>>> import PyXA
+
>>> app = PyXA.Application("Photos")
+
>>> photo = app.media_items().by_property("id", "CB24FE9F-E9DC-4A5C-A0B0-CC779B1CEDCE/L0/001")
+
>>> print(photo)
+
<<class 'PyXA.apps.PhotosApp.XAPhotosMediaItem'>id=CB24FE9F-E9DC-4A5C-A0B0-CC779B1CEDCE/L0/001>
+
+
.. versionadded:: 0.0.6
+
"""
+
predicate = XAPredicate()
+
predicate.add_eq_condition(property, value)
+
ls = predicate.evaluate(self.xa_elem)
+
# if hasattr(ls, "get"):
+
# ls = predicate.evaluate(self.xa_elem).get()
+
+
if len(ls) == 0:
+
return None
+
+
obj = ls[0]
+
return self._new_element(obj, self.xa_ocls)
+
+
[docs] def equalling(self, property: str, value: str) -> XAObject:
+
"""Retrieves all elements whose property value equals the given value.
+
+
:param property: The property to match
+
:type property: str
+
:param value: The value to search for
+
:type value: str
+
:return: The list of matching elements
+
:rtype: XAList
+
+
:Example:
+
+
>>> import PyXA
+
>>> app = PyXA.Application("TV")
+
>>> print(app.tracks().equalling("playedCount", 0))
+
<<class 'PyXA.apps.TV.XATVTrackList'>['Frozen', 'Sunshine', 'The Hunger Games: Mockingjay - Part 2', ...]>
+
+
.. versionadded:: 0.1.0
+
"""
+
predicate = XAPredicate()
+
predicate.add_eq_condition(property, value)
+
ls = predicate.evaluate(self.xa_elem)
+
return self._new_element(ls, self.__class__)
+
+
[docs] def not_equalling(self, property: str, value: str) -> XAObject:
+
"""Retrieves all elements whose property value does not equal the given value.
+
+
:param property: The property to match
+
:type property: str
+
:param value: The value to search for
+
:type value: str
+
:return: The list of matching elements
+
:rtype: XAList
+
+
:Example:
+
+
>>> import PyXA
+
>>> app = PyXA.Application("TV")
+
>>> print(app.tracks().not_equalling("playedCount", 0))
+
<<class 'PyXA.apps.TV.XATVTrackList'>['The Avatar State', 'The Cave of Two Lovers', 'Return to Omashu', ...]>
+
+
.. versionadded:: 0.1.0
+
"""
+
predicate = XAPredicate()
+
predicate.add_neq_condition(property, value)
+
ls = predicate.evaluate(self.xa_elem)
+
return self._new_element(ls, self.__class__)
+
+
[docs] def containing(self, property: str, value: str) -> XAObject:
+
"""Retrieves all elements whose property value contains the given value.
+
+
:param property: The property to match
+
:type property: str
+
:param value: The value to search for
+
:type value: str
+
:return: The list of matching elements
+
:rtype: XAList
+
+
:Example:
+
+
>>> import PyXA
+
>>> app = PyXA.Application("Reminders")
+
>>> print(app.reminders().containing("name", "PyXA"))
+
<<class 'PyXA.apps.Reminders.XARemindersReminderList'>['PyXA v0.1.0 release']>
+
+
.. versionadded:: 0.0.6
+
"""
+
predicate = XAPredicate()
+
predicate.add_contains_condition(property, value)
+
ls = predicate.evaluate(self.xa_elem)
+
return self._new_element(ls, self.__class__)
+
+
[docs] def not_containing(self, property: str, value: str) -> XAObject:
+
"""Retrieves all elements whose property value does not contain the given value.
+
+
:param property: The property to match
+
:type property: str
+
:param value: The value to search for
+
:type value: str
+
:return: The list of matching elements
+
:rtype: XAList
+
+
:Example:
+
+
>>> import PyXA
+
>>> app = PyXA.Application("Reminders")
+
>>> print(app.reminders().not_containing("name", " "))
+
<<class 'PyXA.apps.Reminders.XARemindersReminderList'>['Trash', 'Thing', 'Reminder', ...]>
+
+
.. versionadded:: 0.1.0
+
"""
+
ls = XAPredicate.evaluate_with_format(self.xa_elem, f"NOT {property} CONTAINS \"{value}\"")
+
return self._new_element(ls, self.__class__)
+
+
[docs] def beginning_with(self, property: str, value: str) -> XAObject:
+
"""Retrieves all elements whose property value begins with the given value.
+
+
:param property: The property to match
+
:type property: str
+
:param value: The value to search for
+
:type value: str
+
:return: The list of matching elements
+
:rtype: XAList
+
+
:Example:
+
+
>>> import PyXA
+
>>> app = PyXA.Application("System Events")
+
>>> print(app.downloads_folder.files().beginning_with("name", "Example"))
+
<<class 'PyXA.apps.SystemEvents.XASystemEventsFileList'>['Example.png', 'ExampleImage.png', ...]>
+
+
.. versionadded:: 0.1.0
+
"""
+
predicate = XAPredicate()
+
predicate.add_begins_with_condition(property, value)
+
ls = predicate.evaluate(self.xa_elem)
+
return self._new_element(ls, self.__class__)
+
+
[docs] def ending_with(self, property: str, value: str) -> XAObject:
+
"""Retrieves all elements whose property value ends with the given value.
+
+
:param property: The property to match
+
:type property: str
+
:param value: The value to search for
+
:type value: str
+
:return: The list of matching elements
+
:rtype: XAList
+
+
:Example:
+
+
>>> import PyXA
+
>>> app = PyXA.Application("System Events")
+
>>> print(app.downloads_folder.files().ending_with("name", ".png"))
+
<<class 'PyXA.apps.SystemEvents.XASystemEventsFileList'>['Example.png', 'Image.png', ...]>
+
+
.. versionadded:: 0.1.0
+
"""
+
predicate = XAPredicate()
+
predicate.add_ends_with_condition(property, value)
+
ls = predicate.evaluate(self.xa_elem)
+
return self._new_element(ls, self.__class__)
+
+
[docs] def greater_than(self, property: str, value: Union[int, float]) -> XAObject:
+
"""Retrieves all elements whose property value is greater than the given value.
+
+
:param property: The property to match
+
:type property: str
+
:param value: The value to compare against
+
:type value: Union[int, float]
+
:return: The list of matching elements
+
:rtype: XAList
+
+
:Example:
+
+
>>> import PyXA
+
>>> app = PyXA.Application("Photos")
+
>>> print(app.media_items().greater_than("altitude", 10000)[0].spotlight())
+
<<class 'PyXA.apps.PhotosApp.XAPhotosMediaItem'>id=53B0F28E-0B39-446B-896C-484CD0DC2D3C/L0/001>
+
+
.. versionadded:: 0.1.0
+
"""
+
predicate = XAPredicate()
+
predicate.add_gt_condition(property, value)
+
ls = predicate.evaluate(self.xa_elem)
+
return self._new_element(ls, self.__class__)
+
+
[docs] def less_than(self, property: str, value: Union[int, float]) -> XAObject:
+
"""Retrieves all elements whose property value is less than the given value.
+
+
:param property: The property to match
+
:type property: str
+
:param value: The value to compare against
+
:type value: Union[int, float]
+
:return: The list of matching elements
+
:rtype: XAList
+
+
:Example:
+
+
>>> app = PyXA.Application("Music")
+
>>> tracks = app.tracks()
+
>>> print(tracks.less_than("playedCount", 5).name())
+
['Outrunning Karma', 'Death of a Hero', '1994', 'Mind Is a Prison']
+
+
.. versionadded:: 0.1.0
+
"""
+
predicate = XAPredicate()
+
predicate.add_lt_condition(property, value)
+
ls = predicate.evaluate(self.xa_elem)
+
return self._new_element(ls, self.__class__)
+
+
[docs] def between(self, property: str, value1: Union[int, float], value2: Union[int, float]) -> XAObject:
+
"""Retrieves all elements whose property value is between the given values.
+
+
:param property: The property to match
+
:type property: str
+
:param value1: The lower-end of the range to match
+
:type value1: Union[int, float]
+
:param value2: The upper-end of the range to match
+
:type value2: Union[int, float]
+
:return: The list of matching elements
+
:rtype: XAList
+
+
:Example:
+
+
>>> import PyXA
+
>>> from datetime import datetime, timedelta
+
>>>
+
>>> app = PyXA.Application("Calendar")
+
>>> events = app.calendars()[3].events()
+
>>> now = datetime.now()
+
>>> print(events.between("startDate", now, now + timedelta(days=1)))
+
<<class 'PyXA.apps.Calendar.XACalendarEventList'>['Capstone Meeting', 'Lunch with Dan']>
+
+
.. versionadded:: 0.1.0
+
"""
+
predicate = XAPredicate()
+
predicate.add_gt_condition(property, value1)
+
predicate.add_lt_condition(property, value2)
+
ls = predicate.evaluate(self.xa_elem).get()
+
return self._new_element(ls, self.__class__)
+
+
[docs] def filter(self, filter: str, comparison_operation: Union[str, None] = None, value1: Union[Any, None] = None, value2: Union[Any, None] = None) -> 'XAList':
+
"""Filters the list by the given parameters.
+
+
The filter may be either a format string, used to create an NSPredicate, or up to 4 arguments specifying the filtered property name, the comparison operation, and up to two values to compare against.
+
+
:param filter: A format string or a property name
+
:type filter: str
+
:param comparison_operation: The symbol or name of a comparison operation, such as > or <, defaults to None
+
:type comparison_operation: Union[str, None], optional
+
:param value1: The first value to compare each list item's property value against, defaults to None
+
:type value1: Union[Any, None], optional
+
:param value2: The second value to compare each list item's property value against, defaults to None
+
:type value2: Union[Any, None], optional
+
:return: The filtered XAList object
+
:rtype: XAList
+
+
:Example 1: Get the last file sent by you (via this machine) in Messages.app
+
+
>>> import PyXA
+
>>> app = PyXA.Application("Messages")
+
>>> last_file_transfer = app.file_transfers().filter("direction", "==", app.MessageDirection.OUTGOING)[-1]
+
>>> print(last_file_transfer)
+
<<class 'PyXA.apps.Messages.XAMessagesFileTransfer'>Test.jpg>
+
+
:Example 2: Get the list of favorite photos/videos from Photos.app
+
+
>>> import PyXA
+
>>> app = PyXA.Application("Photos")
+
>>> favorites = app.media_items().filter("favorite", "==", True)
+
>>> print(favorites)
+
<<class 'PyXA.apps.PhotosApp.XAPhotosMediaItemList'>['CB24FE9F-E9DC-4A5C-A0B0-CC779B1CEDCE/L0/001', 'EFEB7F37-8373-4972-8E43-21612F597185/L0/001', ...]>
+
+
.. note::
+
+
For properties that appear to be boolean but fail to return expected filter results, try using the corresponding 0 or 1 value instead.
+
+
:Example 3: Provide a custom format string
+
+
>>> import PyXA
+
>>> app = PyXA.Application("Photos")
+
>>> photo = app.media_items().filter("id == 'CB24FE9F-E9DC-4A5C-A0B0-CC779B1CEDCE/L0/001'")[0]
+
>>> print(photo)
+
<<class 'PyXA.apps.PhotosApp.XAPhotosMediaItem'>id=CB24FE9F-E9DC-4A5C-A0B0-CC779B1CEDCE/L0/001>
+
+
.. versionadded:: 0.0.8
+
"""
+
if comparison_operation is not None and value1 is not None:
+
predicate = XAPredicate()
+
if comparison_operation in ["=", "==", "eq", "EQ", "equals", "EQUALS"]:
+
predicate.add_eq_condition(filter, value1)
+
elif comparison_operation in ["!=", "!==", "neq", "NEQ", "not equal to", "NOT EQUAL TO"]:
+
predicate.add_neq_condition(filter, value1)
+
elif comparison_operation in [">", "gt", "GT", "greater than", "GREATER THAN"]:
+
predicate.add_gt_condition(filter, value1)
+
elif comparison_operation in ["<", "lt", "LT", "less than", "LESS THAN"]:
+
predicate.add_lt_condition(filter, value1)
+
elif comparison_operation in [">=", "geq", "GEQ", "greater than or equal to", "GREATER THAN OR EQUAL TO"]:
+
predicate.add_geq_condition(filter, value1)
+
elif comparison_operation in ["<=", "leq", "LEQ", "less than or equal to", "LESS THAN OR EQUAL TO"]:
+
predicate.add_leq_condition(filter, value1)
+
elif comparison_operation in ["begins with", "beginswith", "BEGINS WITH", "BEGINSWITH"]:
+
predicate.add_begins_with_condition(filter, value1)
+
elif comparison_operation in ["contains", "CONTAINS"]:
+
predicate.add_contains_condition(filter, value1)
+
elif comparison_operation in ["ends with", "endswith", "ENDS WITH", "ENDSWITH"]:
+
predicate.add_ends_with_condition(filter, value1)
+
elif comparison_operation in ["between", "BETWEEN"]:
+
predicate.add_between_condition(filter, value1, value2)
+
elif comparison_operation in ["matches", "MATCHES"]:
+
predicate.add_match_condition(filter, value1)
+
+
filtered_list = predicate.evaluate(self.xa_elem)
+
return super()._new_element(filtered_list, self.__class__)
+
else:
+
filtered_list = XAPredicate.evaluate_with_format(self.xa_elem, filter)
+
return super()._new_element(filtered_list, self.__class__)
+
+
[docs] def at(self, index: int) -> XAObject:
+
"""Retrieves the element at the specified index.
+
+
:param index: The index of the desired element
+
:type index: int
+
:return: The PyXA-wrapped element object
+
:rtype: XAObject
+
+
.. versionadded:: 0.0.6
+
"""
+
return self._new_element(self.xa_elem[index], self.xa_ocls)
+
+
@property
+
def first(self) -> XAObject:
+
"""Retrieves the first element of the list as a wrapped PyXA object.
+
+
:return: The wrapped object
+
:rtype: XAObject
+
+
.. versionadded:: 0.0.3
+
"""
+
return self._new_element(self.xa_elem.firstObject(), self.xa_ocls)
+
+
@property
+
def last(self) -> XAObject:
+
"""Retrieves the last element of the list as a wrapped PyXA object.
+
+
:return: The wrapped object
+
:rtype: XAObject
+
+
.. versionadded:: 0.0.3
+
"""
+
return self._new_element(self.xa_elem.lastObject(), self.xa_ocls)
+
+
[docs] def shuffle(self) -> 'XAList':
+
"""Randomizes the order of objects in the list.
+
+
:return: A reference to the shuffled XAList
+
:rtype: XAList
+
+
.. versionadded:: 0.0.3
+
"""
+
try:
+
self.xa_elem = self.xa_elem.shuffledArray()
+
except AttributeError:
+
random.shuffle(self.xa_elem)
+
return self
+
+
[docs] def extend(self, ls: Union['XAList', list]):
+
"""Appends all elements of the supplied list to the end of this list.
+
+
:param ls: _description_
+
:type ls: Union[XAList, list]
+
+
.. versionadded:: 0.1.1
+
"""
+
arr1 = AppKit.NSMutableArray.alloc().initWithArray_(self.xa_elem)
+
+
if isinstance(ls, XAList):
+
ls = ls.xa_elem
+
else:
+
ls = AppKit.NSMutableArray.alloc().initWithArray_(ls)
+
+
arr1.addObjectsFromArray_(ls)
+
self.xa_elem = arr1
+
+
[docs] def push(self, *elements: list[XAObject]) -> Union[XAObject, list[XAObject], None]:
+
"""Appends the object referenced by the provided PyXA wrapper to the end of the list.
+
+
.. versionadded:: 0.0.3
+
"""
+
objects = []
+
num_added = 0
+
+
for element in elements:
+
len_before = len(self.xa_elem)
+
self.xa_elem.addObject_(element.xa_elem)
+
len_after = len(self.xa_elem)
+
+
if len_after == len_before:
+
# Object wasn't added -- try force-getting the list before adding
+
self.xa_elem.get().addObject_(element.xa_elem)
+
+
if len_after > len_before:
+
num_added += 1
+
objects.append(self[len_after - 1])
+
+
if num_added == 1:
+
return objects[0]
+
+
if num_added == 0:
+
return None
+
+
return objects
+
+
[docs] def insert(self, element: XAObject, index: int):
+
"""Inserts the object referenced by the provided PyXA wrapper at the specified index.
+
+
.. versionadded:: 0.0.3
+
"""
+
self.xa_elem.insertObject_atIndex_(element.xa_elem, index)
+
+
[docs] def pop(self, index: int = -1) -> XAObject:
+
"""Removes the object at the specified index from the list and returns it.
+
+
.. versionadded:: 0.0.3
+
"""
+
removed = self.xa_elem.lastObject()
+
self.xa_elem.removeLastObject()
+
return self._new_element(removed, self.xa_ocls)
+
+
[docs] def index(self, element: XAObject) -> int:
+
"""Returns the index of the first occurrence of the element in the list, or -1 if no such element exists in the list.
+
+
.. versionadded:: 0.1.2
+
"""
+
for index, item in enumerate(self.xa_elem):
+
if item == element.xa_elem:
+
return index
+
+
for index, item in enumerate(self):
+
if item == element:
+
return index
+
+
return -1
+
+
[docs] def count(self, count_function: Callable[[object], bool]) -> int:
+
"""Counts the number of entries in the list for which the provided function is True.
+
+
:param count_function: The function to check entries against
+
:type count_function: Callable[[object], bool]
+
:return: The number of entries for which the given function is True.
+
:rtype: int
+
+
.. versionadded:: 0.1.0
+
"""
+
count = 0
+
for index in range(len(self)):
+
in_count = False
+
try:
+
in_count = count_function(self.xa_elem[index])
+
except:
+
# TODO: Add logging message here
+
pass
+
+
if not in_count:
+
try:
+
in_count = count_function(self[index])
+
except:
+
pass
+
+
if in_count:
+
count += 1
+
return count
+
+
[docs] def __getitem__(self, key: Union[int, slice]):
+
if isinstance(key, slice):
+
arr = AppKit.NSMutableArray.alloc().initWithArray_([self.xa_elem[index] for index in range(key.start, key.stop, key.step or 1)])
+
return self._new_element(arr, self.__class__)
+
if key < 0:
+
key = self.xa_elem.count() + key
+
return self._new_element(self.xa_elem.objectAtIndex_(key), self.xa_ocls)
+
+
[docs] def __len__(self):
+
return len(self.xa_elem)
+
+
[docs] def __reversed__(self):
+
self.xa_elem = self.xa_elem.reverseObjectEnumerator().allObjects()
+
return self
+
+
[docs] def __iter__(self):
+
return (self._new_element(object, self.xa_ocls) for object in self.xa_elem.objectEnumerator())
+
+
[docs] def __contains__(self, item):
+
if isinstance(item, XAObject):
+
item = item.xa_elem
+
return item in self.xa_elem
+
+
[docs] def __repr__(self):
+
return "<" + str(type(self)) + str(self.xa_elem) + ">"
+
+
+
+
+[docs]class XAApplicationList(XAList):
+
"""A wrapper around a list of applications.
+
+
.. versionadded:: 0.0.5
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, XAApplication, filter)
+
+
if filter is not None:
+
self.xa_elem = XAPredicate().from_dict(filter).evaluate(self.xa_elem)
+
+
[docs] def first(self) -> XAObject:
+
"""Retrieves the first element of the list as a wrapped PyXA application object.
+
+
:return: The wrapped object
+
:rtype: XAObject
+
+
.. versionadded:: 0.0.5
+
"""
+
return self.__getitem__(0)
+
+
[docs] def last(self) -> XAObject:
+
"""Retrieves the last element of the list as a wrapped PyXA application object.
+
+
:return: The wrapped object
+
:rtype: XAObject
+
+
.. versionadded:: 0.0.5
+
"""
+
return self.__getitem__(-1)
+
+
[docs] def pop(self, index: int = -1) -> XAObject:
+
"""Removes the application at the specified index from the list and returns it.
+
+
.. versionadded:: 0.0.5
+
"""
+
removed = self.xa_elem.lastObject()
+
self.xa_elem.removeLastObject()
+
app_name = removed["kCGWindowOwnerName"]
+
return Application(app_name)
+
+
[docs] def __getitem__(self, key: Union[int, slice]):
+
"""Retrieves the wrapped application object(s) at the specified key.
+
"""
+
if isinstance(key, slice):
+
arr = AppKit.NSArray.alloc().initWithArray_([self.xa_elem[index] for index in range(key.start, key.stop, key.step or 1)])
+
return self._new_element(arr, self.__class__)
+
app_name = self.xa_elem[key]["kCGWindowOwnerName"]
+
return Application(app_name)
+
+
[docs] def bundle_identifier(self) -> list[str]:
+
return [app.bundle_identifier for app in self]
+
+
[docs] def bundle_url(self) -> list['XAURL']:
+
return [XAURL(app.bundle_url)for app in self]
+
+
[docs] def executable_url(self) -> list['XAURL']:
+
return [XAURL(app.executable_url) for app in self]
+
+
[docs] def launch_date(self) -> list[datetime]:
+
return [app.launch_date for app in self]
+
+
[docs] def localized_name(self) -> list[str]:
+
return [x.get("kCGWindowOwnerName") for x in self.xa_elem]
+
+
[docs] def process_identifier(self) -> list[str]:
+
return [x.get("kCGWindowOwnerPID") for x in self.xa_elem]
+
+
[docs] def by_bundle_identifier(self, bundle_identifier: str) -> Union['XAApplication', None]:
+
for app in self:
+
if app.bundle_identifier == bundle_identifier:
+
return app
+
+
[docs] def by_bundle_url(self, bundle_url: Union['XAURL', str]) -> Union['XAApplication', None]:
+
if isinstance(bundle_url, str):
+
bundle_url = XAURL(bundle_url)
+
+
for app in self:
+
if app.bundle_url.xa_elem == bundle_url.xa_elem:
+
return app
+
+
[docs] def by_executable_url(self, executable_url: Union['XAURL', str]) -> Union['XAApplication', None]:
+
if isinstance(executable_url, str):
+
executable_url = XAURL(executable_url)
+
+
for app in self:
+
if app.executable_url.xa_elem == executable_url.xa_elem:
+
return app
+
+
[docs] def by_launch_date(self, launch_date: datetime) -> Union['XAApplication', None]:
+
for app in self:
+
if app.launch_date == launch_date:
+
return app
+
+
[docs] def by_localized_name(self, localized_name: str) -> Union['XAApplication', None]:
+
for index, app in enumerate(self.xa_elem):
+
if app.get("kCGWindowOwnerName") == localized_name:
+
return self.__getitem__(index)
+
+
[docs] def by_process_identifier(self, process_identifier: str) -> Union['XAApplication', None]:
+
for index, app in enumerate(self.xa_elem):
+
if app.get("kCGWindowOwnerPID") == process_identifier:
+
return self.__getitem__(index)
+
+
[docs] def hide(self):
+
"""Hides all applications in the list.
+
+
:Example 1: Hide all visible running applications
+
+
>>> import PyXA
+
>>> apps = PyXA.running_applications()
+
>>> apps.hide()
+
+
.. seealso:: :func:`unhide`
+
+
.. versionadded:: 0.0.5
+
"""
+
for app in self:
+
app.hide()
+
+
[docs] def unhide(self):
+
"""Unhides all applications in the list.
+
+
:Example 1: Hide then unhide all visible running applications
+
+
>>> import PyXA
+
>>> apps = PyXA.running_applications()
+
>>> apps.hide()
+
>>> apps.unhide()
+
+
.. seealso:: :func:`hide`
+
+
.. versionadded:: 0.0.5
+
"""
+
for app in self:
+
app.unhide()
+
+
[docs] def terminate(self):
+
"""Quits (terminates) all applications in the list. Synonymous with :func:`quit`.
+
+
:Example 1: Terminate all visible running applications
+
+
>>> import PyXA
+
>>> apps = PyXA.running_applications()
+
>>> apps.terminate()
+
+
.. versionadded:: 0.0.5
+
"""
+
for app in self:
+
app.terminate()
+
+
[docs] def quit(self):
+
"""Quits (terminates) all applications in the list. Synonymous with :func:`terminate`.
+
+
:Example 1: Quit all visible running applications
+
+
>>> import PyXA
+
>>> apps = PyXA.running_applications()
+
>>> apps.quit()
+
+
.. versionadded:: 0.0.5
+
"""
+
for app in self:
+
app.terminate()
+
+
[docs] def windows(self) -> 'XAList':
+
"""Retrieves a list of every window belonging to each application in the list.
+
+
Operations on the list of windows will specialized to scriptable and non-scriptable application window operations as necessary.
+
+
:return: A list containing both scriptable and non-scriptable windows
+
:rtype: XAList
+
+
:Example:
+
+
>>> import PyXA
+
>>> windows = PyXA.running_applications().windows()
+
>>> windows.collapse()
+
>>> sleep(1)
+
>>> windows.uncollapse()
+
+
.. versionchanged 0.1.2
+
+
Now returns an instance of :class:`PyXA.apps.SystemEvents.XASystemEventsWindowList`
+
+
.. versionchanged:: 0.1.1
+
+
Now returns an instance of :class:`XAWindowList` instead of :class:`XACombinedWindowList`.
+
+
.. versionadded:: 0.0.5
+
"""
+
return self.xa_sevt.processes().windows()
+
+
[docs] def __iter__(self):
+
return (Application(object["kCGWindowOwnerName"]) for object in self.xa_elem.objectEnumerator())
+
+
[docs] def __contains__(self, item):
+
if isinstance(item, XAApplication):
+
return item.process_identifier in self.process_identifier()
+
+
[docs] def __repr__(self):
+
return "<" + str(type(self)) + str(self.localized_name()) + ">"
+
+[docs]class Application(XAObject):
+
"""A wrapper around a macOS application providing access to its scripting functionality.
+
+
.. versionchanged:: 0.1.1
+
+
Moved into the XABase module.
+
+
.. versionadded:: 0.1.0
+
"""
+
+
app_paths: list[str] = [] #: A list containing the path to each application
+
+
[docs] def __init__(self, app_name: str):
+
"""Creates a new application object.
+
+
:param app_name: The name of the target application
+
:type app_name: str
+
+
.. versionadded:: 0.1.0
+
"""
+
# Elevate to XAApplication
+
new_self = self.__get_application(app_name)
+
self.__class__ = new_self.__class__
+
self.__dict__.update(new_self.__dict__)
+
+
def __xa_get_path_to_app(self, app_identifier: str) -> str:
+
self.__xa_load_app_paths()
+
for path in self.app_paths:
+
if app_identifier.lower() in path.lower():
+
return path
+
+
raise ApplicationNotFoundError(app_identifier)
+
+
def __xa_load_app_paths(self):
+
if self.app_paths == []:
+
search = XASpotlight()
+
search.predicate = "kMDItemContentType == 'com.apple.application-bundle'"
+
search.run()
+
self.app_paths = [x.path for x in search.results]
+
+
def __get_application(self, app_identifier: str) -> 'XAApplication':
+
"""Retrieves a PyXA application object representation of the target application without launching or activating the application.
+
+
:param app_identifier: The name of the application to get an object of.
+
:type app_identifier: str
+
:return: A PyXA application object referencing the target application.
+
:rtype: XAApplication
+
+
.. versionadded:: 0.0.1
+
"""
+
global workspace
+
if workspace is None:
+
workspace = AppKit.NSWorkspace.sharedWorkspace()
+
+
app_identifier_l = app_identifier.lower()
+
+
def _match_open_app(obj, index, stop):
+
res = obj.localizedName().lower() == app_identifier_l
+
return res, res
+
+
idx_set = workspace.runningApplications().indexesOfObjectsPassingTest_(_match_open_app)
+
if idx_set.count() == 1:
+
index = idx_set.firstIndex()
+
app = workspace.runningApplications()[index]
+
properties = {
+
"parent": None,
+
"element": app,
+
"appref": app,
+
}
+
+
app_obj = application_classes.get(app_identifier_l, XAApplication)
+
if isinstance(app_obj, tuple):
+
module = importlib.import_module("PyXA.apps." + app_obj[0])
+
app_class = getattr(module, app_obj[1], None)
+
if app_class is not None:
+
application_classes[app_identifier_l] = app_class
+
app = app_class
+
else:
+
raise NotImplementedError()
+
+
# Check if the app is supported by PyXA
+
app_ref = application_classes.get(app_identifier_l, XAApplication)(properties)
+
return app_ref
+
+
app_path = app_identifier
+
if not app_identifier.startswith("/"):
+
app_path = self.__xa_get_path_to_app(app_identifier)
+
bundle = AppKit.NSBundle.alloc().initWithPath_(app_path)
+
url = workspace.URLForApplicationWithBundleIdentifier_(bundle.bundleIdentifier())
+
+
config = AppKit.NSWorkspaceOpenConfiguration.alloc().init()
+
config.setActivates_(False)
+
config.setHides_(True)
+
+
app_ref = None
+
def _launch_completion_handler(app, _error):
+
nonlocal app_ref
+
properties = {
+
"parent": None,
+
"element": app,
+
"appref": app,
+
}
+
+
app_obj = application_classes.get(app_identifier_l, None)
+
if isinstance(app_obj, tuple):
+
module = importlib.import_module("PyXA.apps." + app_obj[0])
+
app_class = getattr(module, app_obj[1], None)
+
if app_class is not None:
+
application_classes[app_identifier_l] = app_class
+
app = app_class
+
else:
+
raise NotImplementedError()
+
+
app_ref = application_classes.get(app_identifier_l, XAApplication)(properties)
+
+
workspace.openApplicationAtURL_configuration_completionHandler_(url, config, _launch_completion_handler)
+
while app_ref is None:
+
time.sleep(0.01)
+
return app_ref
+
+
+[docs]def current_application() -> 'XAApplication':
+
"""Retrieves a PyXA representation of the frontmost application.
+
+
:return: A PyXA application object referencing the current application.
+
:rtype: XAApplication
+
+
.. versionchanged:: 0.1.1
+
+
Moved into the XABase module.
+
+
.. versionadded:: 0.0.1
+
"""
+
global workspace
+
if workspace is None:
+
workspace = AppKit.NSWorkspace.sharedWorkspace()
+
return Application(workspace.frontmostApplication().localizedName())
+
+
+[docs]def running_applications() -> list['XAApplication']:
+
"""Gets PyXA references to all currently visible (not hidden or minimized) running applications whose app bundles are stored in typical application directories.
+
+
:return: A list of PyXA application objects.
+
:rtype: list[XAApplication]
+
+
:Example 1: Get the name of each running application
+
+
>>> import PyXA
+
>>> apps = PyXA.running_applications()
+
>>> print(apps.localized_name())
+
['GitHub Desktop', 'Safari', 'Code', 'Terminal', 'Notes', 'Messages', 'TV']
+
+
.. versionadded:: 0.0.1
+
"""
+
windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListOptionAll, Quartz.kCGNullWindowID)
+
ls = XAPredicate.evaluate_with_format(windows, "kCGWindowIsOnscreen == 1 && kCGWindowLayer == 0")
+
properties = {
+
"element": ls,
+
}
+
arr = XAApplicationList(properties)
+
return arr
+
+
+[docs]class XAApplication(XAObject, XAClipboardCodable):
+
"""A general application class for both officially scriptable and non-scriptable applications.
+
+
.. versionadded:: 0.0.1
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
self.__xa_prcs = None
+
+
@property
+
def xa_apsc(self):
+
import appscript
+
return appscript.app(self.bundle_url.path())
+
+
@property
+
def xa_prcs(self):
+
if self.__xa_prcs == None:
+
processes = self.xa_sevt.processes()
+
self.__xa_prcs = processes.by_displayed_name(self.xa_elem.localizedName())
+
return self.__xa_prcs
+
+
@property
+
def bundle_identifier(self) -> str:
+
"""The bundle identifier for the application.
+
+
.. versionadded:: 0.0.1
+
"""
+
return self.xa_elem.bundleIdentifier()
+
+
@property
+
def bundle_url(self) -> str:
+
"""The file URL of the application bundle.
+
+
.. versionadded:: 0.0.1
+
"""
+
return self.xa_elem.bundleURL()
+
+
@property
+
def executable_url(self) -> str:
+
"""The file URL of the application's executable.
+
+
.. versionadded:: 0.0.1
+
"""
+
return self.xa_elem.executableURL()
+
+
@property
+
def frontmost(self) -> bool:
+
"""Whether the application is the active application.
+
+
.. versionadded:: 0.0.1
+
"""
+
return self.xa_elem.isActive()
+
+
@frontmost.setter
+
def frontmost(self, frontmost: bool):
+
if frontmost is True:
+
self.xa_elem.activateWithOptions_(AppKit.NSApplicationActivateIgnoringOtherApps)
+
+
@property
+
def launch_date(self) -> datetime:
+
"""The date and time that the application was launched.
+
+
.. versionadded:: 0.0.1
+
"""
+
return self.xa_elem.launchDate()
+
+
@property
+
def localized_name(self) -> str:
+
"""The application's name.
+
+
.. versionadded:: 0.0.1
+
"""
+
return self.xa_elem.localizedName()
+
+
@property
+
def owns_menu_bar(self) -> bool:
+
"""Whether the application owns the top menu bar.
+
+
.. versionadded:: 0.0.1
+
"""
+
return self.xa_elem.ownsMenuBar()
+
+
@property
+
def process_identifier(self) -> str:
+
"""The process identifier for the application instance.
+
+
.. versionadded:: 0.0.1
+
"""
+
return self.xa_elem.processIdentifier()
+
+
@property
+
def icon(self) -> 'XAImage':
+
"""The application's icon.
+
+
.. versionadded:: 0.1.1
+
"""
+
return XAImage(self.xa_elem.icon())
+
+
[docs] def launch(self) -> Self:
+
"""Launches the application.
+
+
:return: The application object.
+
:rtype: Self
+
+
.. versionadded:: 0.1.1
+
"""
+
config = AppKit.NSWorkspaceOpenConfiguration.alloc().init()
+
config.setActivates_(False)
+
config.setHides_(True)
+
+
finished_launching = False
+
def test(app, error):
+
nonlocal finished_launching
+
finished_launching = True
+
+
self.xa_wksp.openApplicationAtURL_configuration_completionHandler_(self.bundle_url, config, test)
+
+
while not finished_launching:
+
time.sleep(0.05)
+
+
return self
+
+
[docs] def activate(self) -> 'XAApplication':
+
"""Activates the application, bringing its window(s) to the front and launching the application beforehand if necessary.
+
+
:return: A reference to the PyXA application object.
+
:rtype: XAApplication
+
+
.. seealso:: :func:`terminate`, :func:`unhide`, :func:`focus`
+
+
.. versionadded:: 0.0.1
+
"""
+
if not self.xa_elem.isFinishedLaunching():
+
self.launch()
+
+
if self.xa_elem.isHidden():
+
self.unhide()
+
+
self.xa_elem.activateWithOptions_(AppKit.NSApplicationActivateAllWindows | AppKit.NSApplicationActivateIgnoringOtherApps)
+
return self
+
+
[docs] def terminate(self) -> 'XAApplication':
+
"""Quits the application. Synonymous with quit().
+
+
:return: A reference to the PyXA application object.
+
:rtype: XAApplication
+
+
:Example:
+
+
>>> import PyXA
+
>>> safari = PyXA.Application("Safari")
+
>>> safari.terminate()
+
+
.. seealso:: :func:`quit`, :func:`activate`
+
+
.. versionadded:: 0.0.1
+
"""
+
self.xa_elem.terminate()
+
return self
+
+
[docs] def quit(self) -> 'XAApplication':
+
"""Quits the application. Synonymous with terminate().
+
+
:return: A reference to the PyXA application object.
+
:rtype: XAApplication
+
+
:Example:
+
+
>>> import PyXA
+
>>> safari = PyXA.Application("Safari")
+
>>> safari.quit()
+
+
.. seealso:: :func:`terminate`, :func:`activate`
+
+
.. versionadded:: 0.0.1
+
"""
+
self.xa_elem.terminate()
+
return self
+
+
[docs] def hide(self) -> 'XAApplication':
+
"""Hides all windows of the application.
+
+
:return: A reference to the PyXA application object.
+
:rtype: XAApplication
+
+
:Example:
+
+
>>> import PyXA
+
>>> safari = PyXA.Application("Safari")
+
>>> safari.hide()
+
+
.. seealso:: :func:`unhide`
+
+
.. versionadded:: 0.0.1
+
"""
+
self.xa_elem.hide()
+
return self
+
+
[docs] def unhide(self) -> 'XAApplication':
+
"""Unhides (reveals) all windows of the application, but does not does not activate them.
+
+
:return: A reference to the PyXA application object.
+
:rtype: XAApplication
+
+
:Example:
+
+
>>> import PyXA
+
>>> safari = PyXA.Application("Safari")
+
>>> safari.unhide()
+
+
.. seealso:: :func:`hide`
+
+
.. versionadded:: 0.0.1
+
"""
+
self.xa_elem.unhide()
+
return self
+
+
[docs] def focus(self) -> 'XAApplication':
+
"""Hides the windows of all applications except this one.
+
+
:return: A reference to the PyXA application object.
+
:rtype: XAApplication
+
+
:Example:
+
+
>>> import PyXA
+
>>> safari = PyXA.Application("Safari")
+
>>> safari.focus()
+
+
.. seealso:: :func:`unfocus`
+
+
.. versionadded:: 0.0.1
+
"""
+
for app in self.xa_wksp.runningApplications():
+
if app.localizedName() != self.xa_elem.localizedName():
+
app.hide()
+
else:
+
app.unhide()
+
return self
+
+
[docs] def unfocus(self) -> 'XAApplication':
+
"""Unhides (reveals) the windows of all other applications, but does not activate them.
+
+
:return: A reference to the PyXA application object.
+
:rtype: XAApplication
+
+
:Example:
+
+
>>> import PyXA
+
>>> safari = PyXA.Application("Safari")
+
>>> safari.unfocus()
+
+
.. seealso:: :func:`focus`
+
+
.. versionadded:: 0.0.1
+
"""
+
for app in self.xa_wksp.runningApplications():
+
app.unhide()
+
return self
+
+
def _get_processes(self, processes):
+
for process in self.xa_sevt.processes():
+
processes.append(process)
+
+
[docs] def windows(self, filter: dict = None) -> XAList:
+
return self.xa_prcs.windows(filter)
+
+
@property
+
def front_window(self) -> XAObject:
+
"""The frontmost window of the application.
+
"""
+
return self.xa_prcs.front_window
+
+
+
+
[docs] def get_clipboard_representation(self) -> list[Union[str, 'AppKit.NSURL', 'AppKit.NSImage']]:
+
"""Gets a clipboard-codable representation of the application.
+
+
When the clipboard content is set to an application, three items are placed on the clipboard:
+
1. The application's name
+
2. The URL to the application bundle
+
3. The application icon
+
+
After copying an application to the clipboard, pasting will have the following effects:
+
- In Finder: Paste a copy of the application bundle in the current directory
+
- In Terminal: Paste the name of the application followed by the path to the application
+
- In iWork: Paste the application name
+
- In Safari: Paste the application name
+
- In Notes: Attach a copy of the application bundle to the active note
+
The pasted content may be different for other applications.
+
+
:return: The clipboard-codable representation
+
:rtype: list[Union[str, AppKit.NSURL, AppKit.NSImage]]
+
+
.. versionadded:: 0.0.8
+
"""
+
return [self.xa_elem.localizedName(), self.xa_elem.bundleURL(), self.xa_elem.icon()]
+
+
[docs] def __getattr__(self, attr):
+
attributes = [x for y in [cls.__dict__.keys() for cls in self.__class__.__mro__ if cls.__name__ != "object"] for x in y]
+
if attr in attributes:
+
# If possible, use PyXA attribute
+
return super().__getattribute__(attr)
+
else:
+
# Otherwise, fall back to appscript
+
return getattr(self.xa_apsc, attr)
+
+
+
+
+######################
+### PyXA Utilities ###
+######################
+[docs]class AppleScript():
+
"""A class for constructing and executing AppleScript scripts.
+
+
.. versionadded:: 0.0.5
+
"""
+
[docs] def __init__(self, script: Union[str, list[str], None] = None):
+
"""Creates a new AppleScript object.
+
+
:param script: A string or list of strings representing lines of AppleScript code, or the path to a script plaintext file, defaults to None
+
:type script: Union[str, list[str], None], optional
+
+
.. versionadded:: 0.0.5
+
"""
+
self.script: list[str] #: The lines of AppleScript code contained in the script
+
self.last_result: Any #: The return value of the last execution of the script
+
self.file_path: XAPath #: The file path of this script, if one exists
+
+
if isinstance(script, str):
+
if script.startswith("/"):
+
with open(script, 'r') as f:
+
script = f.readlines()
+
else:
+
self.script = [script]
+
elif isinstance(script, list):
+
self.script = script
+
elif script == None:
+
self.script = []
+
+
@property
+
def last_result(self) -> Any:
+
return self.__last_result
+
+
@property
+
def file_path(self) -> 'XAPath':
+
return self.__file_path
+
+
[docs] def add(self, script: Union[str, list[str], 'AppleScript']):
+
"""Adds the supplied string, list of strings, or script as a new line entry in the script.
+
+
:param script: The script to append to the current script string.
+
:type script: Union[str, list[str], AppleScript]
+
+
:Example:
+
+
>>> import PyXA
+
>>> script = PyXA.AppleScript("tell application \"Safari\"")
+
>>> script.add("print the document of window 1")
+
>>> script.add("end tell")
+
>>> script.run()
+
+
.. versionadded:: 0.0.5
+
"""
+
if isinstance(script, str):
+
self.script.append(script)
+
elif isinstance(script, list):
+
self.script.extend(script)
+
elif isinstance(script, AppleScript):
+
self.script.extend(script.script)
+
+
[docs] def insert(self, index: int, script: Union[str, list[str], 'AppleScript']):
+
"""Inserts the supplied string, list of strings, or script as a line entry in the script starting at the given line index.
+
+
:param index: The line index to begin insertion at
+
:type index: int
+
:param script: The script to insert into the current script
+
:type script: Union[str, list[str], AppleScript]
+
+
:Example:
+
+
>>> import PyXA
+
>>> script = PyXA.AppleScript.load("/Users/exampleUser/Downloads/Test.scpt")
+
>>> script.insert(1, "activate")
+
>>> script.run()
+
+
.. versionadded:: 0.0.9
+
"""
+
if isinstance(script, str):
+
self.script.insert(index, script)
+
elif isinstance(script, list):
+
for line in script:
+
self.script.insert(index, line)
+
index += 1
+
elif isinstance(script, AppleScript):
+
for line in script.script:
+
self.script.insert(index, line)
+
index += 1
+
+
[docs] def pop(self, index: int = -1) -> str:
+
"""Removes the line at the given index from the script.
+
+
:param index: The index of the line to remove
+
:type index: int
+
:return: The text of the removed line
+
:rtype: str
+
+
:Example:
+
+
>>> import PyXA
+
>>> script = PyXA.AppleScript.load("/Users/exampleUser/Downloads/Test.scpt")
+
>>> print(script.pop(1))
+
get chats
+
+
.. versionadded:: 0.0.9
+
"""
+
return self.script.pop(index)
+
+
[docs] def load(path: Union['XAPath', str]) -> 'AppleScript':
+
"""Loads an AppleScript (.scpt) file as a runnable AppleScript object.
+
+
:param path: The path of the .scpt file to load
+
:type path: Union[XAPath, str]
+
:return: The newly loaded AppleScript object
+
:rtype: AppleScript
+
+
:Example 1: Load and run a script
+
+
>>> import PyXA
+
>>> script = PyXA.AppleScript.load("/Users/exampleUser/Downloads/Test.scpt")
+
>>> print(script.run())
+
{
+
'string': None,
+
'int': 0,
+
'bool': False,
+
'float': 0.0,
+
'date': None,
+
'file_url': None,
+
'type_code': 845507684,
+
'data': {length = 8962, bytes = 0x646c6532 00000000 6c697374 000022f2 ... 6e756c6c 00000000 },
+
'event': <NSAppleEventDescriptor: [ 'obj '{ ... } ]>
+
}
+
+
:Example 2: Load, modify, and run a script
+
+
>>> import PyXA
+
>>> script = PyXA.AppleScript.load("/Users/exampleUser/Downloads/Test.scpt")
+
>>> script.pop(1)
+
>>> script.insert(1, "activate")
+
>>> script.run()
+
+
.. versionadded:: 0.0.8
+
"""
+
if isinstance(path, str):
+
path = XAPath(path)
+
script = AppKit.NSAppleScript.alloc().initWithContentsOfURL_error_(path.xa_elem, None)[0]
+
+
attributed_string = script.richTextSource()
+
attributed_string = str(attributed_string).split("}")
+
parts = []
+
for x in attributed_string:
+
parts.extend(x.split("{"))
+
+
for x in parts:
+
if "=" in x:
+
parts.remove(x)
+
+
script = AppleScript("".join(parts).split("\n"))
+
script.__file_path = path
+
return script
+
+
[docs] def save(self, path: Union['XAPath', str, None] = None):
+
"""Saves the script to the specified file path, or to the path from which the script was loaded.
+
+
:param path: The path to save the script at, defaults to None
+
:type path: Union[XAPath, str, None], optional
+
+
:Example 1: Save the script to a specified path
+
+
>>> import PyXA
+
>>> script = PyXA.AppleScript(f\"\"\"
+
>>> tell application "Safari"
+
>>> activate
+
>>> end tell
+
>>> \"\"\")
+
>>> script.save("/Users/exampleUser/Downloads/Example.scpt")
+
+
:Example 2: Load a script, modify it, then save it
+
+
>>> import PyXA
+
>>> script = PyXA.AppleScript.load("/Users/steven/Downloads/Example.scpt")
+
>>> script.insert(2, "delay 2")
+
>>> script.insert(3, "set the miniaturized of window 1 to true")
+
>>> script.save()
+
+
.. versionadded:: 0.0.9
+
"""
+
if path is None and self.file_path is None:
+
print("No path to save script to!")
+
return
+
+
if isinstance(path, str):
+
path = XAPath(path)
+
+
script = ""
+
for line in self.script:
+
script += line + "\n"
+
script = AppKit.NSAppleScript.alloc().initWithSource_(script)
+
script.compileAndReturnError_(None)
+
source = (script.richTextSource().string())
+
+
if path is not None:
+
self.__file_path = path
+
+
with open(self.file_path.xa_elem.path(), "w") as f:
+
f.write(source)
+
+
[docs] def parse_result_data(result: dict) -> list[tuple[str, str]]:
+
"""Extracts string data from an AppleScript execution result dictionary.
+
+
:param result: The execution result dictionary to extract data from
+
:type result: dict
+
:return: A list of responses contained in the result structured as tuples
+
:rtype: list[tuple[str, str]]
+
+
:Example:
+
+
>>> import PyXA
+
>>> script = PyXA.AppleScript.load("/Users/exampleUser/Downloads/Test.scpt")
+
>>> print(script.script)
+
>>> result = script.run()
+
>>> print(PyXA.AppleScript.parse_result_data(result))
+
['tell application "Messages"', '\\tget chats', 'end tell']
+
[('ID', 'iMessage;-;+12345678910'), ('ID', 'iMessage;-;+12345678911'), ('ID', 'iMessage;-;example@icloud.com'), ...]
+
+
.. versionadded:: 0.0.9
+
"""
+
result = result["event"]
+
response_objects = []
+
num_responses = result.numberOfItems()
+
for response_index in range(1, num_responses + 1):
+
response = result.descriptorAtIndex_(response_index)
+
+
data = ()
+
num_params = response.numberOfItems()
+
if num_params == 0:
+
data = response.stringValue().strip()
+
+
else:
+
for param_index in range(1, num_params + 1):
+
param = response.descriptorAtIndex_(param_index).stringValue()
+
if param is not None:
+
data += (param.strip(), )
+
response_objects.append(data)
+
+
return response_objects
+
+
[docs] def run(self) -> Any:
+
"""Compiles and runs the script, returning the result.
+
+
:return: The return value of the script.
+
:rtype: Any
+
+
:Example:
+
+
>>> import PyXA
+
>>> script = PyXA.AppleScript(f\"\"\"tell application "System Events"
+
>>> return 1 + 2
+
>>> end tell
+
>>> \"\"\")
+
>>> print(script.run())
+
{
+
'string': '3',
+
'int': 3,
+
'bool': False,
+
'float': 3.0,
+
'date': None,
+
'file_url': None,
+
'type_code': 3,
+
'data': {length = 4, bytes = 0x03000000},
+
'event': <NSAppleEventDescriptor: 3>
+
}
+
+
.. versionadded:: 0.0.5
+
"""
+
script = ""
+
for line in self.script:
+
script += line + "\n"
+
script = AppKit.NSAppleScript.alloc().initWithSource_(script)
+
+
result = script.executeAndReturnError_(None)[0]
+
if result is not None:
+
self.__last_result = {
+
"string": result.stringValue(),
+
"int": result.int32Value(),
+
"bool": result.booleanValue(),
+
"float": result.doubleValue(),
+
"date": result.dateValue(),
+
"file_url": result.fileURLValue(),
+
"type_code": result.typeCodeValue(),
+
"data": result.data(),
+
"event": result,
+
}
+
return self.last_result
+
+
[docs] def __repr__(self):
+
return "<" + str(type(self)) + str(self.script) + ">"
+
+
+
+
+[docs]class XAPredicate(XAObject, XAClipboardCodable):
+
"""A predicate used to filter arrays.
+
+
.. versionadded:: 0.0.4
+
"""
+
[docs] def __init__(self):
+
self.keys: list[str] = []
+
self.operators: list[str] = []
+
self.values: list[str] = []
+
+
[docs] def from_dict(self, ref_dict: dict) -> 'XAPredicate':
+
"""Populates the XAPredicate object from the supplied dictionary.
+
+
The predicate will use == for all comparisons.
+
+
:param ref_dict: A specification of key, value pairs
+
:type ref_dict: dict
+
:return: The populated predicate object
+
:rtype: XAPredicate
+
+
.. versionadded:: 0.0.4
+
"""
+
for key, value in ref_dict.items():
+
self.keys.append(key)
+
self.operators.append("==")
+
self.values.append(value)
+
return self
+
+
[docs] def from_args(self, *args) -> 'XAPredicate':
+
"""Populates the XAPredicate object from the supplied key, value argument pairs.
+
+
The number of keys and values must be equal. The predicate will use == for all comparisons.
+
+
:raises InvalidPredicateError: Raised when the number of keys does not match the number of values
+
:return: The populated predicate object
+
:rtype: XAPredicate
+
+
.. versionadded:: 0.0.4
+
"""
+
arg_num = len(args)
+
if arg_num % 2 != 0:
+
raise InvalidPredicateError("The number of keys and values must be equal; the number of arguments must be an even number.")
+
+
for index, value in enumerate(args):
+
if index % 2 == 0:
+
self.keys.append(value)
+
self.operators.append("==")
+
self.values.append(args[index + 1])
+
return self
+
+
[docs] def evaluate(self, target: Union['AppKit.NSArray', XAList]) -> 'AppKit.NSArray':
+
"""Evaluates the predicate on the given array.
+
+
:param target: The array to evaluate against the predicate
+
:type target: AppKit.NSArray
+
:return: The filtered array
+
:rtype: AppKit.NSArray
+
+
.. versionadded:: 0.0.4
+
"""
+
target_list = target
+
if isinstance(target, XAList):
+
target_list = target.xa_elem
+
+
placeholders = ["%@"] * len(self.values)
+
expressions = [" ".join(expr) for expr in zip(self.keys, self.operators, placeholders)]
+
format = "( " + " ) && ( ".join(expressions) + " )"
+
+
ls = []
+
predicate = AppKit.NSPredicate.predicateWithFormat_(format, *self.values)
+
try:
+
# Not sure why this is necessary sometimes, but it is.
+
ls = target_list.filteredArrayUsingPredicate_(AppKit.NSPredicate.predicateWithFormat_(str(predicate)))
+
except ValueError:
+
ls = target_list.filteredArrayUsingPredicate_(predicate)
+
+
if isinstance(target, XAList):
+
return target.__class__({
+
"parent": target,
+
"element": ls,
+
"appref": self.xa_aref,
+
})
+
return ls
+
+
+
+
[docs] def evaluate_with_dict(target: Union['AppKit.NSArray', XAList], properties_dict: dict) -> 'AppKit.NSArray':
+
"""Evaluates the specified array against a predicate constructed from the supplied dictionary.
+
+
The predicate will use == for all comparisons.
+
+
:param target: The array to filter
+
:type target: AppKit.NSArray
+
:param properties_dict: The specification of key, value pairs
+
:type properties_dict: dict
+
:return: The filtered array
+
:rtype: AppKit.NSArray
+
+
.. versionadded:: 0.0.4
+
"""
+
target_list = target
+
if isinstance(target, XAList):
+
target_list = target.xa_elem
+
+
fmt = ""
+
for key, value in properties_dict.items():
+
if isinstance(value, str):
+
value = "'" + value + "'"
+
fmt += f"( {key} == {value} ) &&"
+
+
predicate = AppKit.NSPredicate.predicateWithFormat_(fmt[:-3])
+
ls = target_list.filteredArrayUsingPredicate_(predicate)
+
+
if isinstance(target, XAList):
+
return target.__class__({
+
"parent": target,
+
"element": ls,
+
"appref": AppKit.NSApplication.sharedApplication(),
+
})
+
return ls
+
+
# EQUAL
+
[docs] def add_eq_condition(self, property: str, value: Any):
+
"""Appends an `==` condition to the end of the predicate format.
+
+
The added condition will have the form `property == value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.append(property)
+
self.operators.append("==")
+
self.values.append(value)
+
+
[docs] def insert_eq_condition(self, index: int, property: str, value: Any):
+
"""Inserts an `==` condition to the predicate format at the desired location, specified by index.
+
+
The added condition will have the form `property == value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.insert(index, property)
+
self.operators.insert(index, "==")
+
self.values.insert(index, value)
+
+
# NOT EQUAL
+
[docs] def add_neq_condition(self, property: str, value: Any):
+
"""Appends a `!=` condition to the end of the predicate format.
+
+
The added condition will have the form `property != value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.append(property)
+
self.operators.append("!=")
+
self.values.append(value)
+
+
[docs] def insert_neq_condition(self, index: int, property: str, value: Any):
+
"""Inserts a `!=` condition to the predicate format at the desired location, specified by index.
+
+
The added condition will have the form `property != value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.insert(index, property)
+
self.operators.insert(index, "!=")
+
self.values.insert(index, value)
+
+
# GREATER THAN OR EQUAL
+
[docs] def add_geq_condition(self, property: str, value: Any):
+
"""Appends a `>=` condition to the end of the predicate format.
+
+
The added condition will have the form `property >= value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.append(property)
+
self.operators.append(">=")
+
self.values.append(value)
+
+
[docs] def insert_geq_condition(self, index: int, property: str, value: Any):
+
"""Inserts a `>=` condition to the predicate format at the desired location, specified by index.
+
+
The added condition will have the form `property >= value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.insert(index, property)
+
self.operators.insert(index, ">=")
+
self.values.insert(index, value)
+
+
# LESS THAN OR EQUAL
+
[docs] def add_leq_condition(self, property: str, value: Any):
+
"""Appends a `<=` condition to the end of the predicate format.
+
+
The added condition will have the form `property <= value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.append(property)
+
self.operators.append("<=")
+
self.values.append(value)
+
+
[docs] def insert_leq_condition(self, index: int, property: str, value: Any):
+
"""Inserts a `<=` condition to the predicate format at the desired location, specified by index.
+
+
The added condition will have the form `property <= value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.insert(index, property)
+
self.operators.insert(index, "<=")
+
self.values.insert(index, value)
+
+
# GREATER THAN
+
[docs] def add_gt_condition(self, property: str, value: Any):
+
"""Appends a `>` condition to the end of the predicate format.
+
+
The added condition will have the form `property > value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.append(property)
+
self.operators.append(">")
+
self.values.append(value)
+
+
[docs] def insert_gt_condition(self, index: int, property: str, value: Any):
+
"""Inserts a `>` condition to the predicate format at the desired location, specified by index.
+
+
The added condition will have the form `property > value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.insert(index, property)
+
self.operators.insert(index, ">")
+
self.values.insert(index, value)
+
+
# LESS THAN
+
[docs] def add_lt_condition(self, property: str, value: Any):
+
"""Appends a `<` condition to the end of the predicate format.
+
+
The added condition will have the form `property < value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.append(property)
+
self.operators.append("<")
+
self.values.append(value)
+
+
[docs] def insert_lt_condition(self, index: int, property: str, value: Any):
+
"""Inserts a `<` condition to the predicate format at the desired location, specified by index.
+
+
The added condition will have the form `property < value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.insert(index, property)
+
self.operators.insert(index, "<")
+
self.values.insert(index, value)
+
+
# BETWEEN
+
[docs] def add_between_condition(self, property: str, value1: Union[int, float], value2: Union[int, float]):
+
"""Appends a `BETWEEN` condition to the end of the predicate format.
+
+
The added condition will have the form `property BETWEEN [value1, value2]`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value1: The lower target value of the condition
+
:type value1: Union[int, float]
+
:param value2: The upper target value of the condition
+
:type value2: Union[int, float]
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.append(property)
+
self.operators.append("BETWEEN")
+
self.values.append([value1, value2])
+
+
[docs] def insert_between_condition(self, index: int, property: str, value1: Union[int, float], value2: Union[int, float]):
+
"""Inserts a `BETWEEN` condition to the predicate format at the desired location, specified by index.
+
+
The added condition will have the form `property BETWEEN [value1, value2]`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value1: The lower target value of the condition
+
:type value1: Union[int, float]
+
:param value2: The upper target value of the condition
+
:type valu2e: Union[int, float]
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.insert(index, property)
+
self.operators.insert(index, "BETWEEN")
+
self.values.insert(index, [value1, value2])
+
+
# BEGINSWITH
+
[docs] def add_begins_with_condition(self, property: str, value: Any):
+
"""Appends a `BEGINSWITH` condition to the end of the predicate format.
+
+
The added condition will have the form `property BEGINSWITH value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.append(property)
+
self.operators.append("BEGINSWITH")
+
self.values.append(value)
+
+
[docs] def insert_begins_with_condition(self, index: int, property: str, value: Any):
+
"""Inserts a `BEGINSWITH` condition to the predicate format at the desired location, specified by index.
+
+
The added condition will have the form `property BEGINSWITH value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.insert(index, property)
+
self.operators.insert(index, "BEGINSWITH")
+
self.values.insert(index, value)
+
+
# ENDSWITH
+
[docs] def add_ends_with_condition(self, property: str, value: Any):
+
"""Appends a `ENDSWITH` condition to the end of the predicate format.
+
+
The added condition will have the form `property ENDSWITH value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.append(property)
+
self.operators.append("ENDSWITH")
+
self.values.append(value)
+
+
[docs] def insert_ends_with_condition(self, index: int, property: str, value: Any):
+
"""Inserts a `ENDSWITH` condition to the predicate format at the desired location, specified by index.
+
+
The added condition will have the form `property ENDSWITH value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.insert(index, property)
+
self.operators.insert(index, "ENDSWITH")
+
self.values.insert(index, value)
+
+
# CONTAINS
+
[docs] def add_contains_condition(self, property: str, value: Any):
+
"""Appends a `CONTAINS` condition to the end of the predicate format.
+
+
The added condition will have the form `property CONTAINS value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.append(property)
+
self.operators.append("CONTAINS")
+
self.values.append(value)
+
+
[docs] def insert_contains_condition(self, index: int, property: str, value: Any):
+
"""Inserts a `CONTAINS` condition to the predicate format at the desired location, specified by index.
+
+
The added condition will have the form `property CONTAINS value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.insert(index, property)
+
self.operators.insert(index, "CONTAINS")
+
self.values.insert(index, value)
+
+
# MATCHES
+
[docs] def add_match_condition(self, property: str, value: Any):
+
"""Appends a `MATCHES` condition to the end of the predicate format.
+
+
The added condition will have the form `property MATCHES value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.append(property)
+
self.operators.append("MATCHES")
+
self.values.append(value)
+
+
[docs] def insert_match_condition(self, index: int, property: str, value: Any):
+
"""Inserts a `MATCHES` condition to the predicate format at the desired location, specified by index.
+
+
The added condition will have the form `property MATCHES value`.
+
+
:param property: A property of an object to check the condition against
+
:type property: str
+
:param value: The target value of the condition
+
:type value: Any
+
+
.. versionadded:: 0.0.4
+
"""
+
self.keys.insert(index, property)
+
self.operators.insert(index, "MATCHES")
+
self.values.insert(index, value)
+
+
[docs] def get_clipboard_representation(self) -> str:
+
"""Gets a clipboard-codable representation of the predicate.
+
+
When a predicate is copied to the clipboard, the string representation of the predicate is added to the clipboard.
+
+
:return: The string representation of the predicate
+
:rtype: str
+
+
.. versionadded:: 0.0.8
+
"""
+
placeholders = ["%@"] * len(self.values)
+
expressions = [" ".join(expr) for expr in zip(self.keys, self.operators, placeholders)]
+
format = "( " + " ) && ( ".join(expressions) + " )"
+
predicate = AppKit.NSPredicate.predicateWithFormat_(format, *self.values)
+
return predicate.predicateFormat()
+
+
+
+
+[docs]class XAURLList(XAList):
+
"""A list of URLs. Supports bulk operations.
+
+
.. versionadded:: 0.1.2
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, XAURL, filter)
+
+
[docs] def base_url(self) -> list[str]:
+
return [url.base_url for url in self]
+
+
[docs] def parameters(self) -> list[str]:
+
return [url.parameters for url in self]
+
+
[docs] def scheme(self) -> list[str]:
+
return [url.scheme for url in self]
+
+
[docs] def fragment(self) -> list[str]:
+
return [url.fragment for url in self]
+
+
[docs] def port(self) -> list[int]:
+
return [url.port for url in self]
+
+
[docs] def html(self) -> list[element.Tag]:
+
return [url.html for url in self]
+
+
[docs] def title(self) -> list[str]:
+
return [url.title for url in self]
+
+
[docs] def open(self):
+
"""Opens each URL in the list.
+
+
.. versionadded:: 0.1.2
+
"""
+
for url in self:
+
url.open()
+
+
+
+
+
+[docs]class XAURL(XAObject, XAClipboardCodable):
+
"""A URL using any scheme recognized by the system. This can be a file URL.
+
+
.. versionadded:: 0.0.5
+
"""
+
[docs] def __init__(self, url: Union[str, 'AppKit.NSURL', 'XAURL', 'XAPath']):
+
super().__init__()
+
self.soup: BeautifulSoup = None #: The bs4 object for the URL, starts as None until a bs4-related action is made
+
self.url: str #: The string form of the URL
+
+
if isinstance(url, list):
+
# Elevate to XAURLList
+
new_self = XAURLList({"element": AppKit.NSArray.alloc().initWithArray_(url)})
+
self.__dict__ = new_self.__dict__
+
self.__class__ = new_self.__class__
+
self = new_self
+
return
+
+
if isinstance(url, dict):
+
# Initialized via XAURLList
+
url = url["element"]
+
+
if isinstance(url, str):
+
# URL-encode spaces
+
url = url.replace(" ", "%20")
+
+
if url.startswith("/"):
+
# Prepend file scheme
+
url = "file://" + url
+
elif url.replace(".", "").isdecimal():
+
# URL is an IP -- must add http:// prefix
+
if ":" not in url:
+
# No port provided, add port 80 by default
+
url = "http://" + url + ":80"
+
else:
+
url = "http://" + url
+
elif "://" not in url:
+
# URL is not currently valid, try prepending http://
+
url = "http://" + url
+
+
self.url = url
+
url = AppKit.NSURL.alloc().initWithString_(url)
+
elif isinstance(url, XAURL) or isinstance(url, XAPath):
+
self.url = url.url
+
url = url.xa_elem
+
else:
+
self.url = str(url)
+
+
self.xa_elem = url
+
+
@property
+
def base_url(self) -> str:
+
return self.xa_elem.host()
+
+
@property
+
def parameters(self) -> str:
+
"""The query parameters of the URL.
+
"""
+
return self.xa_elem.query()
+
+
@property
+
def scheme(self) -> str:
+
"""The URI scheme of the URL.
+
"""
+
return self.xa_elem.scheme()
+
+
@property
+
def fragment(self) -> str:
+
"""The fragment identifier following a # symbol in the URL.
+
"""
+
return self.xa_elem.fragment()
+
+
@property
+
def port(self) -> int:
+
"""The port that the URL points to.
+
"""
+
return self.xa_elem.port()
+
+
@property
+
def html(self) -> element.Tag:
+
"""The html of the URL.
+
"""
+
if self.soup is None:
+
self.__get_soup()
+
return self.soup.html
+
+
@property
+
def title(self) -> str:
+
"""The title of the URL.
+
"""
+
if self.soup is None:
+
self.__get_soup()
+
return self.soup.title.text
+
+
def __get_soup(self):
+
req = requests.get(str(self.xa_elem))
+
self.soup = BeautifulSoup(req.text, "html.parser")
+
+
[docs] def open(self):
+
"""Opens the URL in the appropriate default application.
+
+
.. versionadded:: 0.0.5
+
"""
+
global workspace
+
if workspace is None:
+
workspace = AppKit.NSWorkspace.sharedWorkspace()
+
workspace.openURL_(self.xa_elem)
+
+
+
+
+
+
[docs] def get_clipboard_representation(self) -> list[Union['AppKit.NSURL', str]]:
+
"""Gets a clipboard-codable representation of the URL.
+
+
When the clipboard content is set to a URL, the raw URL data and the string representation of the URL are added to the clipboard.
+
+
:return: The clipboard-codable form of the URL
+
:rtype: Any
+
+
.. versionadded:: 0.0.8
+
"""
+
return [self.xa_elem, str(self.xa_elem)]
+
+
[docs] def __eq__(self, other: 'XAURL'):
+
if not isinstance(other, XAURL):
+
return False
+
+
if self.xa_elem == other.xa_elem:
+
return True
+
+
return self.url == other.url
+
+
[docs] def __str__(self):
+
return str(self.xa_elem)
+
+
[docs] def __repr__(self):
+
return "<" + str(type(self)) + str(self.xa_elem) + ">"
+
+
+
+
+[docs]class XAPath(XAObject, XAClipboardCodable):
+
"""A path to a file on the disk.
+
+
.. versionadded:: 0.0.5
+
"""
+
[docs] def __init__(self, path: Union[str, 'AppKit.NSURL']):
+
super().__init__()
+
if isinstance(path, str):
+
path = AppKit.NSURL.alloc().initFileURLWithPath_(path)
+
self.xa_elem = path
+
self.path = path.path() #: The path string without the file:// prefix
+
self.url = str(self.xa_elem) #: The path string with the file:// prefix included
+
+
[docs] def open(self):
+
"""Opens the file in its default application.
+
+
.. versionadded: 0.0.5
+
"""
+
global workspace
+
if workspace is None:
+
workspace = AppKit.NSWorkspace.sharedWorkspace()
+
workspace.openURL_(self.xa_elem)
+
+
[docs] def show_in_finder(self):
+
"""Opens a Finder window showing the folder containing this path, with the associated file selected. Synonymous with :func:`select`.
+
+
.. versionadded: 0.0.9
+
"""
+
self.select()
+
+
[docs] def select(self):
+
"""Opens a Finder window showing the folder containing this path, with the associated file selected. Synonymous with :func:`show_in_finder`.
+
+
.. versionadded: 0.0.5
+
"""
+
global workspace
+
if workspace is None:
+
workspace = AppKit.NSWorkspace.sharedWorkspace()
+
workspace.activateFileViewerSelectingURLs_([self.xa_elem])
+
+
[docs] def get_clipboard_representation(self) -> list[Union['AppKit.NSURL', str]]:
+
"""Gets a clipboard-codable representation of the path.
+
+
When the clipboard content is set to a path, the raw file URL data and the string representation of the path are added to the clipboard.
+
+
:return: The clipboard-codable form of the path
+
:rtype: Any
+
+
.. versionadded:: 0.0.8
+
"""
+
return [self.xa_elem, self.xa_elem.path()]
+
+
[docs] def __eq__(self, other: 'XAPath'):
+
if not isinstance(other, XAPath):
+
return False
+
+
if self.xa_elem == other.xa_elem:
+
return True
+
+
return self.path == other.path
+
+
[docs] def __repr__(self):
+
return "<" + str(type(self)) + str(self.xa_elem) + ">"
+
+
+
+########################
+### System Utilities ###
+########################
+[docs]class XAClipboard(XAObject):
+
"""A wrapper class for managing and interacting with the system clipboard.
+
+
.. versionadded:: 0.0.5
+
"""
+
[docs] def __init__(self):
+
self.xa_elem = AppKit.NSPasteboard.generalPasteboard()
+
+
@property
+
def content(self) -> dict[str, list[Any]]:
+
"""The content of the clipboard.
+
"""
+
info_by_type = {}
+
for item in self.xa_elem.pasteboardItems():
+
for item_type in item.types():
+
info_by_type[item_type] = {
+
"data": item.dataForType_(item_type),
+
"properties": item.propertyListForType_(item_type),
+
"strings": item.stringForType_(item_type),
+
}
+
return info_by_type
+
+
@content.setter
+
def content(self, value: list[Any]):
+
if not isinstance(value, list):
+
value = [value]
+
self.xa_elem.clearContents()
+
for index, item in enumerate(value):
+
if item == None:
+
value[index] = ""
+
elif isinstance(item, XAObject):
+
if not isinstance(item, XAClipboardCodable):
+
print(item, "is not a clipboard-codable object.")
+
continue
+
if isinstance(item.xa_elem, ScriptingBridge.SBElementArray) and item.xa_elem.get() is None:
+
value[index] = ""
+
else:
+
content = item.get_clipboard_representation()
+
if isinstance(content, list):
+
value.pop(index)
+
value += content
+
else:
+
value[index] = content
+
elif isinstance(item, int) or isinstance(item, float):
+
value[index] = str(item)
+
self.xa_elem.writeObjects_(value)
+
+
[docs] def clear(self):
+
"""Clears the system clipboard.
+
+
.. versionadded:: 0.0.5
+
"""
+
self.xa_elem.clearContents()
+
+
[docs] def get_strings(self) -> list[str]:
+
"""Retrieves string type data from the clipboard, if any such data exists.
+
+
:return: The list of strings currently copied to the clipboard
+
:rtype: list[str]
+
+
.. versionadded:: 0.0.8
+
"""
+
items = []
+
for item in self.xa_elem.pasteboardItems():
+
string = item.stringForType_(AppKit.NSPasteboardTypeString)
+
if string is not None:
+
items.append(string)
+
return items
+
+
[docs] def get_urls(self) -> list['XAURL']:
+
"""Retrieves URL type data from the clipboard, as instances of :class:`XAURL` and :class:`XAPath`, if any such data exists.
+
+
:return: The list of file URLs and web URLs currently copied to the clipboard
+
:rtype: list[XAURL]
+
+
.. versionadded:: 0.0.8
+
"""
+
items = []
+
for item in self.xa_elem.pasteboardItems():
+
url = None
+
string = item.stringForType_(AppKit.NSPasteboardTypeURL)
+
if string is None:
+
string = item.stringForType_(AppKit.NSPasteboardTypeFileURL)
+
if string is not None:
+
url = XAPath(XAURL(string).xa_elem)
+
else:
+
url = XAURL(string)
+
+
if url is not None:
+
items.append(url)
+
return items
+
+
[docs] def get_images(self) -> list['XAImage']:
+
"""Retrieves image type data from the clipboard, as instances of :class:`XAImage`, if any such data exists.
+
+
:return: The list of images currently copied to the clipboard
+
:rtype: list[XAImage]
+
+
.. versionadded:: 0.0.8
+
"""
+
image_types = [AppKit.NSPasteboardTypePNG, AppKit.NSPasteboardTypeTIFF, 'public.jpeg', 'com.apple.icns']
+
items = []
+
for item in self.xa_elem.pasteboardItems():
+
for image_type in image_types:
+
if image_type in item.types():
+
img = XAImage(data = item.dataForType_(image_type))
+
items.append(img)
+
return items
+
+
[docs] def set_contents(self, content: list[Any]):
+
"""Sets the content of the clipboard
+
+
:param content: A list of the content to add fill the clipboard with.
+
:type content: list[Any]
+
+
.. deprecated:: 0.0.8
+
Set the :attr:`content` property directly instead.
+
+
.. versionadded:: 0.0.5
+
"""
+
self.xa_elem.clearContents()
+
self.xa_elem.writeObjects_(content)
+
+
+
+
+[docs]class XASpotlight(XAObject):
+
"""A Spotlight query for files on the disk.
+
+
.. versionadded:: 0.0.9
+
"""
+
[docs] def __init__(self, *query: list[Any]):
+
self.query: list[Any] = query #: The query terms to search
+
self.timeout: int = 10 #: The amount of time in seconds to timeout the search after
+
self.predicate: Union[str, XAPredicate] = None #: The predicate to filter search results by
+
self.results: list[XAPath] #: The results of the search
+
self.__results = None
+
+
self.query_object = AppKit.NSMetadataQuery.alloc().init()
+
nc = AppKit.NSNotificationCenter.defaultCenter()
+
nc.addObserver_selector_name_object_(self, '_queryNotification:', None, self.query_object)
+
+
@property
+
def results(self) -> list['XAPath']:
+
if len(self.query) == 0 and self.predicate is None:
+
return []
+
self.run()
+
total_time = 0
+
while self.__results is None and total_time < self.timeout:
+
AppKit.NSRunLoop.currentRunLoop().runUntilDate_(datetime.now() + timedelta(seconds = 0.01))
+
total_time += 0.01
+
if self.__results is None:
+
return []
+
return self.__results
+
+
[docs] def run(self):
+
"""Runs the search.
+
+
:Example:
+
+
>>> import PyXA
+
>>> from datetime import date, datetime, time
+
>>> date1 = datetime.combine(date(2022, 5, 17), time(0, 0, 0))
+
>>> date2 = datetime.combine(date(2022, 5, 18), time(0, 0, 0))
+
>>> search = PyXA.XASpotlight(date1, date2)
+
>>> print(search.results)
+
[<<class 'PyXA.XAPath'>file:///Users/exampleUser/Downloads/>, <<class 'PyXA.XAPath'>file:///Users/exampleUser/Downloads/Example.txt>, ...]
+
+
.. versionadded:: 0.0.9
+
"""
+
if self.predicate is not None:
+
# Search with custom predicate
+
if isinstance(self.predicate, XAPredicate):
+
self.predicate = self.predicate.get_clipboard_representation()
+
self.__search_with_predicate(self.predicate)
+
elif len(self.query) == 1 and isinstance(self.query[0], datetime):
+
# Search date + or - 24 hours
+
self.__search_by_date(self.query)
+
elif len(self.query) == 2 and isinstance(self.query[0], datetime) and isinstance(self.query[1], datetime):
+
# Search date range
+
self.__search_by_date_range(self.query[0], self.query[1])
+
elif all(isinstance(x, str) or isinstance(x, int) or isinstance(x, float) for x in self.query):
+
# Search matching multiple strings
+
self.__search_by_strs(self.query)
+
elif isinstance(self.query[0], datetime) and all(isinstance(x, str) or isinstance(x, int) or isinstance(x, float) for x in self.query[1:]):
+
# Search by date and string
+
self.__search_by_date_strings(self.query[0], self.query[1:])
+
elif isinstance(self.query[0], datetime) and isinstance(self.query[1], datetime) and all(isinstance(x, str) or isinstance(x, int) or isinstance(x, float) for x in self.query[2:]):
+
# Search by date range and string
+
self.__search_by_date_range_strings(self.query[0], self.query[1], self.query[2:])
+
+
AppKit.NSRunLoop.currentRunLoop().runUntilDate_(datetime.now() + timedelta(seconds = 0.01))
+
+
[docs] def show_in_finder(self):
+
"""Shows the search in Finder. This might not reveal the same search results.
+
+
.. versionadded:: 0.0.9
+
"""
+
global workspace
+
if workspace is None:
+
workspace = AppKit.NSWorkspace.sharedWorkspace()
+
workspace.showSearchResultsForQueryString_(str(self.query))
+
+
def __search_by_strs(self, terms: tuple[str]):
+
expanded_terms = [x for y in terms for x in [y]*3]
+
format = "((kMDItemDisplayName CONTAINS %@) OR (kMDItemTextContent CONTAINS %@) OR (kMDItemFSName CONTAINS %@)) AND " * len(terms)
+
self.__search_with_predicate(format[:-5], *expanded_terms)
+
+
def __search_by_date(self, date: datetime):
+
self.__search_with_predicate(f"((kMDItemContentCreationDate > %@) AND (kMDItemContentCreationDate < %@)) OR ((kMDItemContentModificationDate > %@) AND (kMDItemContentModificationDate < %@)) OR ((kMDItemFSCreationDate > %@) AND (kMDItemFSCreationDate < %@)) OR ((kMDItemFSContentChangeDate > %@) AND (kMDItemFSContentChangeDate < %@)) OR ((kMDItemDateAdded > %@) AND (kMDItemDateAdded < %@))", *[date - timedelta(hours=12), date + timedelta(hours=12)]*5)
+
+
def __search_by_date_range(self, date1: datetime, date2: datetime):
+
self.__search_with_predicate(f"((kMDItemContentCreationDate > %@) AND (kMDItemContentCreationDate < %@)) OR ((kMDItemContentModificationDate > %@) AND (kMDItemContentModificationDate < %@)) OR ((kMDItemFSCreationDate > %@) AND (kMDItemFSCreationDate < %@)) OR ((kMDItemFSContentChangeDate > %@) AND (kMDItemFSContentChangeDate < %@)) OR ((kMDItemDateAdded > %@) AND (kMDItemDateAdded < %@))", *[date1, date2]*5)
+
+
def __search_by_date_strings(self, date: datetime, terms: tuple[str]):
+
expanded_terms = [x for y in terms for x in [y]*3]
+
format = "((kMDItemDisplayName CONTAINS %@) OR (kMDItemTextContent CONTAINS %@) OR (kMDItemFSName CONTAINS %@)) AND " * len(terms)
+
format += "(((kMDItemContentCreationDate > %@) AND (kMDItemContentCreationDate < %@)) OR ((kMDItemContentModificationDate > %@) AND (kMDItemContentModificationDate < %@)) OR ((kMDItemFSCreationDate > %@) AND (kMDItemFSCreationDate < %@)) OR ((kMDItemFSContentChangeDate > %@) AND (kMDItemFSContentChangeDate < %@)) OR ((kMDItemDateAdded > %@) AND (kMDItemDateAdded < %@)))"
+
self.__search_with_predicate(format, *expanded_terms, *[date - timedelta(hours=12), date + timedelta(hours=12)]*5)
+
+
def __search_by_date_range_strings(self, date1: datetime, date2: datetime, terms: tuple[str]):
+
expanded_terms = [x for y in terms for x in [y]*3]
+
format = "((kMDItemDisplayName CONTAINS %@) OR (kMDItemTextContent CONTAINS %@) OR (kMDItemFSName CONTAINS %@)) AND " * len(terms)
+
format += "(((kMDItemContentCreationDate > %@) AND (kMDItemContentCreationDate < %@)) OR ((kMDItemContentModificationDate > %@) AND (kMDItemContentModificationDate < %@)) OR ((kMDItemFSCreationDate > %@) AND (kMDItemFSCreationDate < %@)) OR ((kMDItemFSContentChangeDate > %@) AND (kMDItemFSContentChangeDate < %@)) OR ((kMDItemDateAdded > %@) AND (kMDItemDateAdded < %@)))"
+
self.__search_with_predicate(format, *expanded_terms, *[date1, date2]*5)
+
+
def __search_with_predicate(self, predicate_format: str, *args: list[Any]):
+
predicate = AppKit.NSPredicate.predicateWithFormat_(predicate_format, *args)
+
self.query_object.setPredicate_(predicate)
+
self.query_object.startQuery()
+
+
def _queryNotification_(self, notification):
+
if notification.name() == AppKit.NSMetadataQueryDidFinishGatheringNotification:
+
self.query_object.stopQuery()
+
results = notification.object().results()
+
self.__results = [XAPath(x.valueForAttribute_(AppKit.NSMetadataItemPathKey)) for x in results]
+
+
+
+
+############
+### Text ###
+############
+[docs]class XATextDocumentList(XAList, XAClipboardCodable):
+
"""A wrapper around lists of text documents that employs fast enumeration techniques.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None, obj_class = None):
+
if obj_class is None:
+
obj_class = XATextDocument
+
super().__init__(properties, obj_class, filter)
+
+
[docs] def properties(self) -> list[dict]:
+
ls = self.xa_elem.arrayByApplyingSelector_("properties") or []
+
return [dict(x) for x in ls]
+
+
[docs] def text(self) -> 'XATextList':
+
ls = self.xa_elem.arrayByApplyingSelector_("text") or []
+
return self._new_element(ls, XATextList)
+
+
[docs] def by_properties(self, properties: dict) -> Union['XATextDocument', None]:
+
return self.by_property("properties", properties)
+
+
[docs] def by_text(self, text: str) -> Union['XATextDocument', None]:
+
return self.by_property("text", text)
+
+
[docs] def paragraphs(self) -> 'XAParagraphList':
+
ls = self.xa_elem.arrayByApplyingSelector_("paragraphs") or []
+
return self._new_element([plist for plist in ls], XAParagraphList)
+
+
[docs] def words(self) -> 'XAWordList':
+
ls = self.xa_elem.arrayByApplyingSelector_("words") or []
+
return [self._new_element([plist for plist in ls], XAWordList)]
+
+
[docs] def characters(self) -> 'XACharacterList':
+
ls = self.xa_elem.arrayByApplyingSelector_("characters") or []
+
return [self._new_element([plist for plist in ls], XACharacterList)]
+
+
[docs] def attribute_runs(self) -> 'XAAttributeRunList':
+
ls = self.xa_elem.arrayByApplyingSelector_("attributeRuns") or []
+
return [self._new_element([plist for plist in ls], XAAttributeRunList)]
+
+
[docs] def attachments(self) -> 'XAAttachmentList':
+
ls = self.xa_elem.arrayByApplyingSelector_("attachments") or []
+
return [self._new_element([plist for plist in ls], XAAttachmentList)]
+
+
[docs] def get_clipboard_representation(self) -> list[Union[str, 'AppKit.NSURL']]:
+
"""Gets a clipboard-codable representation of each document in the list.
+
+
When the clipboard content is set to a list of documents, each documents's file URL and name are added to the clipboard.
+
+
:return: A list of each document's file URL and name
+
:rtype: list[Union[str, AppKit.NSURL]]
+
+
.. versionadded:: 0.0.8
+
"""
+
return [str(x) for x in self.text()]
+
+
[docs] def __repr__(self):
+
return "<" + str(type(self)) + str(self.text()) + ">"
+
+[docs]class XATextDocument(XAObject):
+
"""A class for managing and interacting with text documents.
+
+
.. versionadded:: 0.0.1
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
@property
+
def text(self) -> 'XAText':
+
"""The text of the document.
+
"""
+
return self._new_element(self.xa_elem.text(), XAText)
+
+
@text.setter
+
def text(self, text: Union[str, 'XAText']):
+
if isinstance(text, XAText):
+
text = text.xa_elem
+
self.set_property("text", text)
+
+
[docs] def prepend(self, text: str) -> 'XATextDocument':
+
"""Inserts the provided text at the beginning of the document.
+
+
:param text: The text to insert.
+
:type text: str
+
:return: A reference to the document object.
+
:rtype: XATextDocument
+
+
.. seealso:: :func:`append`, :func:`set_text`
+
+
.. versionadded:: 0.0.1
+
"""
+
old_text = str(self.text)
+
self.set_property("text", text + old_text)
+
return self
+
+
[docs] def append(self, text: str) -> 'XATextDocument':
+
"""Appends the provided text to the end of the document.
+
+
:param text: The text to append.
+
:type text: str
+
:return: A reference to the document object.
+
:rtype: XATextDocument
+
+
.. seealso:: :func:`prepend`, :func:`set_text`
+
+
.. versionadded:: 0.0.1
+
"""
+
old_text = str(self.text)
+
self.set_property("text", old_text + text)
+
return self
+
+
[docs] def reverse(self) -> 'XATextDocument':
+
"""Reverses the text of the document.
+
+
:return: A reference to the document object.
+
:rtype: XATextDocument
+
+
.. versionadded:: 0.0.4
+
"""
+
self.set_property("text", reversed(str(self.text)))
+
return self
+
+
[docs] def paragraphs(self, filter: dict = None) -> 'XAParagraphList':
+
return self.text.paragraphs(filter)
+
+
[docs] def sentences(self, filter: dict = None) -> 'XASentenceList':
+
return self.text.sentences(filter)
+
+
[docs] def words(self, filter: dict = None) -> 'XAWordList':
+
return self.text.words(filter)
+
+
[docs] def characters(self, filter: dict = None) -> 'XACharacterList':
+
return self.text.characters(filter)
+
+
[docs] def attribute_runs(self, filter: dict = None) -> 'XAAttributeRunList':
+
return self.text.attribute_runs(filter)
+
+
[docs] def attachments(self, filter: dict = None) -> 'XAAttachmentList':
+
return self.text.attachments(filter)
+
+
+
+
+[docs]class XATextList(XAList):
+
"""A wrapper around lists of text objects that employs fast enumeration techniques.
+
+
.. versionadded:: 0.0.4
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None, obj_class = None):
+
if obj_class is None:
+
obj_class = XAText
+
super().__init__(properties, obj_class, filter)
+
+
[docs] def paragraphs(self, filter: dict = None) -> 'XAParagraphList':
+
ls = []
+
if hasattr(self.xa_elem, "get"):
+
ls = self.xa_elem.arrayByApplyingSelector_("paragraphs") or []
+
else:
+
ls = [x.xa_elem.split("\n") for x in self]
+
ls = [paragraph for paragraphlist in ls for paragraph in paragraphlist if paragraph.strip() != '']
+
return self._new_element(ls, XAParagraphList, filter)
+
+
[docs] def sentences(self) -> 'XASentenceList':
+
ls = [x.sentences() for x in self]
+
ls = [sentence for sentencelist in ls for sentence in sentencelist]
+
return self._new_element(ls, XASentenceList)
+
+
[docs] def words(self, filter: dict = None) -> 'XAWordList':
+
ls = []
+
if hasattr(self.xa_elem, "get"):
+
ls = self.xa_elem.arrayByApplyingSelector_("words") or []
+
else:
+
ls = [x.xa_elem.split() for x in self]
+
ls = [word for wordlist in ls for word in wordlist]
+
return self._new_element(ls, XAWordList, filter)
+
+
[docs] def characters(self, filter: dict = None) -> 'XACharacterList':
+
ls = []
+
if hasattr(self.xa_elem, "get"):
+
ls = self.xa_elem.arrayByApplyingSelector_("characters") or []
+
else:
+
ls = [list(x.xa_elem) for x in self]
+
ls = [character for characterlist in ls for character in characterlist]
+
return self._new_element(ls, XACharacterList, filter)
+
+
[docs] def attribute_runs(self, filter: dict = None) -> 'XAAttributeRunList':
+
ls = []
+
if hasattr(self.xa_elem, "get"):
+
ls = self.xa_elem.arrayByApplyingSelector_("attributeRuns") or []
+
ls = [attribute_run for attribute_run_list in ls for attribute_run in attribute_run_list]
+
return self._new_element(ls, XAAttributeRunList, filter)
+
+
[docs] def attachments(self, filter: dict = None) -> 'XAAttachmentList':
+
ls = []
+
if hasattr(self.xa_elem, "get"):
+
ls = self.xa_elem.arrayByApplyingSelector_("attachments") or []
+
ls = [attachment for attachment_list in ls for attachment in attachment_list]
+
return self._new_element(ls, XAAttachmentList, filter)
+
+
[docs] def __repr__(self):
+
try:
+
if isinstance(self.xa_elem[0], ScriptingBridge.SBObject):
+
# List items will not resolved to text upon dereferencing the list; need to resolve items individually
+
count = self.xa_elem.count()
+
if count <= 500:
+
# Too many unresolved pointers, save time by just reporting the length
+
return "<" + str(type(self)) + str([x.get() for x in self.xa_elem]) + ">"
+
return "<" + str(type(self)) + "length: " + str(self.xa_elem.count()) + ">"
+
+
# List items will resolve to text upon dereferencing the list
+
return "<" + str(type(self)) + str(self.xa_elem.get()) + ">"
+
except:
+
return "<" + str(type(self)) + str(list(self.xa_elem)) + ">"
+
+[docs]class XAText(XAObject):
+
"""A class for managing and interacting with the text of documents.
+
+
.. versionadded:: 0.0.1
+
"""
+
[docs] def __init__(self, properties):
+
if isinstance(properties, dict):
+
super().__init__(properties)
+
elif isinstance(properties, str):
+
super().__init__({"element": properties})
+
+
self.text: str #: The plaintext contents of the rich text
+
self.color: XAColor #: The color of the first character
+
self.font: str #: The name of the font of the first character
+
self.size: int #: The size in points of the first character
+
+
@property
+
def text(self) -> str:
+
if isinstance(self.xa_elem, str):
+
return self.xa_elem
+
else:
+
return self.xa_elem.text()
+
+
@text.setter
+
def text(self, text: str):
+
if isinstance(self.xa_elem, str):
+
self.xa_elem = text
+
else:
+
self.set_property("text", text)
+
+
@property
+
def color(self) -> 'XAColor':
+
if isinstance(self.xa_elem, str):
+
return None
+
else:
+
return XAColor(self.xa_elem.color())
+
+
@color.setter
+
def color(self, color: 'XAColor'):
+
if isinstance(self.xa_elem, str):
+
self.color = color.xa_elem
+
else:
+
self.set_property("color", color.xa_elem)
+
+
@property
+
def font(self) -> str:
+
if isinstance(self.xa_elem, str):
+
return None
+
else:
+
return self.xa_elem.font()
+
+
@font.setter
+
def font(self, font: str):
+
if isinstance(self.xa_elem, str):
+
self.font = font
+
else:
+
self.set_property("font", font)
+
+
@property
+
def size(self) -> int:
+
if isinstance(self.xa_elem, str):
+
return 0
+
else:
+
return self.xa_elem.size()
+
+
@size.setter
+
def size(self, size: int):
+
if isinstance(self.xa_elem, str):
+
self.size = size
+
else:
+
self.set_property("size", size)
+
+
[docs] def tag_parts_of_speech(self, unit: Literal["word", "sentence", "paragraph", "document"] = "word") -> list[tuple[str, str]]:
+
"""Tags each word of the text with its associated part of speech.
+
+
:param unit: The grammatical unit to divide the text into for tagging, defaults to "word"
+
:type unit: Literal["word", "sentence", "paragraph", "document"]
+
:return: A list of tuples identifying each word of the text and its part of speech
+
:rtype: list[tuple[str, str]]
+
+
:Example 1: Extract nouns from a text
+
+
>>> import PyXA
+
>>> text = PyXA.XAText("Here’s to the crazy ones. The misfits. The rebels.")
+
>>> nouns = [pos[0] for pos in text.tag_parts_of_speech() if pos[1] == "Noun"]
+
>>> print(nouns)
+
['ones', 'misfits', 'rebels']
+
+
.. versionadded:: 0.1.0
+
"""
+
import NaturalLanguage
+
tagger = NaturalLanguage.NLTagger.alloc().initWithTagSchemes_([NaturalLanguage.NLTagSchemeLexicalClass])
+
tagger.setString_(str(self.xa_elem))
+
+
if unit == "word":
+
unit = NaturalLanguage.NLTokenUnitWord
+
elif unit == "sentence":
+
unit = NaturalLanguage.NLTokenUnitSentence
+
elif unit == "paragraph":
+
unit = NaturalLanguage.NLTokenUnitParagraph
+
elif unit == "document":
+
unit = NaturalLanguage.NLTokenUnitDocument
+
+
tagged_pos = []
+
def apply_tags(tag, token_range, error):
+
word_phrase = str(self.xa_elem)[token_range.location:token_range.location + token_range.length]
+
tagged_pos.append((word_phrase, tag))
+
+
tagger.enumerateTagsInRange_unit_scheme_options_usingBlock_((0, len(str(self.xa_elem))), unit, NaturalLanguage.NLTagSchemeLexicalClass, NaturalLanguage.NLTaggerOmitPunctuation | NaturalLanguage.NLTaggerOmitWhitespace, apply_tags)
+
return tagged_pos
+
+
[docs] def tag_languages(self, unit: Literal["word", "sentence", "paragraph", "document"] = "paragraph") -> list[tuple[str, str]]:
+
"""Tags each paragraph of the text with its language.
+
+
:param unit: The grammatical unit to divide the text into for tagging, defaults to "paragraph"
+
:type unit: Literal["word", "sentence", "paragraph", "document"]
+
:return: A list of tuples identifying each paragraph of the text and its language
+
:rtype: list[tuple[str, str]]
+
+
:Example:
+
+
>>> import PyXA
+
>>> text = PyXA.XAText("This is English.\\nQuesto è Italiano.\\nDas ist deutsch.\\nこれは日本語です。")
+
>>> print(text.tag_languages())
+
[('This is English.\\n', 'en'), ('Questo è Italiano.\\n', 'it'), ('Das ist deutsch.\\n', 'de'), ('これは日本語です。', 'ja')]
+
+
.. versionadded:: 0.1.0
+
"""
+
import NaturalLanguage
+
tagger = NaturalLanguage.NLTagger.alloc().initWithTagSchemes_([NaturalLanguage.NLTagSchemeLanguage])
+
tagger.setString_(str(self.xa_elem))
+
+
if unit == "word":
+
unit = NaturalLanguage.NLTokenUnitWord
+
elif unit == "sentence":
+
unit = NaturalLanguage.NLTokenUnitSentence
+
elif unit == "paragraph":
+
unit = NaturalLanguage.NLTokenUnitParagraph
+
elif unit == "document":
+
unit = NaturalLanguage.NLTokenUnitDocument
+
+
tagged_languages = []
+
def apply_tags(tag, token_range, error):
+
paragraph = str(self.xa_elem)[token_range.location:token_range.location + token_range.length]
+
if paragraph.strip() != "":
+
tagged_languages.append((paragraph, tag))
+
+
tagger.enumerateTagsInRange_unit_scheme_options_usingBlock_((0, len(str(self.xa_elem))), unit, NaturalLanguage.NLTagSchemeLanguage, NaturalLanguage.NLTaggerOmitPunctuation | NaturalLanguage.NLTaggerOmitWhitespace, apply_tags)
+
return tagged_languages
+
+
[docs] def tag_entities(self, unit: Literal["word", "sentence", "paragraph", "document"] = "word") -> list[tuple[str, str]]:
+
"""Tags each word of the text with either the category of entity it represents (i.e. person, place, or organization) or its part of speech.
+
+
:param unit: The grammatical unit to divide the text into for tagging, defaults to "word"
+
:type unit: Literal["word", "sentence", "paragraph", "document"]
+
:return: A list of tuples identifying each word of the text and its entity category or part of speech
+
:rtype: list[tuple[str, str]]
+
+
:Example:
+
+
>>> import PyXA
+
>>> text = PyXA.XAText("Tim Cook is the CEO of Apple.")
+
>>> print(text.tag_entities())
+
[('Tim', 'PersonalName'), ('Cook', 'PersonalName'), ('is', 'Verb'), ('the', 'Determiner'), ('CEO', 'Noun'), ('of', 'Preposition'), ('Apple', 'OrganizationName')]
+
+
.. versionadded:: 0.1.0
+
"""
+
import NaturalLanguage
+
tagger = NaturalLanguage.NLTagger.alloc().initWithTagSchemes_([NaturalLanguage.NLTagSchemeNameTypeOrLexicalClass])
+
tagger.setString_(str(self.xa_elem))
+
+
if unit == "word":
+
unit = NaturalLanguage.NLTokenUnitWord
+
elif unit == "sentence":
+
unit = NaturalLanguage.NLTokenUnitSentence
+
elif unit == "paragraph":
+
unit = NaturalLanguage.NLTokenUnitParagraph
+
elif unit == "document":
+
unit = NaturalLanguage.NLTokenUnitDocument
+
+
tagged_languages = []
+
def apply_tags(tag, token_range, error):
+
word_phrase = str(self.xa_elem)[token_range.location:token_range.location + token_range.length]
+
if word_phrase.strip() != "":
+
tagged_languages.append((word_phrase, tag))
+
+
tagger.enumerateTagsInRange_unit_scheme_options_usingBlock_((0, len(str(self.xa_elem))), unit, NaturalLanguage.NLTagSchemeNameTypeOrLexicalClass, NaturalLanguage.NLTaggerOmitPunctuation | NaturalLanguage.NLTaggerOmitWhitespace, apply_tags)
+
return tagged_languages
+
+
[docs] def tag_lemmas(self, unit: Literal["word", "sentence", "paragraph", "document"] = "word") -> list[tuple[str, str]]:
+
"""Tags each word of the text with its stem word.
+
+
:param unit: The grammatical unit to divide the text into for tagging, defaults to "word"
+
:type unit: Literal["word", "sentence", "paragraph", "document"]
+
:return: A list of tuples identifying each word of the text and its stem words
+
:rtype: list[tuple[str, str]]
+
+
:Example 1: Lemmatize each word in a text
+
+
>>> import PyXA
+
>>> text = PyXA.XAText("Here’s to the crazy ones. The misfits. The rebels.")
+
>>> print(text.tag_lemmas())
+
[('Here’s', 'here'), ('to', 'to'), ('the', 'the'), ('crazy', 'crazy'), ('ones', 'one'), ('The', 'the'), ('misfits', 'misfit'), ('The', 'the'), ('rebels', 'rebel')]
+
+
:Example 2: Combine parts of speech tagging and lemmatization
+
+
>>> import PyXA
+
>>> text = PyXA.XAText("The quick brown fox tries to jump over the sleeping lazy dog.")
+
>>> verbs = [pos[0] for pos in text.tag_parts_of_speech() if pos[1] == "Verb"]
+
>>> for index, verb in enumerate(verbs):
+
>>> print(index, PyXA.XAText(verb).tag_lemmas())
+
0 [('tries', 'try')]
+
1 [('jump', 'jump')]
+
2 [('sleeping', 'sleep')]
+
+
.. versionadded:: 0.1.0
+
"""
+
import NaturalLanguage
+
tagger = NaturalLanguage.NLTagger.alloc().initWithTagSchemes_([NaturalLanguage.NLTagSchemeLemma])
+
tagger.setString_(str(self.xa_elem))
+
+
if unit == "word":
+
unit = NaturalLanguage.NLTokenUnitWord
+
elif unit == "sentence":
+
unit = NaturalLanguage.NLTokenUnitSentence
+
elif unit == "paragraph":
+
unit = NaturalLanguage.NLTokenUnitParagraph
+
elif unit == "document":
+
unit = NaturalLanguage.NLTokenUnitDocument
+
+
tagged_lemmas = []
+
def apply_tags(tag, token_range, error):
+
word_phrase = str(self.xa_elem)[token_range.location:token_range.location + token_range.length]
+
if word_phrase.strip() != "":
+
tagged_lemmas.append((word_phrase, tag))
+
+
tagger.enumerateTagsInRange_unit_scheme_options_usingBlock_((0, len(str(self.xa_elem))), unit, NaturalLanguage.NLTagSchemeLemma, NaturalLanguage.NLTaggerOmitPunctuation | NaturalLanguage.NLTaggerOmitWhitespace | NaturalLanguage.NLTaggerJoinContractions, apply_tags)
+
return tagged_lemmas
+
+
[docs] def tag_sentiments(self, sentiment_scale: list[str] = None, unit: Literal["word", "sentence", "paragraph", "document"] = "paragraph") -> list[tuple[str, str]]:
+
"""Tags each paragraph of the text with a sentiment rating.
+
+
:param sentiment_scale: A list of terms establishing a range of sentiments from most negative to most postive
+
:type sentiment_scale: list[str]
+
:param unit: The grammatical unit to divide the text into for tagging, defaults to "paragraph"
+
:type unit: Literal["word", "sentence", "paragraph", "document"]
+
:return: A list of tuples identifying each paragraph of the text and its sentiment rating
+
:rtype: list[tuple[str, str]]
+
+
:Example 1: Assess the sentiment of a string
+
+
>>> import PyXA
+
>>> text = PyXA.XAText("This sucks.\\nBut this is great!")
+
>>> print(text.tag_sentiments())
+
[('This sucks.\\n', 'Negative'), ('But this is great!', 'Positive')]
+
+
:Example 2: Use a custom sentiment scale
+
+
>>> import PyXA
+
>>> text = PyXA.XAText("This sucks.\\nBut this is good!\\nAnd this is great!")
+
>>> print(text.tag_sentiments(sentiment_scale=["Very Negative", "Negative", "Somewhat Negative", "Neutral", "Somewhat Positive", "Positive", "Very Positive"]))
+
[('This sucks.\\n', 'Very Negative'), ('But this is good!\\n', 'Neutral'), ('And this is great!', 'Very Positive')]
+
+
:Example 3: Use other tag units
+
+
>>> import PyXA
+
>>> text = PyXA.XAText("This sucks.\\nBut this is good!\\nAnd this is great!")
+
>>> print(1, text.tag_sentiments())
+
>>> print(2, text.tag_sentiments(unit="word"))
+
>>> print(3, text.tag_sentiments(unit="document"))
+
1 [('This sucks.\\n', 'Negative'), ('But this is good!\\n', 'Neutral'), ('And this is great!', 'Positive')]
+
2 [('This', 'Negative'), ('sucks', 'Negative'), ('.', 'Negative'), ('But', 'Neutral'), ('this', 'Neutral'), ('is', 'Neutral'), ('good', 'Neutral'), ('!', 'Neutral'), ('And', 'Positive'), ('this', 'Positive'), ('is', 'Positive'), ('great', 'Positive'), ('!', 'Positive')]
+
3 [('This sucks.\\nBut this is good!\\nAnd this is great!', 'Neutral')]
+
+
.. versionadded:: 0.1.0
+
"""
+
import NaturalLanguage
+
if sentiment_scale is None or len(sentiment_scale) == 0:
+
sentiment_scale = ["Negative", "Neutral", "Positive"]
+
+
if unit == "word":
+
unit = NaturalLanguage.NLTokenUnitWord
+
elif unit == "sentence":
+
unit = NaturalLanguage.NLTokenUnitSentence
+
elif unit == "paragraph":
+
unit = NaturalLanguage.NLTokenUnitParagraph
+
elif unit == "document":
+
unit = NaturalLanguage.NLTokenUnitDocument
+
+
tagger = NaturalLanguage.NLTagger.alloc().initWithTagSchemes_([NaturalLanguage.NLTagSchemeSentimentScore])
+
tagger.setString_(str(self.xa_elem))
+
+
tagged_sentiments = []
+
def apply_tags(tag, token_range, error):
+
paragraph = str(self.xa_elem)[token_range.location:token_range.location + token_range.length]
+
if paragraph.strip() != "":
+
# Map raw tag value to range length
+
raw_value = float(tag or 0)
+
scaled = (raw_value + 1.0) / 2.0 * (len(sentiment_scale) - 1)
+
+
label = sentiment_scale[int(scaled)]
+
tagged_sentiments.append((paragraph, label))
+
+
tagger.enumerateTagsInRange_unit_scheme_options_usingBlock_((0, len(self.xa_elem)), unit, NaturalLanguage.NLTagSchemeSentimentScore, 0, apply_tags)
+
return tagged_sentiments
+
+
[docs] def paragraphs(self, filter: dict = None) -> 'XAParagraphList':
+
"""Gets a list of paragraphs in the text.
+
+
:param filter: The properties and associated values to filter paragraphs by, defaults to None
+
:type filter: dict, optional
+
:return: The list of paragraphs
+
:rtype: XAParagraphList
+
+
:Example 1: Get paragraphs of a text string
+
+
>>> import PyXA
+
>>> string = \"\"\"This is the first paragraph.
+
>>>
+
>>> This is the second paragraph.\"\"\"
+
>>> text = PyXA.XAText(string)
+
>>> print(text.paragraphs())
+
<<class 'PyXA.XAWordList'>['This is the first paragraph.', 'This is the second paragraph. Neat! Very cool.']>
+
+
:Example 2: Get paragraphs of a Note
+
+
>>> import PyXA
+
>>> app = PyXA.Application("Notes")
+
>>> note = app.notes()[0]
+
>>> text = PyXA.XAText(note.plaintext)
+
>>> print(text.paragraphs())
+
<<class 'PyXA.XAWordList'>['This is the first paragraph.', 'This is the second paragraph. Neat! Very cool.']>
+
+
.. versionadded:: 0.0.1
+
"""
+
if isinstance(self.xa_elem, str):
+
ls = [x for x in self.xa_elem.split("\n") if x.strip() != '']
+
return self._new_element(ls, XAWordList, filter)
+
else:
+
return self._new_element(self.xa_elem.paragraphs(), XAParagraphList, filter)
+
+
[docs] def sentences(self) -> 'XASentenceList':
+
"""Gets a list of sentences in the text.
+
+
:return: The list of sentencnes
+
:rtype: XASentenceList
+
+
:Example:
+
+
>>> import PyXA
+
>>> app = PyXA.Application("Notes")
+
>>> note = app.notes()[0]
+
>>> text = PyXA.XAText(note.plaintext)
+
>>> print(text.sentences())
+
<<class 'PyXA.XASentenceList'>['This is the first paragraph.\\n', '\\n', 'This is the second paragraph. ', 'Neat! ', 'Very cool.']>
+
+
.. versionadded:: 0.1.0
+
"""
+
raw_string = self.xa_elem
+
if hasattr(self.xa_elem, "get"):
+
raw_string = self.xa_elem.get()
+
+
sentences = []
+
tokenizer = AppKit.NLTokenizer.alloc().initWithUnit_(AppKit.kCFStringTokenizerUnitSentence)
+
tokenizer.setString_(raw_string)
+
for char_range in tokenizer.tokensForRange_((0, len(raw_string))):
+
start = char_range.rangeValue().location
+
end = start + char_range.rangeValue().length
+
sentences.append(raw_string[start:end])
+
+
ls = AppKit.NSArray.alloc().initWithArray_(sentences)
+
return self._new_element(sentences, XASentenceList)
+
+
[docs] def words(self, filter: dict = None) -> 'XAWordList':
+
"""Gets a list of words in the text.
+
+
:return: The list of words
+
:rtype: XAWordList
+
+
:Example:
+
+
>>> import PyXA
+
>>> app = PyXA.Application("Notes")
+
>>> note = app.notes()[0]
+
>>> text = PyXA.XAText(note.plaintext)
+
>>> print(text.words())
+
<<class 'PyXA.XAWordList'>['This', 'is', 'the', 'first', 'paragraph.', 'This', 'is', 'the', 'second', 'paragraph.', 'Neat!', 'Very', 'cool.']>
+
+
.. versionadded:: 0.0.1
+
"""
+
if isinstance(self.xa_elem, str):
+
ls = self.xa_elem.split()
+
return self._new_element(ls, XAWordList, filter)
+
else:
+
return self._new_element(self.xa_elem.words(), XAWordList, filter)
+
+
[docs] def characters(self, filter: dict = None) -> 'XACharacterList':
+
"""Gets a list of characters in the text.
+
+
:return: The list of characters
+
:rtype: XACharacterList
+
+
:Example 1: Get all characters in a text
+
+
>>> import PyXA
+
>>> app = PyXA.Application("Notes")
+
>>> note = app.notes()[0]
+
>>> text = PyXA.XAText(note.plaintext)
+
>>> print(text.characters())
+
<<class 'PyXA.XACharacterList'>['T', 'h', 'i', 's', ' ', 'i', 's', ' ', 't', 'h', 'e', ' ', 'f', 'i', 'r', 's', 't', ' ', 'p', 'a', 'r', 'a', 'g', 'r', 'a', 'p', 'h', '.', '\\n', '\\n', 'T', 'h', 'i', 's', ' ', 'i', 's', ' ', 't', 'h', 'e', ' ', 's', 'e', 'c', 'o', 'n', 'd', ' ', 'p', 'a', 'r', 'a', 'g', 'r', 'a', 'p', 'h', '.', ' ', 'N', 'e', 'a', 't', '!', ' ', 'V', 'e', 'r', 'y', ' ', 'c', 'o', 'o', 'l', '.']>
+
+
:Example 2: Get the characters of the first word in a text
+
+
>>> import PyXA
+
>>> app = PyXA.Application("Notes")
+
>>> note = app.notes()[0]
+
>>> text = PyXA.XAText(note.plaintext)
+
>>> print(text.words()[0].characters())
+
<<class 'PyXA.XACharacterList'>['T', 'h', 'i', 's']>
+
+
.. versionadded:: 0.0.1
+
"""
+
if isinstance(self.xa_elem, str):
+
ls = list(self.xa_elem)
+
return self._new_element(ls, XACharacterList, filter)
+
else:
+
return self._new_element(self.xa_elem.characters().get(), XACharacterList, filter)
+
+
[docs] def attribute_runs(self, filter: dict = None) -> 'XAAttributeRunList':
+
"""Gets a list of attribute runs in the text. For formatted text, this returns all sequences of characters sharing the same attributes.
+
+
:param filter: The properties and associated values to filter attribute runs by, defaults to None
+
:type filter: dict, optional
+
:return: The list of attribute runs
+
:rtype: XAAttributeRunList
+
+
.. versionadded:: 0.0.1
+
"""
+
if isinstance(self.xa_elem, str):
+
return []
+
else:
+
return self._new_element(self.xa_elem.attributeRuns(), XAAttributeRunList, filter)
+
+
[docs] def attachments(self, filter: dict = None) -> 'XAAttachmentList':
+
"""Gets a list of attachments of the text.
+
+
:param filter: The properties and associated values to filter attachments by, defaults to None
+
:type filter: dict, optional
+
:return: The list of attachments
+
:rtype: XAAttachmentList
+
+
.. versionadded:: 0.0.1
+
"""
+
if isinstance(self.xa_elem, str):
+
return []
+
else:
+
return self._new_element(self.xa_elem.attachments(), XAAttachmentList, filter)
+
+
[docs] def __len__(self):
+
return len(self.xa_elem.get())
+
+
[docs] def __str__(self):
+
if isinstance(self.xa_elem, str):
+
return self.xa_elem
+
return str(self.xa_elem.get())
+
+
[docs] def __repr__(self):
+
if isinstance(self.xa_elem, str):
+
return self.xa_elem
+
return str(self.xa_elem.get())
+
+
+
+
+[docs]class XAParagraphList(XATextList):
+
"""A wrapper around lists of paragraphs that employs fast enumeration techniques.
+
+
.. versionadded:: 0.0.5
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, filter, XAParagraph)
+
+[docs]class XAParagraph(XAText):
+
"""A class for managing and interacting with paragraphs in text documents.
+
+
.. versionadded:: 0.0.1
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
+
+[docs]class XASentenceList(XATextList):
+
"""A wrapper around lists of sentences that employs fast enumeration techniques.
+
+
.. versionadded:: 0.0.5
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, filter, XASentence)
+
+[docs]class XASentence(XAText):
+
"""A class for managing and interacting with sentences in text documents.
+
+
.. versionadded:: 0.0.1
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
+
+
+[docs]class XAWordList(XATextList):
+
"""A wrapper around lists of words that employs fast enumeration techniques.
+
+
.. versionadded:: 0.0.5
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, filter, XAWord)
+
+[docs]class XAWord(XAText):
+
"""A class for managing and interacting with words in text documents.
+
+
.. versionadded:: 0.0.1
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
+
+
+[docs]class XACharacterList(XATextList):
+
"""A wrapper around lists of characters that employs fast enumeration techniques.
+
+
.. versionadded:: 0.0.5
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, filter, XACharacter)
+
+[docs]class XACharacter(XAText):
+
"""A class for managing and interacting with characters in text documents.
+
+
.. versionadded:: 0.0.1
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
+
+
+[docs]class XAAttributeRunList(XATextList):
+
"""A wrapper around lists of attribute runs that employs fast enumeration techniques.
+
+
.. versionadded:: 0.0.5
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, filter, XAAttributeRun)
+
+[docs]class XAAttributeRun(XAText):
+
"""A class for managing and interacting with attribute runs in text documents.
+
+
.. versionadded:: 0.0.1
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
+
+
+[docs]class XAAttachmentList(XATextList):
+
"""A wrapper around lists of text attachments that employs fast enumeration techniques.
+
+
.. versionadded:: 0.0.5
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, filter, XAAttachment)
+
+[docs]class XAAttachment(XAObject):
+
"""A class for managing and interacting with attachments in text documents.
+
+
.. versionadded:: 0.0.1
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
+
+
+[docs]class XAColorList(XATextList):
+
"""A wrapper around lists of colors that employs fast enumeration techniques.
+
+
.. versionadded:: 0.0.6
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, XAColor, filter)
+
+[docs]class XAColor(XAObject, XAClipboardCodable):
+
[docs] def __init__(self, *args):
+
if len(args) == 0:
+
# No color specified -- default to white
+
self.xa_elem = XAColor.white_color().xa_elem
+
elif len(args) == 1 and isinstance(args[0], AppKit.NSColor):
+
# Initialize copy of non-mutable NSColor object
+
self.copy_color(args[0])
+
elif len(args) == 1 and isinstance(args[0], XAColor):
+
# Initialize copy of another XAColor object
+
self.copy_color(args[0].xa_elem)
+
else:
+
# Initialize from provided RGBA values
+
red = args[0] if len(args) >= 0 else 255
+
green = args[1] if len(args) >= 1 else 255
+
blue = args[2] if len(args) >= 3 else 255
+
alpha = args[3] if len(args) == 4 else 1.0
+
self.xa_elem = AppKit.NSCalibratedRGBColor.alloc().initWithRed_green_blue_alpha_(red, green, blue, alpha)
+
+
[docs] def red() -> 'XAColor':
+
"""Initializes and returns a pure red :class:`XAColor` object.
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAColor(65535, 0, 0)
+
+
[docs] def orange() -> 'XAColor':
+
"""Initializes and returns an :class:`XAColor` object whose RGB values are (1.0, 0.5, 0.0).
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAColor(AppKit.NSColor.orangeColor())
+
+
[docs] def yellow() -> 'XAColor':
+
"""Initializes and returns an :class:`XAColor` object whose RGB values are (1.0, 1.0, 0.0).
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAColor(AppKit.NSColor.yellowColor())
+
+
[docs] def green() -> 'XAColor':
+
"""Initializes and returns a pure green :class:`XAColor` object.
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAColor(0, 65535, 0)
+
+
[docs] def cyan() -> 'XAColor':
+
"""Initializes and returns an :class:`XAColor` object whose RGB values are (0.0, 1.0, 1.0).
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAColor(AppKit.NSColor.cyanColor())
+
+
[docs] def blue() -> 'XAColor':
+
"""Initializes and returns a pure blue :class:`XAColor` object.
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAColor(0, 0, 65535)
+
+
[docs] def magenta() -> 'XAColor':
+
"""Initializes and returns an :class:`XAColor` object whose RGB values are (1.0, 0.0, 1.0).
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAColor(AppKit.NSColor.magentaColor())
+
+
[docs] def purple() -> 'XAColor':
+
"""Initializes and returns an :class:`XAColor` object whose RGB values are (0.5, 0.0, 0.5).
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAColor(AppKit.NSColor.purpleColor())
+
+
[docs] def brown() -> 'XAColor':
+
"""Initializes and returns an :class:`XAColor` object whose RGB values are (0.6, 0.4, 0.2).
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAColor(AppKit.NSColor.brownColor())
+
+
[docs] def white() -> 'XAColor':
+
"""Initializes and returns a pure white :class:`XAColor` object.
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAColor(65535, 65535, 65535)
+
+
[docs] def gray() -> 'XAColor':
+
"""Initializes and returns an :class:`XAColor` object whose RGB values are (0.5, 0.5, 0.5).
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAColor(0.5, 0.5, 0.5)
+
+
[docs] def black() -> 'XAColor':
+
"""Initializes and returns a pure black :class:`XAColor` object.
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAColor(0, 0, 0)
+
+
[docs] def clear() -> 'XAColor':
+
"""Initializes and returns a an :class:`XAColor` object whose alpha value is 0.0.
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAColor(0, 0, 0, 0)
+
+
@property
+
def hex_value(self) -> str:
+
"""The HEX representation of the color.
+
+
.. versionadded:: 0.1.1
+
"""
+
return f"{hex(int(self.red_value * 255))[2:]}{hex(int(self.green_value * 255))[2:]}{hex(int(self.blue_value * 255))[2:]}".upper()
+
+
@property
+
def red_value(self) -> float:
+
"""The red value of the color on the scale of 0.0 to 1.0.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.redComponent()
+
+
@red_value.setter
+
def red_value(self, red_value: float):
+
self.xa_elem = AppKit.NSCalibratedRGBColor.alloc().initWithRed_green_blue_alpha_(red_value, self.green_value, self.blue_value, self.alpha_value)
+
+
@property
+
def green_value(self) -> float:
+
"""The green value of the color on the scale of 0.0 to 1.0.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.greenComponent()
+
+
@green_value.setter
+
def green_value(self, green_value: float):
+
self.xa_elem = AppKit.NSCalibratedRGBColor.alloc().initWithRed_green_blue_alpha_(self.red_value, green_value, self.blue_value, self.alpha_value)
+
+
@property
+
def blue_value(self) -> float:
+
"""The blue value of the color on the scale of 0.0 to 1.0.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.blueComponent()
+
+
@blue_value.setter
+
def blue_value(self, blue_value: float):
+
self.xa_elem = AppKit.NSCalibratedRGBColor.alloc().initWithRed_green_blue_alpha_(self.red_value, self.green_value, blue_value, self.alpha_value)
+
+
@property
+
def alpha_value(self) -> float:
+
"""The alpha value of the color on the scale of 0.0 to 1.0.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.alphaComponent()
+
+
@alpha_value.setter
+
def alpha_value(self, alpha_value: float):
+
self.xa_elem = AppKit.NSCalibratedRGBColor.alloc().initWithRed_green_blue_alpha_(self.red_value, self.green_value, self.blue_value, alpha_value)
+
+
@property
+
def hue_value(self):
+
"""The hue value of the color on the scale of 0.0 to 1.0.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.hueComponent()
+
+
@hue_value.setter
+
def hue_value(self, hue_value: float):
+
self.xa_elem = AppKit.NSCalibratedRGBColor.initWithHue_saturation_brightness_alpha_(hue_value, self.saturation_value, self.brightness_value, self.alpha_value)
+
+
@property
+
def saturation_value(self):
+
"""The staturation value of the color on the scale of 0.0 to 1.0.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.saturationComponent()
+
+
@saturation_value.setter
+
def saturation_value(self, saturation_value: float):
+
self.xa_elem = AppKit.NSCalibratedRGBColor.initWithHue_saturation_brightness_alpha_(self.hue_value, saturation_value, self.brightness_value, self.alpha_value)
+
+
@property
+
def brightness_value(self):
+
"""The brightness value of the color on the scale of 0.0 to 1.0.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.brightnessComponent()
+
+
@brightness_value.setter
+
def brightness_value(self, brightness_value: float):
+
self.xa_elem = AppKit.NSCalibratedRGBColor.initWithHue_saturation_brightness_alpha_(self.hue_value, self.saturation_value, brightness_value, self.alpha_value)
+
+
[docs] def copy_color(self, color: 'AppKit.NSColor') -> 'XAColor':
+
"""Initializes a XAColor copy of an NSColor object.
+
+
:param color: The NSColor to copy
+
:type color: AppKit.NSColor
+
:return: The newly created XAColor object
+
:rtype: XAColor
+
+
.. versionadded:: 0.1.0
+
"""
+
self.xa_elem = AppKit.NSCalibratedRGBColor.alloc().initWithRed_green_blue_alpha_(
+
color.redComponent(),
+
color.greenComponent(),
+
color.blueComponent(),
+
color.alphaComponent()
+
)
+
return self
+
+
[docs] def set_rgba(self, red: float, green: float, blue: float, alpha: float) -> 'XAColor':
+
"""Sets the RGBA values of the color.
+
+
:param red: The red value of the color, from 0.0 to 1.0
+
:type red: float
+
:param green: The green value of the color, from 0.0 to 1.0
+
:type green: float
+
:param blue: The blue value of the color, from 0.0 to 1.0
+
:type blue: float
+
:param alpha: The opacity of the color, from 0.0 to 1.0
+
:type alpha: float
+
:return: The XAColor object
+
:rtype: XAColor
+
+
.. versionadded:: 0.1.0
+
"""
+
self.xa_elem = AppKit.NSCalibratedRGBColor.alloc().initWithRed_green_blue_alpha_(red, green, blue, alpha)
+
return self
+
+
[docs] def set_hsla(self, hue: float, saturation: float, brightness: float, alpha: float) -> 'XAColor':
+
"""Sets the HSLA values of the color.
+
+
:param hue: The hue value of the color, from 0.0 to 1.0
+
:type hue: float
+
:param saturation: The saturation value of the color, from 0.0 to 1.0
+
:type saturation: float
+
:param brightness: The brightness value of the color, from 0.0 to 1.0
+
:type brightness: float
+
:param alpha: The opacity of the color, from 0.0 to 1.0
+
:type alpha: float
+
:return: The XAColor object
+
:rtype: XAColor
+
+
.. versionadded:: 0.1.0
+
"""
+
self.xa_elem = AppKit.NSCalibratedRGBColor.initWithHue_saturation_brightness_alpha_(hue, saturation, brightness, alpha)
+
return self
+
+
[docs] def mix_with(self, color: 'XAColor', fraction: int = 0.5) -> 'XAColor':
+
"""Blends this color with the specified fraction of another.
+
+
:param color: The color to blend this color with
+
:type color: XAColor
+
:param fraction: The fraction of the other color to mix into this color, from 0.0 to 1.0, defaults to 0.5
+
:type fraction: int, optional
+
:return: The resulting color after mixing
+
:rtype: XAColor
+
+
.. versionadded:: 0.1.0
+
"""
+
new_color = self.xa_elem.blendedColorWithFraction_ofColor_(fraction, color.xa_elem)
+
return XAColor(new_color.redComponent(), new_color.greenComponent(), new_color.blueComponent(), new_color.alphaComponent())
+
+
[docs] def brighten(self, fraction: float = 0.5) -> 'XAColor':
+
"""Brightens the color by mixing the specified fraction of the system white color into it.
+
+
:param fraction: The amount (fraction) of white to mix into the color, defaults to 0.5
+
:type fraction: float, optional
+
:return: The resulting color after brightening
+
:rtype: XAColor
+
+
.. versionadded:: 0.1.0
+
"""
+
self.xa_elem = self.xa_elem.highlightWithLevel_(fraction)
+
return self
+
+
[docs] def darken(self, fraction: float = 0.5) -> 'XAColor':
+
"""Darkens the color by mixing the specified fraction of the system black color into it.
+
+
:param fraction: The amount (fraction) of black to mix into the color, defaults to 0.5
+
:type fraction: float, optional
+
:return: The resulting color after darkening
+
:rtype: XAColor
+
+
.. versionadded:: 0.1.0
+
"""
+
self.xa_elem = self.xa_elem.shadowWithLevel_(fraction)
+
return self
+
+
[docs] def make_swatch(self, width: int = 100, height: int = 100) -> 'XAImage':
+
"""Creates an image swatch of the color with the specified dimensions.
+
+
:param width: The width of the swatch image, in pixels, defaults to 100
+
:type width: int, optional
+
:param height: The height of the swatch image, in pixels, defaults to 100
+
:type height: int, optional
+
:return: The image swatch as an XAImage object
+
:rtype: XAImage
+
+
:Example: View swatches in Preview
+
+
>>> import PyXA
+
>>> from time import sleep
+
>>>
+
>>> blue = PyXA.XAColor.blue()
+
>>> red = PyXA.XAColor.red()
+
>>>
+
>>> swatches = [
+
>>> blue.make_swatch(),
+
>>> blue.darken(0.5).make_swatch(),
+
>>> blue.mix_with(red).make_swatch()
+
>>> ]
+
>>>
+
>>> for swatch in swatches:
+
>>> swatch.show_in_preview()
+
>>> sleep(0.2)
+
+
.. versionadded:: 0.1.0
+
"""
+
img = AppKit.NSImage.alloc().initWithSize_(AppKit.NSMakeSize(width, height))
+
img.lockFocus()
+
self.xa_elem.drawSwatchInRect_(AppKit.NSMakeRect(0, 0, width, height))
+
img.unlockFocus()
+
return XAImage(img)
+
+
[docs] def get_clipboard_representation(self) -> 'AppKit.NSColor':
+
"""Gets a clipboard-codable representation of the color.
+
+
When the clipboard content is set to a color, the raw color data is added to the clipboard.
+
+
:return: The raw color data
+
:rtype: AppKit.NSColor
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem
+
+
[docs] def __repr__(self):
+
return f"<{str(type(self))}r={str(self.red_value)}, g={self.green_value}, b={self.blue_value}, a={self.alpha_value}>"
+
+
+
+
+[docs]class XALocation(XAObject):
+
"""A location with a latitude and longitude, along with other data.
+
+
.. versionadded:: 0.0.2
+
"""
+
current_location: 'XALocation' #: The current location of the device
+
+
[docs] def __init__(self, raw_value = None, title: str = None, latitude: float = 0, longitude: float = 0, altitude: float = None, radius: int = 0, address: str = None):
+
self.raw_value = raw_value #: The raw CLLocation object
+
self.title = title #: The name of the location
+
self.latitude = latitude #: The latitude of the location
+
self.longitude = longitude #: The longitude of the location
+
self.altitude = altitude #: The altitude of the location
+
self.radius = radius #: The horizontal accuracy of the location measurement
+
self.address = address #: The address of the location
+
+
import CoreLocation
+
if self.raw_value is None:
+
if latitude is not None and longitude is not None:
+
self.raw_value = CoreLocation.CLLocation.alloc().initWithLatitude_longitude_(latitude, longitude)
+
+
@property
+
def raw_value(self):
+
return self.__raw_value
+
+
@raw_value.setter
+
def raw_value(self, raw_value):
+
self.__raw_value = raw_value
+
if raw_value is not None:
+
self.latitude = raw_value.coordinate()[0]
+
self.longitude = raw_value.coordinate()[1]
+
self.altitude = raw_value.altitude()
+
self.radius = raw_value.horizontalAccuracy()
+
+
@property
+
def current_location(self) -> 'XALocation':
+
"""The location of the user's computer.
+
"""
+
self.raw_value = None
+
self._spawn_thread(self.__get_current_location)
+
while self.raw_value is None:
+
time.sleep(0.01)
+
return self
+
+
[docs] def show_in_maps(self):
+
"""Shows the location in Maps.app.
+
+
.. versionadded:: 0.0.6
+
"""
+
XAURL(f"maps://?q={self.title},ll={self.latitude},{self.longitude}").open()
+
+
[docs] def reverse_geocode(self) -> dict[str, str]:
+
"""Obtains reverse-geocode information from the location's latitude and longitude.
+
+
:return: A dictionary containing the location's name, street address, locality, state, country, timezone, and notable features.
+
:rtype: dict[str, str]
+
+
:Example:
+
+
>>> import PyXA
+
>>> loc = PyXA.XALocation(latitude=44.460552, longitude=-110.82807)
+
>>> print(loc.reverse_geocode())
+
{'name': 'Old Faithful', 'street_number': None, 'street': 'Upper Geyser Basin Trail', 'sub_locality': None, 'locality': 'Alta', 'county': 'Teton County', 'state': 'WY', 'postal_code': '83414', 'country': 'United States', 'timezone': America/Denver (MST) offset -25200, 'notable_features': (
+
"Old Faithful",
+
"Yellowstone National Park"
+
)}
+
+
.. versionadded:: 0.1.1
+
"""
+
self._placemark = None
+
+
def get_place(place, error):
+
if place is not None:
+
self._placemark = place[0]
+
AppHelper.stopEventLoop()
+
+
import CoreLocation
+
CoreLocation.CLGeocoder.alloc().init().reverseGeocodeLocation_completionHandler_(self.raw_value, get_place)
+
AppHelper.runConsoleEventLoop()
+
+
return {
+
"name": self._placemark.name(),
+
"street_number": self._placemark.subThoroughfare(),
+
"street": self._placemark.thoroughfare(),
+
"sub_locality": self._placemark.subLocality(),
+
"locality": self._placemark.locality(),
+
"county": self._placemark.subAdministrativeArea(),
+
"state": self._placemark.administrativeArea(),
+
"postal_code": self._placemark.postalCode(),
+
"country": self._placemark.country(),
+
"timezone": self._placemark.timeZone(),
+
"notable_features": self._placemark.areasOfInterest(),
+
}
+
+
def __get_current_location(self):
+
import CoreLocation
+
location_manager = CoreLocation.CLLocationManager.alloc().init()
+
old_self = self
+
class CLLocationManagerDelegate(AppKit.NSObject):
+
def locationManager_didUpdateLocations_(self, manager, locations):
+
if locations is not None:
+
old_self.raw_value = locations[0]
+
AppHelper.stopEventLoop()
+
+
def locationManager_didFailWithError_(self, manager, error):
+
print(manager, error)
+
+
location_manager.requestWhenInUseAuthorization()
+
location_manager.setDelegate_(CLLocationManagerDelegate.alloc().init().retain())
+
location_manager.requestLocation()
+
+
AppHelper.runConsoleEventLoop()
+
+
[docs] def __repr__(self):
+
return "<" + str(type(self)) + str((self.latitude, self.longitude)) + ">"
+
+
+
+
+[docs]class XAColorPickerStyle(Enum):
+
"""Options for which tab a color picker should display when first opened.
+
"""
+
GRAYSCALE = AppKit.NSColorPanelModeGray
+
RGB_SLIDERS = AppKit.NSColorPanelModeRGB
+
CMYK_SLIDERS = AppKit.NSColorPanelModeCMYK
+
HSB_SLIDERS = AppKit.NSColorPanelModeHSB
+
COLOR_LIST = AppKit.NSColorPanelModeColorList
+
COLOR_WHEEL = AppKit.NSColorPanelModeWheel
+
CRAYONS = AppKit.NSColorPanelModeCrayon
+
IMAGE_PALETTE = AppKit.NSColorPanelModeCustomPalette
+
+[docs]class XAColorPicker(XAObject):
+
"""A class for creating and interacting with a color picker window.
+
+
.. versionadded:: 0.0.5
+
"""
+
[docs] def __init__(self, style: XAColorPickerStyle = XAColorPickerStyle.COLOR_WHEEL):
+
super().__init__()
+
self.style = style
+
+
[docs] def display(self) -> XAColor:
+
"""Displays the color picker.
+
+
:return: The color that the user selected
+
:rtype: XAColor
+
+
.. versionadded:: 0.0.5
+
"""
+
panel = AppKit.NSColorPanel.sharedColorPanel()
+
panel.setMode_(self.style.value)
+
panel.setShowsAlpha_(True)
+
+
def run_modal(panel):
+
initial_color = panel.color()
+
time.sleep(0.5)
+
while panel.isVisible() and panel.color() == initial_color:
+
time.sleep(0.01)
+
AppKit.NSApp.stopModal()
+
+
modal_thread = threading.Thread(target=run_modal, args=(panel, ), name="Run Modal", daemon=True)
+
modal_thread.start()
+
+
AppKit.NSApp.runModalForWindow_(panel)
+
return XAColor(panel.color())
+
+
+
+
+[docs]class XADialog(XAObject):
+
"""A custom dialog window.
+
+
.. versionadded:: 0.0.8
+
"""
+
[docs] def __init__(self, text: str = "", title: Union[str, None] = None, buttons: Union[None, list[Union[str, int]]] = None, hidden_answer: bool = False, default_button: Union[str, int, None] = None, cancel_button: Union[str, int, None] = None, icon: Union[Literal["stop", "note", "caution"], None] = None, default_answer: Union[str, int, None] = None):
+
super().__init__()
+
self.text: str = text
+
self.title: str = title
+
self.buttons: Union[None, list[Union[str, int]]] = buttons or []
+
self.hidden_answer: bool = hidden_answer
+
self.icon: Union[str, None] = icon
+
self.default_button: Union[str, int, None] = default_button
+
self.cancel_button: Union[str, int, None] = cancel_button
+
self.default_answer: Union[str, int, None] = default_answer
+
+
[docs] def display(self) -> Union[str, int, None, list[str]]:
+
"""Displays the dialog, waits for the user to select an option or cancel, then returns the selected button or None if cancelled.
+
+
:return: The selected button or None if no value was selected
+
:rtype: Union[str, int, None, list[str]]
+
+
.. versionadded:: 0.0.8
+
"""
+
buttons = [x.replace("'", "") for x in self.buttons]
+
buttons = str(buttons).replace("'", '"')
+
+
default_button = str(self.default_button).replace("'", "")
+
default_button_str = "default button \"" + default_button + "\"" if self.default_button is not None else ""
+
+
cancel_button = str(self.cancel_button).replace("'", "")
+
cancel_button_str = "cancel button \"" + cancel_button + "\"" if self.cancel_button is not None else ""
+
+
icon_str = "with icon " + self.icon + "" if self.icon is not None else ""
+
+
default_answer = str(self.default_answer).replace("'", '"')
+
default_answer_str = "default answer \"" + default_answer + "\"" if self.default_answer is not None else ""
+
+
script = AppleScript(f"""
+
tell application "System Events"
+
display dialog \"{self.text}\" with title \"{self.title}\" buttons {buttons} {default_button_str} {cancel_button_str} {icon_str} {default_answer_str} hidden answer {self.hidden_answer}
+
end tell
+
""")
+
+
result = script.run()["event"]
+
if result is not None:
+
if result.numberOfItems() > 1:
+
return [result.descriptorAtIndex_(1).stringValue(), result.descriptorAtIndex_(2).stringValue()]
+
else:
+
return result.descriptorAtIndex_(1).stringValue()
+
+
+
+
+
+
+
+
+
+[docs]class XAFilePicker(XAObject):
+
"""A file selection window.
+
+
.. versionadded:: 0.0.8
+
"""
+
[docs] def __init__(self, prompt: str = "Choose File", types: list[str] = None, default_location: Union[str, None] = None, show_invisibles: bool = False, multiple_selections_allowed: bool = False, show_package_contents: bool = False):
+
super().__init__()
+
self.prompt: str = prompt #: The prompt to display in the dialog box
+
self.types: list[str] = types #: The file types/type identifiers to allow for selection
+
self.default_location: Union[str, None] = default_location #: The default file location
+
self.show_invisibles: bool = show_invisibles #: Whether invisible files and folders are shown
+
self.multiple_selections_allowed: bool = multiple_selections_allowed #: Whether the user can select multiple files
+
self.show_package_contents: bool = show_package_contents #: Whether to show the contents of packages
+
+
[docs] def display(self) -> Union[XAPath, None]:
+
"""Displays the file chooser, waits for the user to select a file or cancel, then returns the selected file URL or None if cancelled.
+
+
:return: The selected file URL or None if no file was selected
+
:rtype: Union[XAPath, None]
+
+
.. versionadded:: 0.0.8
+
"""
+
types = [x.replace("'", "") for x in self.types]
+
types = str(types).replace("'", '"')
+
types_str = "of type " + types if self.types is not None else ""
+
+
default_location_str = "default location \"" + self.default_location + "\"" if self.default_location is not None else ""
+
+
script = AppleScript(f"""
+
tell application "System Events"
+
choose file with prompt \"{self.prompt}\" {types_str}{default_location_str} invisibles {self.show_invisibles} multiple selections allowed {self.multiple_selections_allowed} showing package contents {self.show_package_contents}
+
end tell
+
""")
+
result = script.run()["event"]
+
+
if result is not None:
+
if self.multiple_selections_allowed:
+
values = []
+
for x in range(1, result.numberOfItems() + 1):
+
desc = result.descriptorAtIndex_(x)
+
values.append(XAPath(desc.fileURLValue()))
+
return values
+
else:
+
return XAPath(result.fileURLValue())
+
+
+
+
+[docs]class XAFolderPicker(XAObject):
+
"""A folder selection window.
+
+
.. versionadded:: 0.0.8
+
"""
+
[docs] def __init__(self, prompt: str = "Choose Folder", default_location: Union[str, None] = None, show_invisibles: bool = False, multiple_selections_allowed: bool = False, show_package_contents: bool = False):
+
super().__init__()
+
self.prompt: str = prompt #: The prompt to display in the dialog box
+
self.default_location: Union[str, None] = default_location #: The default folder location
+
self.show_invisibles: bool = show_invisibles #: Whether invisible files and folders are shown
+
self.multiple_selections_allowed: bool = multiple_selections_allowed #: Whether the user can select multiple folders
+
self.show_package_contents: bool = show_package_contents #: Whether to show the contents of packages
+
+
[docs] def display(self) -> Union[XAPath, None]:
+
"""Displays the folder chooser, waits for the user to select a folder or cancel, then returns the selected folder URL or None if cancelled.
+
+
:return: The selected folder URL or None if no folder was selected
+
:rtype: Union[XAPath, None]
+
+
.. versionadded:: 0.0.8
+
"""
+
+
default_location_str = "default location \"" + self.default_location + "\"" if self.default_location is not None else ""
+
+
script = AppleScript(f"""
+
tell application "System Events"
+
choose folder with prompt \"{self.prompt}\" {default_location_str} invisibles {self.show_invisibles} multiple selections allowed {self.multiple_selections_allowed} showing package contents {self.show_package_contents}
+
end tell
+
""")
+
result = script.run()["event"]
+
if result is not None:
+
if self.multiple_selections_allowed:
+
values = []
+
for x in range(1, result.numberOfItems() + 1):
+
desc = result.descriptorAtIndex_(x)
+
values.append(XAPath(desc.fileURLValue()))
+
return values
+
else:
+
return XAPath(result.fileURLValue())
+
+
+
+
+[docs]class XAApplicationPicker(XAObject):
+
"""An application selection window.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, title: Union[str, None] = None, prompt: Union[str, None] = None, multiple_selections_allowed: bool = False):
+
super().__init__()
+
self.title: str = title #: The dialog window title
+
self.prompt: str = prompt #: The prompt to be displayed in the dialog box
+
self.multiple_selections_allowed: bool = multiple_selections_allowed #: Whether to allow multiple items to be selected
+
+
[docs] def display(self) -> str:
+
"""Displays the application chooser, waits for the user to select an application or cancel, then returns the selected application's name or None if cancelled.
+
+
:return: The name of the selected application
+
:rtype: str
+
+
.. versionadded:: 0.0.8
+
"""
+
+
script = AppleScript("tell application \"System Events\"")
+
dialog_str = "choose application "
+
if self.title is not None:
+
dialog_str += f"with title \"{self.title}\" "
+
if self.prompt is not None:
+
dialog_str += f"with prompt \"{self.prompt}\""
+
dialog_str += f"multiple selections allowed {self.multiple_selections_allowed} "
+
script.add(dialog_str)
+
script.add("end tell")
+
+
return script.run()["string"]
+
+
+
+
+
+
+
+
+
+#############################
+### System / Image Events ###
+#############################
+# ? Move into separate XAFileSystemBase.py file?
+[docs]class XAEventsApplication(XACanOpenPath):
+
"""A base class for the System and Image events applications.
+
+
.. versionadded:: 0.1.0
+
"""
+
+
+[docs]class XADiskItemList(XAList):
+
"""A wrapper around lists of disk items that employs fast enumeration techniques.
+
+
All properties of disk items can be called as methods on the wrapped list, returning a list containing each item's value for the property.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None, object_class = None):
+
if object_class is None:
+
object_class = XADiskItem
+
super().__init__(properties, object_class, filter)
+
+
[docs] def busy_status(self) -> list['bool']:
+
return list(self.xa_elem.arrayByApplyingSelector_("busyStatus") or [])
+
+
[docs] def container(self) -> 'XADiskItemList':
+
ls = self.xa_elem.arrayByApplyingSelector_("container") or []
+
return self._new_element(ls, XADiskItemList)
+
+
[docs] def creation_date(self) -> list['datetime']:
+
return list(self.xa_elem.arrayByApplyingSelector_("creationDate") or [])
+
+
[docs] def displayed_name(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("displayedName") or [])
+
+
[docs] def id(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("id") or [])
+
+
[docs] def modification_date(self) -> list['datetime']:
+
return list(self.xa_elem.arrayByApplyingSelector_("modificationDate") or [])
+
+
[docs] def name(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("name") or [])
+
+
[docs] def name_extension(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("nameExtension") or [])
+
+
[docs] def package_folder(self) -> list['bool']:
+
return list(self.xa_elem.arrayByApplyingSelector_("packageFolder") or [])
+
+
[docs] def path(self) -> list['XAPath']:
+
ls = self.xa_elem.arrayByApplyingSelector_("path") or []
+
return [XAPath(x) for x in ls]
+
+
[docs] def physical_size(self) -> list['int']:
+
return list(self.xa_elem.arrayByApplyingSelector_("physicalSize") or [])
+
+
[docs] def posix_path(self) -> list[XAPath]:
+
ls = self.xa_elem.arrayByApplyingSelector_("POSIXPath") or []
+
return [XAPath(x) for x in ls]
+
+
[docs] def size(self) -> list['int']:
+
return list(self.xa_elem.arrayByApplyingSelector_("size") or [])
+
+
[docs] def url(self) -> list['XAURL']:
+
ls = self.xa_elem.arrayByApplyingSelector_("URL") or []
+
return [XAURL(x) for x in ls]
+
+
[docs] def visible(self) -> list['bool']:
+
return list(self.xa_elem.arrayByApplyingSelector_("visible") or [])
+
+
[docs] def volume(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("volume") or [])
+
+
[docs] def by_busy_status(self, busy_status: bool) -> Union['XADiskItem', None]:
+
return self.by_property("busyStatus", busy_status)
+
+
[docs] def by_container(self, container: 'XADiskItem') -> Union['XADiskItem', None]:
+
return self.by_property("container", container.xa_elem)
+
+
[docs] def by_creation_date(self, creation_date: datetime) -> Union['XADiskItem', None]:
+
return self.by_property("creationDate", creation_date)
+
+
[docs] def by_displayed_name(self, displayed_name: str) -> Union['XADiskItem', None]:
+
return self.by_property("displayedName", displayed_name)
+
+
[docs] def by_id(self, id: str) -> Union['XADiskItem', None]:
+
return self.by_property("id", id)
+
+
[docs] def by_modification_date(self, modification_date: datetime) -> Union['XADiskItem', None]:
+
return self.by_property("modificationDate", modification_date)
+
+
[docs] def by_name(self, name: str) -> Union['XADiskItem', None]:
+
return self.by_property("name", name)
+
+
[docs] def by_name_extension(self, name_extension: str) -> Union['XADiskItem', None]:
+
return self.by_property("nameExtension", name_extension)
+
+
[docs] def by_package_folder(self, package_folder: bool) -> Union['XADiskItem', None]:
+
return self.by_property("packageFolder", package_folder)
+
+
[docs] def by_path(self, path: Union[XAPath, str]) -> Union['XADiskItem', None]:
+
if isinstance(path, XAPath):
+
path = path.path
+
return self.by_property("path", path)
+
+
[docs] def by_physical_size(self, physical_size: int) -> Union['XADiskItem', None]:
+
return self.by_property("physicalSize", physical_size)
+
+
[docs] def by_posix_path(self, posix_path: Union[XAPath, str]) -> Union['XADiskItem', None]:
+
if isinstance(posix_path, XAPath):
+
posix_path = posix_path.path
+
return self.by_property("POSIXPath", posix_path)
+
+
[docs] def by_size(self, size: int) -> Union['XADiskItem', None]:
+
return self.by_property("size", size)
+
+
[docs] def by_url(self, url: XAURL) -> Union['XADiskItem', None]:
+
return self.by_property("URL", url.xa_elem)
+
+
[docs] def by_visible(self, visible: bool) -> Union['XADiskItem', None]:
+
return self.by_property("visible", visible)
+
+
[docs] def by_volume(self, volume: str) -> Union['XADiskItem', None]:
+
return self.by_property("volume", volume)
+
+
[docs] def __repr__(self):
+
return "<" + str(type(self)) + str(self.name()) + ">"
+
+[docs]class XADiskItem(XAObject, XAPathLike):
+
"""An item stored in the file system.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
@property
+
def busy_status(self) -> 'bool':
+
"""Whether the disk item is busy.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.busyStatus()
+
+
@property
+
def container(self) -> 'XADiskItem':
+
"""The folder or disk which has this disk item as an element.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.container(), XADiskItem)
+
+
@property
+
def creation_date(self) -> 'datetime':
+
"""The date on which the disk item was created.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.creationDate()
+
+
@property
+
def displayed_name(self) -> 'str':
+
"""The name of the disk item as displayed in the User Interface.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.displayedName()
+
+
@property
+
def id(self) -> 'str':
+
"""The unique ID of the disk item.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.id()
+
+
@property
+
def modification_date(self) -> 'datetime':
+
"""The date on which the disk item was last modified.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.modificationDate()
+
+
@property
+
def name(self) -> 'str':
+
"""The name of the disk item.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.name()
+
+
@property
+
def name_extension(self) -> 'str':
+
"""The extension portion of the name.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.nameExtension()
+
+
@property
+
def package_folder(self) -> 'bool':
+
"""Whether the disk item is a package.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.packageFolder()
+
+
@property
+
def path(self) -> 'XAPath':
+
"""The file system path of the disk item.
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAPath(self.xa_elem.path())
+
+
@property
+
def physical_size(self) -> 'int':
+
"""The actual space used by the disk item on disk.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.physicalSize()
+
+
@property
+
def posix_path(self) -> XAPath:
+
"""The POSIX file system path of the disk item.
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAPath(self.xa_elem.POSIXPath())
+
+
@property
+
def size(self) -> 'int':
+
"""The logical size of the disk item.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.size()
+
+
@property
+
def url(self) -> 'XAURL':
+
"""The URL of the disk item.
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAURL(self.xa_elem.URL())
+
+
@property
+
def visible(self) -> 'bool':
+
"""Whether the disk item is visible.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.visible()
+
+
@property
+
def volume(self) -> 'str':
+
"""The volume on which the disk item resides.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.volume()
+
+
[docs] def open(self) -> Self:
+
"""Opens the item in its default application.
+
+
+
:return: The item object
+
:rtype: Self
+
+
.. versionadded:: 0.1.1
+
"""
+
self.xa_elem.open()
+
return self
+
+
[docs] def get_path_representation(self) -> XAPath:
+
return self.posix_path
+
+
[docs] def __repr__(self):
+
return "<" + str(type(self)) + str(self.name) + ">"
+
+
+
+
+[docs]class XAAliasList(XADiskItemList):
+
"""A wrapper around lists of aliases that employs fast enumeration techniques.
+
+
All properties of aliases can be called as methods on the wrapped list, returning a list containing each alias' value for the property.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, filter, XAAlias)
+
+
[docs] def creator_type(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("creatorType") or [])
+
+
[docs] def default_application(self) -> 'XADiskItemList':
+
ls = self.xa_elem.arrayByApplyingSelector_("defaultApplication") or []
+
return self._new_element(ls, XADiskItemList)
+
+
[docs] def file_type(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("fileType") or [])
+
+
[docs] def kind(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("kind") or [])
+
+
[docs] def product_version(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("productVersion") or [])
+
+
[docs] def short_version(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("shortVersion") or [])
+
+
[docs] def stationery(self) -> list['bool']:
+
return list(self.xa_elem.arrayByApplyingSelector_("stationery") or [])
+
+
[docs] def type_identifier(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("typeIdentifier") or [])
+
+
[docs] def version(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("version") or [])
+
+
[docs] def by_creator_type(self, creator_type: str) -> Union['XAAlias', None]:
+
return self.by_property("creatorType", creator_type)
+
+
[docs] def by_default_application(self, default_application: 'XADiskItem') -> Union['XAAlias', None]:
+
return self.by_property("defaultApplication", default_application.xa_elem)
+
+
[docs] def by_file_type(self, file_type: str) -> Union['XAAlias', None]:
+
return self.by_property("fileType", file_type)
+
+
[docs] def by_kind(self, kind: str) -> Union['XAAlias', None]:
+
return self.by_property("kind", kind)
+
+
[docs] def by_product_version(self, product_version: str) -> Union['XAAlias', None]:
+
return self.by_property("productVersion", product_version)
+
+
[docs] def by_short_version(self, short_version: str) -> Union['XAAlias', None]:
+
return self.by_property("shortVersion", short_version)
+
+
[docs] def by_stationery(self, stationery: bool) -> Union['XAAlias', None]:
+
return self.by_property("stationery", stationery)
+
+
[docs] def by_type_identifier(self, type_identifier: str) -> Union['XAAlias', None]:
+
return self.by_property("typeIdentifier", type_identifier)
+
+
[docs] def by_version(self, version: str) -> Union['XAAlias', None]:
+
return self.by_property("version", version)
+
+[docs]class XAAlias(XADiskItem):
+
"""An alias in the file system.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
@property
+
def creator_type(self) -> 'str':
+
"""The OSType identifying the application that created the alias.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.creatorType()
+
+
@property
+
def default_application(self) -> 'XADiskItem':
+
"""The application that will launch if the alias is opened.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.defaultApplication(), XADiskItem)
+
+
@property
+
def file_type(self) -> 'str':
+
"""The OSType identifying the type of data contained in the alias.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.fileType()
+
+
@property
+
def kind(self) -> 'str':
+
"""The kind of alias, as shown in Finder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.kind()
+
+
@property
+
def product_version(self) -> 'str':
+
"""The version of the product (visible at the top of the "Get Info" window).
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.productVersion()
+
+
@property
+
def short_version(self) -> 'str':
+
"""The short version of the application bundle referenced by the alias.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.shortVersion()
+
+
@property
+
def stationery(self) -> 'bool':
+
"""Whether the alias is a stationery pad.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.stationery()
+
+
@property
+
def type_identifier(self) -> 'str':
+
"""The type identifier of the alias.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.typeIdentifier()
+
+
@property
+
def version(self) -> 'str':
+
"""The version of the application bundle referenced by the alias (visible at the bottom of the "Get Info" window).
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.version()
+
+
[docs] def aliases(self, filter: Union[dict, None] = None) -> 'XAAliasList':
+
"""Returns a list of aliases, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.aliases(), XAAliasList, filter)
+
+
[docs] def disk_items(self, filter: Union[dict, None] = None) -> 'XADiskItemList':
+
"""Returns a list of disk items, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.diskItems(), XADiskItemList, filter)
+
+
[docs] def files(self, filter: Union[dict, None] = None) -> 'XAFileList':
+
"""Returns a list of files, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.files(), XAFileList, filter)
+
+
[docs] def file_packages(self, filter: Union[dict, None] = None) -> 'XAFilePackageList':
+
"""Returns a list of file packages, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.filePackages(), XAFilePackageList, filter)
+
+
[docs] def folders(self, filter: Union[dict, None] = None) -> 'XAFolderList':
+
"""Returns a list of folders, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.folders(), XAFolderList, filter)
+
+
+
+
+[docs]class XADiskList(XADiskItemList):
+
"""A wrapper around lists of disks that employs fast enumeration techniques.
+
+
All properties of disks can be called as methods on the wrapped list, returning a list containing each disk's value for the property.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, filter, XADisk)
+
+
[docs] def capacity(self) -> list['float']:
+
return list(self.xa_elem.arrayByApplyingSelector_("capacity") or [])
+
+
[docs] def ejectable(self) -> list['bool']:
+
return list(self.xa_elem.arrayByApplyingSelector_("ejectable") or [])
+
+
+
+
[docs] def free_space(self) -> list['float']:
+
return list(self.xa_elem.arrayByApplyingSelector_("freeSpace") or [])
+
+
[docs] def ignore_privileges(self) -> list['bool']:
+
return list(self.xa_elem.arrayByApplyingSelector_("ignorePrivileges") or [])
+
+
[docs] def local_volume(self) -> list['bool']:
+
return list(self.xa_elem.arrayByApplyingSelector_("localVolume") or [])
+
+
[docs] def server(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("server") or [])
+
+
[docs] def startup(self) -> list['bool']:
+
return list(self.xa_elem.arrayByApplyingSelector_("startup") or [])
+
+
[docs] def zone(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("zone") or [])
+
+
[docs] def by_capacity(self, capacity: float) -> Union['XADisk', None]:
+
return self.by_property("capacity", capacity)
+
+
[docs] def by_ejectable(self, ejectable: bool) -> Union['XADisk', None]:
+
return self.by_property("ejectable", ejectable)
+
+
+
+
[docs] def by_free_space(self, free_space: float) -> Union['XADisk', None]:
+
return self.by_property("freeSpace", free_space)
+
+
[docs] def by_ignore_privileges(self, ignore_privileges: bool) -> Union['XADisk', None]:
+
return self.by_property("ignorePrivileges", ignore_privileges)
+
+
[docs] def by_local_volume(self, local_volume: bool) -> Union['XADisk', None]:
+
return self.by_property("localVolume", local_volume)
+
+
[docs] def by_server(self, server: str) -> Union['XADisk', None]:
+
return self.by_property("server", server)
+
+
[docs] def by_startup(self, startup: bool) -> Union['XADisk', None]:
+
return self.by_property("startup", startup)
+
+
[docs] def by_zone(self, zone: str) -> Union['XADisk', None]:
+
return self.by_property("zone", zone)
+
+[docs]class XADisk(XADiskItem):
+
"""A disk in the file system.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
@property
+
def capacity(self) -> 'float':
+
"""The total number of bytes (free or used) on the disk.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.capacity()
+
+
@property
+
def ejectable(self) -> 'bool':
+
"""Whether the media can be ejected (floppies, CD's, and so on).
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.ejectable()
+
+
@property
+
def format(self) -> 'XAEventsApplication.Format':
+
"""The file system format of the disk.
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAEventsApplication.Format(self.xa_elem.format())
+
+
@property
+
def free_space(self) -> 'float':
+
"""The number of free bytes left on the disk.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.freeSpace()
+
+
@property
+
def ignore_privileges(self) -> 'bool':
+
"""Whether to ignore permissions on this disk.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.ignorePrivileges()
+
+
@property
+
def local_volume(self) -> 'bool':
+
"""Whether the media is a local volume (as opposed to a file server).
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.localVolume()
+
+
@property
+
def server(self) -> 'str':
+
"""The server on which the disk resides, AFP volumes only.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.server()
+
+
@property
+
def startup(self) -> 'bool':
+
"""Whether this disk is the boot disk.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.startup()
+
+
@property
+
def zone(self) -> 'str':
+
"""The zone in which the disk's server resides, AFP volumes only.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.zone()
+
+
[docs] def aliases(self, filter: Union[dict, None] = None) -> 'XAAliasList':
+
"""Returns a list of aliases, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.aliases(), XAAliasList, filter)
+
+
[docs] def disk_items(self, filter: Union[dict, None] = None) -> 'XADiskItemList':
+
"""Returns a list of disk items, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.diskItems(), XADiskItemList, filter)
+
+
[docs] def files(self, filter: Union[dict, None] = None) -> 'XAFileList':
+
"""Returns a list of files, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.files(), XAFileList, filter)
+
+
[docs] def file_packages(self, filter: Union[dict, None] = None) -> 'XAFilePackageList':
+
"""Returns a list of file packages, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.fileOackages(), XAFilePackageList, filter)
+
+
[docs] def folders(self, filter: Union[dict, None] = None) -> 'XAFolderList':
+
"""Returns a list of folders, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.folders(), XAFolderList, filter)
+
+
+
+
+[docs]class XADomainList(XAList):
+
"""A wrapper around lists of domains that employs fast enumeration techniques.
+
+
All properties of domains can be called as methods on the wrapped list, returning a list containing each domain's value for the property.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, XADomain, filter)
+
+
[docs] def id(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("id") or [])
+
+
[docs] def name(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("name") or [])
+
+
[docs] def by_id(self, id: str) -> Union['XADomain', None]:
+
return self.by_property("id", id)
+
+
[docs] def by_name(self, name: str) -> Union['XADomain', None]:
+
return self.by_property("name", name)
+
+
[docs] def __repr__(self):
+
return "<" + str(type(self)) + str(self.name()) + ">"
+
+[docs]class XADomain(XAObject):
+
"""A domain in the file system.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
@property
+
def application_support_folder(self) -> 'XAFolder':
+
"""The Application Support folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.applicationSupportFolder(), XAFolder)
+
+
@property
+
def applications_folder(self) -> 'XAFolder':
+
"""The Applications folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.applicationsFolder(), XAFolder)
+
+
@property
+
def desktop_pictures_folder(self) -> 'XAFolder':
+
"""The Desktop Pictures folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.desktopPicturesFolder(), XAFolder)
+
+
@property
+
def folder_action_scripts_folder(self) -> 'XAFolder':
+
"""The Folder Action Scripts folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.folderActionScriptsFolder(), XAFolder)
+
+
@property
+
def fonts_folder(self) -> 'XAFolder':
+
"""The Fonts folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.fontsFolder(), XAFolder)
+
+
@property
+
def id(self) -> 'str':
+
"""The unique identifier of the domain.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.id()
+
+
@property
+
def library_folder(self) -> 'XAFolder':
+
"""The Library folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.libraryFolder(), XAFolder)
+
+
@property
+
def name(self) -> 'str':
+
"""The name of the domain.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.name()
+
+
@property
+
def preferences_folder(self) -> 'XAFolder':
+
"""The Preferences folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.preferencesFolder(), XAFolder)
+
+
@property
+
def scripting_additions_folder(self) -> 'XAFolder':
+
"""The Scripting Additions folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.scriptingAdditionsFolder(), XAFolder)
+
+
@property
+
def scripts_folder(self) -> 'XAFolder':
+
"""The Scripts folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.scriptsFolder(), XAFolder)
+
+
@property
+
def shared_documents_folder(self) -> 'XAFolder':
+
"""The Shared Documents folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.sharedDocumentsFolder(), XAFolder)
+
+
@property
+
def speakable_items_folder(self) -> 'XAFolder':
+
"""The Speakable Items folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.speakableItemsFolder(), XAFolder)
+
+
@property
+
def utilities_folder(self) -> 'XAFolder':
+
"""The Utilities folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.utilitiesFolder(), XAFolder)
+
+
@property
+
def workflows_folder(self) -> 'XAFolder':
+
"""The Automator Workflows folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.workflowsFolder(), XAFolder)
+
+
[docs] def folders(self, filter: Union[dict, None] = None) -> 'XAFolderList':
+
"""Returns a list of folders, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.folders(), XAFolderList, filter)
+
+
[docs] def __repr__(self):
+
return "<" + str(type(self)) + str(self.name) + ">"
+
+
+
+
+[docs]class XAClassicDomainObject(XADomain):
+
"""The Classic domain in the file system.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
@property
+
def apple_menu_folder(self) -> 'XAFolder':
+
"""The Apple Menu Items folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.appleMenuFolder(), XAFolder)
+
+
@property
+
def control_panels_folder(self) -> 'XAFolder':
+
"""The Control Panels folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.controlPanelsFolder(), XAFolder)
+
+
@property
+
def control_strip_modules_folder(self) -> 'XAFolder':
+
"""The Control Strip Modules folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.controlStripModulesFolder(), XAFolder)
+
+
@property
+
def desktop_folder(self) -> 'XAFolder':
+
"""The Classic Desktop folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.desktopFolder(), XAFolder)
+
+
@property
+
def extensions_folder(self) -> 'XAFolder':
+
"""The Extensions folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.extensionsFolder(), XAFolder)
+
+
@property
+
def fonts_folder(self) -> 'XAFolder':
+
"""The Fonts folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.fontsFolder(), XAFolder)
+
+
@property
+
def launcher_items_folder(self) -> 'XAFolder':
+
"""The Launcher Items folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.launcherItemsFolder(), XAFolder)
+
+
@property
+
def preferences_folder(self) -> 'XAFolder':
+
"""The Classic Preferences folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.preferencesFolder(), XAFolder)
+
+
@property
+
def shutdown_folder(self) -> 'XAFolder':
+
"""The Shutdown Items folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.shutdownFolder(), XAFolder)
+
+
@property
+
def startup_items_folder(self) -> 'XAFolder':
+
"""The StartupItems folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.startupItemsFolder(), XAFolder)
+
+
@property
+
def system_folder(self) -> 'XAFolder':
+
"""The System folder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.systemFolder(), XAFolder)
+
+
[docs] def folders(self, filter: Union[dict, None] = None) -> 'XAFolderList':
+
"""Returns a list of folders, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.folders(), XAFolderList, filter)
+
+
+
+
+[docs]class XAFileList(XADiskItemList):
+
"""A wrapper around lists of files that employs fast enumeration techniques.
+
+
All properties of files can be called as methods on the wrapped list, returning a list containing each file's value for the property.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None, object_class = None):
+
if object_class is None:
+
object_class = XAFile
+
super().__init__(properties, filter, object_class)
+
+
[docs] def creator_type(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("creatorType") or [])
+
+
[docs] def default_application(self) -> 'XADiskItemList':
+
ls = self.xa_elem.arrayByApplyingSelector_("defaultApplication") or []
+
return self._new_element(ls, XADiskItemList)
+
+
[docs] def file_type(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("fileType") or [])
+
+
[docs] def kind(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("kind") or [])
+
+
[docs] def product_version(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("productVersion") or [])
+
+
[docs] def short_version(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("shortVersion") or [])
+
+
[docs] def stationery(self) -> list['bool']:
+
return list(self.xa_elem.arrayByApplyingSelector_("stationery") or [])
+
+
[docs] def type_identifier(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("typeIdentifier") or [])
+
+
[docs] def version(self) -> list['str']:
+
return list(self.xa_elem.arrayByApplyingSelector_("version") or [])
+
+
[docs] def by_creator_type(self, creator_type: str) -> Union['XAFile', None]:
+
return self.by_property("creatorType", creator_type)
+
+
[docs] def by_default_application(self, default_application: 'XADiskItem') -> Union['XAFile', None]:
+
return self.by_property("defaultApplication", default_application.xa_elem)
+
+
[docs] def by_file_type(self, file_type: str) -> Union['XAFile', None]:
+
return self.by_property("fileType", file_type)
+
+
[docs] def by_kind(self, kind: str) -> Union['XAFile', None]:
+
return self.by_property("kind", kind)
+
+
[docs] def by_product_version(self, product_version: str) -> Union['XAFile', None]:
+
return self.by_property("productVersion", product_version)
+
+
[docs] def by_short_version(self, short_version: str) -> Union['XAFile', None]:
+
return self.by_property("shortVersion", short_version)
+
+
[docs] def by_stationery(self, stationery: bool) -> Union['XAFile', None]:
+
return self.by_property("stationery", stationery)
+
+
[docs] def by_type_identifier(self, type_identifier: str) -> Union['XAFile', None]:
+
return self.by_property("typeIdentifier", type_identifier)
+
+
[docs] def by_version(self, version: str) -> Union['XAFile', None]:
+
return self.by_property("version", version)
+
+[docs]class XAFile(XADiskItem):
+
"""A file in the file system.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
@property
+
def creator_type(self) -> 'str':
+
"""The OSType identifying the application that created the file.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.creatorType()
+
+
@property
+
def default_application(self) -> 'XADiskItem':
+
"""The application that will launch if the file is opened.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.defaultApplication(), XADiskItem)
+
+
@default_application.setter
+
def default_application(self, default_application: XADiskItem):
+
self.set_property('defaultApplication', default_application.xa_elem)
+
+
@property
+
def file_type(self) -> 'str':
+
"""The OSType identifying the type of data contained in the file.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.fileType()
+
+
@property
+
def kind(self) -> 'str':
+
"""The kind of file, as shown in Finder.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.kind()
+
+
@property
+
def product_version(self) -> 'str':
+
"""The version of the product (visible at the top of the "Get Info" window).
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.productVersion()
+
+
@property
+
def short_version(self) -> 'str':
+
"""The short version of the file.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.shortVersion()
+
+
@property
+
def stationery(self) -> 'bool':
+
"""Whether the file is a stationery pad.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.stationery()
+
+
@property
+
def type_identifier(self) -> 'str':
+
"""The type identifier of the file.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.typeIdentifier()
+
+
@property
+
def version(self) -> 'str':
+
"""The version of the file (visible at the bottom of the "Get Info" window).
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.version()
+
+
+
+
+[docs]class XAFilePackageList(XAFileList):
+
"""A wrapper around lists of file packages that employs fast enumeration techniques.
+
+
All properties of file packages can be called as methods on the wrapped list, returning a list containing each package's value for the property.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, filter, XAFilePackage)
+
+[docs]class XAFilePackage(XAFile):
+
"""A file package in the file system.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
[docs] def aliases(self, filter: Union[dict, None] = None) -> 'XAAliasList':
+
"""Returns a list of aliases, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.aliases(), XAAliasList, filter)
+
+
[docs] def disk_items(self, filter: Union[dict, None] = None) -> 'XADiskItemList':
+
"""Returns a list of disk items, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.diskItems(), XADiskItemList, filter)
+
+
[docs] def files(self, filter: Union[dict, None] = None) -> 'XAFileList':
+
"""Returns a list of files, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.files(), XAFileList, filter)
+
+
[docs] def file_packages(self, filter: Union[dict, None] = None) -> 'XAFilePackageList':
+
"""Returns a list of file packages, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.filePackages(), XAFilePackageList, filter)
+
+
[docs] def folders(self, filter: Union[dict, None] = None) -> 'XAFolderList':
+
"""Returns a list of folders, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.folders(), XAFolderList, filter)
+
+
+
+
+[docs]class XAFolderList(XADiskItemList):
+
"""A wrapper around lists of folders that employs fast enumeration techniques.
+
+
All properties of folders can be called as methods on the wrapped list, returning a list containing each folder's value for the property.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, filter, XAFolder)
+
+[docs]class XAFolder(XADiskItem):
+
"""A folder in the file system.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
[docs] def aliases(self, filter: Union[dict, None] = None) -> 'XAAliasList':
+
"""Returns a list of aliases, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.aliases(), XAAliasList, filter)
+
+
[docs] def disk_items(self, filter: Union[dict, None] = None) -> 'XADiskItemList':
+
"""Returns a list of disk items, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.diskItems(), XADiskItemList, filter)
+
+
[docs] def files(self, filter: Union[dict, None] = None) -> 'XAFileList':
+
"""Returns a list of files, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.files(), XAFileList, filter)
+
+
[docs] def file_packages(self, filter: Union[dict, None] = None) -> 'XAFilePackageList':
+
"""Returns a list of file packages, as PyXA objects, matching the given filter.
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.filePackages(), XAFilePackageList, filter)
+
+
[docs] def folders(self, filter: Union[dict, None] = None) -> 'XAFolderList':
+
"""Returns a list of folders, as PyXA objects, matching the given filter.
+
+
:param filter: A dictionary specifying property-value pairs that all returned folders will have, or None
+
:type filter: Union[dict, None]
+
:return: The list of folders
+
:rtype: XAFolderList
+
+
.. versionadded:: 0.1.0
+
"""
+
self._new_element(self.xa_elem.folders(), XAFolderList, filter)
+
+
+
+
+[docs]class XALocalDomainObject(XADomain):
+
"""The local domain in the file system.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
+
+
+[docs]class XANetworkDomainObject(XADomain):
+
"""The network domain in the file system.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
+
+
+[docs]class XASystemDomainObject(XADomain):
+
"""The system domain in the file system.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
+
+
+[docs]class XAUserDomainObject(XADomain):
+
"""The user domain in the file system.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties):
+
super().__init__(properties)
+
+
@property
+
def desktop_folder(self) -> 'XAFolder':
+
"""The user's Desktop folder
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.desktopFolder(), XAFolder)
+
+
@property
+
def documents_folder(self) -> 'XAFolder':
+
"""The user's Documents folder
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.documentsFolder(), XAFolder)
+
+
@property
+
def downloads_folder(self) -> 'XAFolder':
+
"""The user's Downloads folder
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.downloadsFolder(), XAFolder)
+
+
@property
+
def favorites_folder(self) -> 'XAFolder':
+
"""The user's Favorites folder
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.favoritesFolder(), XAFolder)
+
+
@property
+
def home_folder(self) -> 'XAFolder':
+
"""The user's Home folder
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.homeFolder(), XAFolder)
+
+
@property
+
def movies_folder(self) -> 'XAFolder':
+
"""The user's Movies folder
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.moviesFolder(), XAFolder)
+
+
@property
+
def music_folder(self) -> 'XAFolder':
+
"""The user's Music folder
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.musicFolder(), XAFolder)
+
+
@property
+
def pictures_folder(self) -> 'XAFolder':
+
"""The user's Pictures folder
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.picturesFolder(), XAFolder)
+
+
@property
+
def public_folder(self) -> 'XAFolder':
+
"""The user's Public folder
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.publicFolder(), XAFolder)
+
+
@property
+
def sites_folder(self) -> 'XAFolder':
+
"""The user's Sites folder
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.sitesFolder(), XAFolder)
+
+
@property
+
def temporary_items_folder(self) -> 'XAFolder':
+
"""The Temporary Items folder
+
+
.. versionadded:: 0.1.0
+
"""
+
return self._new_element(self.xa_elem.temporaryItemsFolder(), XAFolder)
+
+
+
+
+#############
+### Media ###
+#############
+[docs]class XAImageList(XAList, XAClipboardCodable):
+
"""A wrapper around lists of images that employs fast enumeration techniques.
+
+
.. versionadded:: 0.0.3
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None, obj_class = None):
+
if obj_class is None:
+
obj_class = XAImage
+
super().__init__(properties, obj_class, filter)
+
+
self.modified = False #: Whether the list of images has been modified since it was initialized
+
+
def __partial_init(self):
+
images = [None] * self.xa_elem.count()
+
+
def init_images(ref, index, stop):
+
if isinstance(ref, str):
+
ref = AppKit.NSImage.alloc().initWithContentsOfURL_(XAPath(ref).xa_elem)
+
elif isinstance(ref, ScriptingBridge.SBObject):
+
ref = AppKit.NSImage.alloc().initWithContentsOfURL_(XAPath(ref.imageFile().POSIXPath()).xa_elem)
+
elif isinstance(ref, XAObject):
+
ref = AppKit.NSImage.alloc().initWithContentsOfURL_(ref.image_file.posix_path.xa_elem)
+
images[index] = ref
+
+
self.xa_elem.enumerateObjectsUsingBlock_(init_images)
+
return AppKit.NSMutableArray.alloc().initWithArray_(images)
+
+
def __apply_filter(self, filter_block, *args):
+
images = self.__partial_init()
+
+
filtered_images = [None] * images.count()
+
def filter_image(image, index, *args):
+
img = Quartz.CIImage.imageWithCGImage_(image.CGImage())
+
filter = filter_block(image, *args)
+
filter.setValue_forKey_(img, "inputImage")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
+
# Crop the result to the original image size
+
cropped = uncropped.imageByCroppingToRect_(Quartz.CGRectMake(0, 0, image.size().width * 2, image.size().height * 2))
+
+
# Convert back to NSImage
+
rep = AppKit.NSCIImageRep.imageRepWithCIImage_(cropped)
+
result = AppKit.NSImage.alloc().initWithSize_(rep.size())
+
result.addRepresentation_(rep)
+
filtered_images[index] = result
+
+
threads = [None] * images.count()
+
for index, image in enumerate(images):
+
threads[index] = self._spawn_thread(filter_image, [image, index, *args])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(filtered_images)
+
return self
+
+
[docs] def file(self) -> list[XAPath]:
+
return [x.file for x in self]
+
+
[docs] def horizontal_stitch(self) -> 'XAImage':
+
"""Horizontally stacks each image in the list.
+
+
The first image in the list is placed at the left side of the resulting image.
+
+
:return: The resulting image after stitching
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAImage.horizontal_stitch(self)
+
+
[docs] def vertical_stitch(self) -> 'XAImage':
+
"""Vertically stacks each image in the list.
+
+
The first image in the list is placed at the bottom side of the resulting image.
+
+
:return: The resulting image after stitching
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
return XAImage.vertical_stitch(self)
+
+
[docs] def additive_composition(self) -> 'XAImage':
+
"""Creates a composition image by adding the color values of each image in the list.
+
+
:param images: The images to add together
+
:type images: list[XAImage]
+
:return: The resulting image composition
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image_data = [None] * self.xa_elem.count()
+
for index, image in enumerate(self.xa_elem):
+
if isinstance(image, str):
+
image = AppKit.NSImage.alloc().initWithContentsOfURL_(XAPath(image).xa_elem)
+
image_data[index] = Quartz.CIImage.imageWithData_(image.TIFFRepresentation())
+
+
current_composition = None
+
while len(image_data) > 1:
+
img1 = image_data.pop(0)
+
img2 = image_data.pop(0)
+
composition_filter = Quartz.CIFilter.filterWithName_("CIAdditionCompositing")
+
composition_filter.setDefaults()
+
composition_filter.setValue_forKey_(img1, "inputImage")
+
composition_filter.setValue_forKey_(img2, "inputBackgroundImage")
+
current_composition = composition_filter.outputImage()
+
image_data.insert(0, current_composition)
+
+
composition_rep = AppKit.NSCIImageRep.imageRepWithCIImage_(current_composition)
+
composition = AppKit.NSImage.alloc().initWithSize_(composition_rep.size())
+
composition.addRepresentation_(composition_rep)
+
return XAImage(composition)
+
+
[docs] def subtractive_composition(self) -> 'XAImage':
+
"""Creates a composition image by subtracting the color values of each image in the list successively.
+
+
:param images: The images to create the composition from
+
:type images: list[XAImage]
+
:return: The resulting image composition
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image_data = [None] * self.xa_elem.count()
+
for index, image in enumerate(self.xa_elem):
+
if isinstance(image, str):
+
image = AppKit.NSImage.alloc().initWithContentsOfURL_(XAPath(image).xa_elem)
+
image_data[index] = Quartz.CIImage.imageWithData_(image.TIFFRepresentation())
+
+
current_composition = None
+
while len(image_data) > 1:
+
img1 = image_data.pop(0)
+
img2 = image_data.pop(0)
+
composition_filter = Quartz.CIFilter.filterWithName_("CISubtractBlendMode")
+
composition_filter.setDefaults()
+
composition_filter.setValue_forKey_(img1, "inputImage")
+
composition_filter.setValue_forKey_(img2, "inputBackgroundImage")
+
current_composition = composition_filter.outputImage()
+
image_data.insert(0, current_composition)
+
+
composition_rep = AppKit.NSCIImageRep.imageRepWithCIImage_(current_composition)
+
composition = AppKit.NSImage.alloc().initWithSize_(composition_rep.size())
+
composition.addRepresentation_(composition_rep)
+
return XAImage(composition)
+
+
[docs] def edges(self, intensity: float = 1.0) -> 'XAImageList':
+
"""Detects the edges in each image of the list and highlights them colorfully, blackening other areas of the images.
+
+
:param intensity: The degree to which edges are highlighted. Higher is brighter. Defaults to 1.0
+
:type intensity: float
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
def filter_block(image, intensity):
+
filter = Quartz.CIFilter.filterWithName_("CIEdges")
+
filter.setDefaults()
+
filter.setValue_forKey_(intensity, "inputIntensity")
+
return filter
+
+
return self.__apply_filter(filter_block, intensity)
+
+
[docs] def gaussian_blur(self, intensity: float = 10) -> 'XAImageList':
+
"""Blurs each image in the list using a Gaussian filter.
+
+
:param intensity: The strength of the blur effect, defaults to 10
+
:type intensity: float
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
def filter_block(image, intensity):
+
filter = Quartz.CIFilter.filterWithName_("CIGaussianBlur")
+
filter.setDefaults()
+
filter.setValue_forKey_(intensity, "inputRadius")
+
return filter
+
+
return self.__apply_filter(filter_block, intensity)
+
+
[docs] def reduce_noise(self, noise_level: float = 0.02, sharpness: float = 0.4) -> 'XAImageList':
+
"""Reduces noise in each image of the list by sharpening areas with a luminance delta below the specified noise level threshold.
+
+
:param noise_level: The threshold for luminance changes in an area below which will be considered noise, defaults to 0.02
+
:type noise_level: float
+
:param sharpness: The sharpness of the resulting images, defaults to 0.4
+
:type sharpness: float
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
def filter_block(image, noise_level, sharpness):
+
filter = Quartz.CIFilter.filterWithName_("CINoiseReduction")
+
filter.setDefaults()
+
filter.setValue_forKey_(noise_level, "inputNoiseLevel")
+
filter.setValue_forKey_(sharpness, "inputSharpness")
+
return filter
+
+
return self.__apply_filter(filter_block, noise_level, sharpness)
+
+
[docs] def pixellate(self, pixel_size: float = 8.0) -> 'XAImageList':
+
"""Pixellates each image in the list.
+
+
:param pixel_size: The size of the pixels, defaults to 8.0
+
:type pixel_size: float
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
def filter_block(image, pixel_size):
+
filter = Quartz.CIFilter.filterWithName_("CIPixellate")
+
filter.setDefaults()
+
filter.setValue_forKey_(pixel_size, "inputScale")
+
return filter
+
+
return self.__apply_filter(filter_block, pixel_size)
+
+
[docs] def outline(self, threshold: float = 0.1) -> 'XAImageList':
+
"""Outlines detected edges within each image of the list in black, leaving the rest transparent.
+
+
:param threshold: The threshold to use when separating edge and non-edge pixels. Larger values produce thinner edge lines. Defaults to 0.1
+
:type threshold: float
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
def filter_block(image, threshold):
+
filter = Quartz.CIFilter.filterWithName_("CILineOverlay")
+
filter.setDefaults()
+
filter.setValue_forKey_(threshold, "inputThreshold")
+
return filter
+
+
return self.__apply_filter(filter_block, threshold)
+
+
[docs] def invert(self) -> 'XAImageList':
+
"""Inverts the colors of each image in the list.
+
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
def filter_block(image):
+
filter = Quartz.CIFilter.filterWithName_("CIColorInvert")
+
filter.setDefaults()
+
return filter
+
+
return self.__apply_filter(filter_block)
+
+
[docs] def sepia(self, intensity: float = 1.0) -> 'XAImageList':
+
"""Applies a sepia filter to each image in the list; maps all colors of the images to shades of brown.
+
+
:param intensity: The opacity of the sepia effect. A value of 0 will have no impact on the image. Defaults to 1.0
+
:type intensity: float
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
def filter_block(image, intensity):
+
filter = Quartz.CIFilter.filterWithName_("CISepiaTone")
+
filter.setDefaults()
+
filter.setValue_forKey_(intensity, "inputIntensity")
+
return filter
+
+
return self.__apply_filter(filter_block, intensity)
+
+
[docs] def vignette(self, intensity: float = 1.0) -> 'XAImageList':
+
"""Applies vignette shading to the corners of each image in the list.
+
+
:param intensity: The intensity of the vignette effect, defaults to 1.0
+
:type intensity: float
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
def filter_block(image, intensity):
+
filter = Quartz.CIFilter.filterWithName_("CIVignette")
+
filter.setDefaults()
+
filter.setValue_forKey_(intensity, "inputIntensity")
+
return filter
+
+
return self.__apply_filter(filter_block, intensity)
+
+
[docs] def depth_of_field(self, focal_region: Union[tuple[tuple[int, int], tuple[int, int]], None] = None, intensity: float = 10.0, focal_region_saturation: float = 1.5) -> 'XAImageList':
+
"""Applies a depth of field filter to each image in the list, simulating a tilt & shift effect.
+
+
:param focal_region: Two points defining a line within each image to focus the effect around (pixels around the line will be in focus), or None to use the center third of the image, defaults to None
+
:type focal_region: Union[tuple[tuple[int, int], tuple[int, int]], None]
+
:param intensity: Controls the amount of distance around the focal region to keep in focus. Higher values decrease the distance before the out-of-focus effect starts. Defaults to 10.0
+
:type intensity: float
+
:param focal_region_saturation: Adjusts the saturation of the focial region. Higher values increase saturation. Defaults to 1.5 (1.5x default saturation)
+
:type focal_region_saturation: float
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
def filter_block(image, focal_region, intensity, focal_region_saturation):
+
if focal_region is None:
+
center_top = Quartz.CIVector.vectorWithX_Y_(image.size().width / 2, image.size().height / 3)
+
center_bottom = Quartz.CIVector.vectorWithX_Y_(image.size().width / 2, image.size().height / 3 * 2)
+
focal_region = (center_top, center_bottom)
+
else:
+
point1 = Quartz.CIVector.vectorWithX_Y_(focal_region[0])
+
point2 = Quartz.CIVector.vectorWithX_Y_(focal_region[1])
+
focal_region = (point1, point2)
+
+
filter = Quartz.CIFilter.filterWithName_("CIDepthOfField")
+
filter.setDefaults()
+
filter.setValue_forKey_(focal_region[0], "inputPoint0")
+
filter.setValue_forKey_(focal_region[1], "inputPoint1")
+
filter.setValue_forKey_(intensity, "inputRadius")
+
filter.setValue_forKey_(focal_region_saturation, "inputSaturation")
+
return filter
+
+
return self.__apply_filter(filter_block, focal_region, intensity, focal_region_saturation)
+
+
[docs] def crystallize(self, crystal_size: float = 20.0) -> 'XAImageList':
+
"""Applies a crystallization filter to each image in the list. Creates polygon-shaped color blocks by aggregating pixel values.
+
+
:param crystal_size: The radius of the crystals, defaults to 20.0
+
:type crystal_size: float
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
def filter_block(image, crystal_size):
+
filter = Quartz.CIFilter.filterWithName_("CICrystallize")
+
filter.setDefaults()
+
filter.setValue_forKey_(crystal_size, "inputRadius")
+
return filter
+
+
return self.__apply_filter(filter_block, crystal_size)
+
+
[docs] def comic(self) -> 'XAImageList':
+
"""Applies a comic filter to each image in the list. Outlines edges and applies a color halftone effect.
+
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
def filter_block(image):
+
filter = Quartz.CIFilter.filterWithName_("CIComicEffect")
+
filter.setDefaults()
+
return filter
+
+
return self.__apply_filter(filter_block)
+
+
[docs] def pointillize(self, point_size: float = 20.0) -> 'XAImageList':
+
"""Applies a pointillization filter to each image in the list.
+
+
:param crystal_size: The radius of the points, defaults to 20.0
+
:type crystal_size: float
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
def filter_block(image, point_size):
+
filter = Quartz.CIFilter.filterWithName_("CIPointillize")
+
filter.setDefaults()
+
filter.setValue_forKey_(point_size, "inputRadius")
+
return filter
+
+
return self.__apply_filter(filter_block, point_size)
+
+
[docs] def bloom(self, intensity: float = 0.5) -> 'XAImageList':
+
"""Applies a bloom effect to each image in the list. Softens edges and adds a glow.
+
+
:param intensity: The strength of the softening and glow effects, defaults to 0.5
+
:type intensity: float
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
def filter_block(image, intensity):
+
filter = Quartz.CIFilter.filterWithName_("CIBloom")
+
filter.setDefaults()
+
filter.setValue_forKey_(intensity, "inputIntensity")
+
return filter
+
+
return self.__apply_filter(filter_block, intensity)
+
+
[docs] def monochrome(self, color: XAColor, intensity: float = 1.0) -> 'XAImageList':
+
"""Remaps the colors of each image in the list to shades of the specified color.
+
+
:param color: The color of map each images colors to
+
:type color: XAColor
+
:param intensity: The strength of recoloring effect. Higher values map colors to darker shades of the provided color. Defaults to 1.0
+
:type intensity: float
+
:return: The resulting images after applying the filter
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
ci_color = Quartz.CIColor.alloc().initWithColor_(color.xa_elem)
+
+
def filter_block(image, intensity):
+
filter = Quartz.CIFilter.filterWithName_("CIColorMonochrome")
+
filter.setDefaults()
+
filter.setValue_forKey_(ci_color, "inputColor")
+
filter.setValue_forKey_(intensity, "inputIntensity")
+
return filter
+
+
return self.__apply_filter(filter_block, intensity)
+
+
[docs] def bump(self, center: Union[tuple[int, int], None] = None, radius: float = 300.0, curvature: float = 0.5) -> 'XAImageList':
+
"""Adds a concave (inward) or convex (outward) bump to each image in the list at the specified location within each image.
+
+
:param center: The center point of the effect, or None to use the center of the image, defaults to None
+
:type center: Union[tuple[int, int], None]
+
:param radius: The radius of the bump in pixels, defaults to 300.0
+
:type radius: float
+
:param curvature: Controls the direction and intensity of the bump's curvature. Positive values create convex bumps while negative values create concave bumps. Defaults to 0.5
+
:type curvature: float
+
:return: The resulting images after applying the distortion
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
images = self.__partial_init()
+
+
bumped_images = [None] * images.count()
+
def bump_image(image, index, center, radius, curvature):
+
if center is None:
+
center = Quartz.CIVector.vectorWithX_Y_(image.size().width / 2, image.size().height / 2)
+
else:
+
center = Quartz.CIVector.vectorWithX_Y_(center[0], center[1])
+
+
img = Quartz.CIImage.imageWithCGImage_(image.CGImage())
+
filter = Quartz.CIFilter.filterWithName_("CIBumpDistortion")
+
filter.setDefaults()
+
filter.setValue_forKey_(img, "inputImage")
+
filter.setValue_forKey_(center, "inputCenter")
+
filter.setValue_forKey_(radius, "inputRadius")
+
filter.setValue_forKey_(curvature, "inputScale")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
+
# Crop the result to the original image size
+
cropped = uncropped.imageByCroppingToRect_(Quartz.CGRectMake(0, 0, image.size().width * 2, image.size().height * 2))
+
+
# Convert back to NSImage
+
rep = AppKit.NSCIImageRep.imageRepWithCIImage_(cropped)
+
result = AppKit.NSImage.alloc().initWithSize_(rep.size())
+
result.addRepresentation_(rep)
+
bumped_images[index] = result
+
+
threads = [None] * images.count()
+
for index, image in enumerate(images):
+
threads[index] = self._spawn_thread(bump_image, [image, index, center, radius, curvature])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(bumped_images)
+
return self
+
+
[docs] def pinch(self, center: Union[tuple[int, int], None] = None, intensity: float = 0.5) -> 'XAImageList':
+
"""Adds an inward pinch distortion to each image in the list at the specified location within each image.
+
+
:param center: The center point of the effect, or None to use the center of the image, defaults to None
+
:type center: Union[tuple[int, int], None]
+
:param intensity: Controls the scale of the pinch effect. Higher values stretch pixels away from the specified center to a greater degree. Defaults to 0.5
+
:type intensity: float
+
:return: The resulting images after applying the distortion
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
images = self.__partial_init()
+
+
pinched_images = [None] * images.count()
+
def pinch_image(image, index, center, intensity):
+
if center is None:
+
center = Quartz.CIVector.vectorWithX_Y_(image.size().width / 2, image.size().height / 2)
+
else:
+
center = Quartz.CIVector.vectorWithX_Y_(center[0], center[1])
+
+
img = Quartz.CIImage.imageWithCGImage_(image.CGImage())
+
filter = Quartz.CIFilter.filterWithName_("CIPinchDistortion")
+
filter.setDefaults()
+
filter.setValue_forKey_(img, "inputImage")
+
filter.setValue_forKey_(center, "inputCenter")
+
filter.setValue_forKey_(intensity, "inputScale")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
+
# Crop the result to the original image size
+
cropped = uncropped.imageByCroppingToRect_(Quartz.CGRectMake(0, 0, image.size().width * 2, image.size().height * 2))
+
+
# Convert back to NSImage
+
rep = AppKit.NSCIImageRep.imageRepWithCIImage_(cropped)
+
result = AppKit.NSImage.alloc().initWithSize_(rep.size())
+
result.addRepresentation_(rep)
+
pinched_images[index] = result
+
+
threads = [None] * images.count()
+
for index, image in enumerate(images):
+
threads[index] = self._spawn_thread(pinch_image, [image, index, center, intensity])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(pinched_images)
+
return self
+
+
[docs] def twirl(self, center: Union[tuple[int, int], None] = None, radius: float = 300.0, angle: float = 3.14) -> 'XAImageList':
+
"""Adds a twirl distortion to each image in the list by rotating pixels around the specified location within each image.
+
+
:param center: The center point of the effect, or None to use the center of the image, defaults to None
+
:type center: Union[tuple[int, int], None]
+
:param radius: The pixel radius around the centerpoint that defines the area to apply the effect to, defaults to 300.0
+
:type radius: float
+
:param angle: The angle of the twirl in radians, defaults to 3.14
+
:type angle: float
+
:return: The resulting images after applying the distortion
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
images = self.__partial_init()
+
+
twirled_images = [None] * images.count()
+
def twirl_image(image, index, center, radius, angle):
+
if center is None:
+
center = Quartz.CIVector.vectorWithX_Y_(image.size().width / 2, image.size().height / 2)
+
else:
+
center = Quartz.CIVector.vectorWithX_Y_(center[0], center[1])
+
+
img = Quartz.CIImage.imageWithCGImage_(image.CGImage())
+
filter = Quartz.CIFilter.filterWithName_("CITwirlDistortion")
+
filter.setDefaults()
+
filter.setValue_forKey_(img, "inputImage")
+
filter.setValue_forKey_(center, "inputCenter")
+
filter.setValue_forKey_(radius, "inputRadius")
+
filter.setValue_forKey_(angle, "inputAngle")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
+
# Crop the result to the original image size
+
cropped = uncropped.imageByCroppingToRect_(Quartz.CGRectMake(0, 0, image.size().width * 2, image.size().height * 2))
+
+
# Convert back to NSImage
+
rep = AppKit.NSCIImageRep.imageRepWithCIImage_(cropped)
+
result = AppKit.NSImage.alloc().initWithSize_(rep.size())
+
result.addRepresentation_(rep)
+
twirled_images[index] = result
+
+
threads = [None] * images.count()
+
for index, image in enumerate(images):
+
threads[index] = self._spawn_thread(twirl_image, [image, index, center, radius, angle])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(twirled_images)
+
return self
+
+
[docs] def auto_enhance(self, correct_red_eye: bool = False, crop_to_features: bool = False, correct_rotation: bool = False) -> 'XAImageList':
+
"""Attempts to enhance each image in the list by applying suggested filters.
+
+
:param correct_red_eye: Whether to attempt red eye removal, defaults to False
+
:type correct_red_eye: bool, optional
+
:param crop_to_features: Whether to crop the images to focus on their main features, defaults to False
+
:type crop_to_features: bool, optional
+
:param correct_rotation: Whether attempt perspective correction by rotating the images, defaults to False
+
:type correct_rotation: bool, optional
+
:return: The list of enhanced images
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
images = self.__partial_init()
+
+
enhanced_images = [None] * images.count()
+
def enhance_image(image, index):
+
ci_image = Quartz.CIImage.imageWithCGImage_(image.CGImage())
+
+
options = {
+
Quartz.kCIImageAutoAdjustRedEye: correct_red_eye,
+
Quartz.kCIImageAutoAdjustCrop: crop_to_features,
+
Quartz.kCIImageAutoAdjustLevel: correct_rotation
+
}
+
+
enhancements = ci_image.autoAdjustmentFiltersWithOptions_(options)
+
for filter in enhancements:
+
filter.setValue_forKey_(ci_image, "inputImage")
+
ci_image = filter.outputImage()
+
+
# Crop the result to the original image size
+
cropped = ci_image.imageByCroppingToRect_(Quartz.CGRectMake(0, 0, image.size().width * 2, image.size().height * 2))
+
+
# Convert back to NSImage
+
rep = AppKit.NSCIImageRep.imageRepWithCIImage_(cropped)
+
result = AppKit.NSImage.alloc().initWithSize_(rep.size())
+
result.addRepresentation_(rep)
+
enhanced_images[index] = result
+
+
threads = [None] * images.count()
+
for index, image in enumerate(images):
+
threads[index] = self._spawn_thread(enhance_image, [image, index])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(enhanced_images)
+
return self
+
+
[docs] def flip_horizontally(self) -> 'XAImageList':
+
"""Flips each image in the list horizontally.
+
+
:return: The list of flipped images
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
images = self.__partial_init()
+
+
flipped_images = [None] * images.count()
+
def flip_image(image, index):
+
flipped_image = AppKit.NSImage.alloc().initWithSize_(image.size())
+
imageBounds = AppKit.NSMakeRect(0, 0, image.size().width, image.size().height)
+
+
transform = AppKit.NSAffineTransform.alloc().init()
+
transform.translateXBy_yBy_(image.size().width, 0)
+
transform.scaleXBy_yBy_(-1, 1)
+
+
flipped_image.lockFocus()
+
transform.concat()
+
image.drawInRect_fromRect_operation_fraction_(imageBounds, Quartz.CGRectZero, AppKit.NSCompositingOperationCopy, 1.0)
+
flipped_image.unlockFocus()
+
flipped_images[index] = flipped_image
+
+
threads = [None] * images.count()
+
for index, image in enumerate(images):
+
threads[index] = self._spawn_thread(flip_image, [image, index])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(flipped_images)
+
return self
+
+
[docs] def flip_vertically(self) -> 'XAImageList':
+
"""Flips each image in the list vertically.
+
+
:return: The list of flipped images
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
images = self.__partial_init()
+
+
flipped_images = [None] * images.count()
+
def flip_image(image, index):
+
flipped_image = AppKit.NSImage.alloc().initWithSize_(image.size())
+
imageBounds = AppKit.NSMakeRect(0, 0, image.size().width, image.size().height)
+
+
transform = AppKit.NSAffineTransform.alloc().init()
+
transform.translateXBy_yBy_(0, image.size().height)
+
transform.scaleXBy_yBy_(1, -1)
+
+
flipped_image.lockFocus()
+
transform.concat()
+
image.drawInRect_fromRect_operation_fraction_(imageBounds, Quartz.CGRectZero, AppKit.NSCompositingOperationCopy, 1.0)
+
flipped_image.unlockFocus()
+
flipped_images[index] = flipped_image
+
+
threads = [None] * images.count()
+
for index, image in enumerate(images):
+
threads[index] = self._spawn_thread(flip_image, [image, index])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(flipped_images)
+
return self
+
+
[docs] def rotate(self, degrees: float) -> 'XAImageList':
+
"""Rotates each image in the list by the specified amount of degrees.
+
+
:param degrees: The number of degrees to rotate the images by
+
:type degrees: float
+
:return: The list of rotated images
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
sinDegrees = abs(math.sin(degrees * math.pi / 180.0))
+
cosDegrees = abs(math.cos(degrees * math.pi / 180.0))
+
+
images = self.__partial_init()
+
+
rotated_images = [None] * images.count()
+
def rotate_image(image, index):
+
new_size = Quartz.CGSizeMake(image.size().height * sinDegrees + image.size().width * cosDegrees, image.size().width * sinDegrees + image.size().height * cosDegrees)
+
rotated_image = AppKit.NSImage.alloc().initWithSize_(new_size)
+
+
imageBounds = Quartz.CGRectMake((new_size.width - image.size().width) / 2, (new_size.height - image.size().height) / 2, image.size().width, image.size().height)
+
+
transform = AppKit.NSAffineTransform.alloc().init()
+
transform.translateXBy_yBy_(new_size.width / 2, new_size.height / 2)
+
transform.rotateByDegrees_(degrees)
+
transform.translateXBy_yBy_(-new_size.width / 2, -new_size.height / 2)
+
+
rotated_image.lockFocus()
+
transform.concat()
+
image.drawInRect_fromRect_operation_fraction_(imageBounds, Quartz.CGRectZero, AppKit.NSCompositingOperationCopy, 1.0)
+
rotated_image.unlockFocus()
+
+
rotated_images[index] = rotated_image
+
+
threads = [None] * images.count()
+
for index, image in enumerate(images):
+
threads[index] = self._spawn_thread(rotate_image, [image, index])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(rotated_images)
+
return self
+
+
[docs] def crop(self, size: tuple[int, int], corner: Union[tuple[int, int], None] = None) -> 'XAImageList':
+
"""Crops each image in the list to the specified dimensions.
+
+
:param size: The dimensions to crop each image to
+
:type size: tuple[int, int]
+
:param corner: The bottom-left location to crom each image from, or None to use (0, 0), defaults to None
+
:type corner: Union[tuple[int, int], None]
+
:return: The list of cropped images
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
if corner is None:
+
# No corner provided -- use (0,0) by default
+
corner = (0, 0)
+
+
images = self.__partial_init()
+
+
cropped_images = [None] * images.count()
+
def crop_image(image, index):
+
cropped_image = AppKit.NSImage.alloc().initWithSize_(AppKit.NSMakeSize(size[0], size[1]))
+
imageBounds = AppKit.NSMakeRect(corner[0], corner[1], image.size().width, image.size().height)
+
+
cropped_image.lockFocus()
+
image.drawInRect_(imageBounds)
+
cropped_image.unlockFocus()
+
cropped_images[index] = cropped_image
+
+
threads = [None] * images.count()
+
for index, image in enumerate(images):
+
threads[index] = self._spawn_thread(crop_image, [image, index])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(cropped_images)
+
return self
+
+
[docs] def scale(self, scale_factor_x: float, scale_factor_y: Union[float, None] = None) -> 'XAImageList':
+
"""Scales each image in the list by the specified horizontal and vertical factors.
+
+
:param scale_factor_x: The factor by which to scale each image in the X dimension
+
:type scale_factor_x: float
+
:param scale_factor_y: The factor by which to scale each image in the Y dimension, or None to match the horizontal factor, defaults to None
+
:type scale_factor_y: Union[float, None]
+
:return: The list of scaled images
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
if scale_factor_y is None:
+
scale_factor_y = scale_factor_x
+
+
images = self.__partial_init()
+
+
scaled_images = [None] * self.xa_elem.count()
+
def scale_image(image, index):
+
scaled_image = AppKit.NSImage.alloc().initWithSize_(AppKit.NSMakeSize(image.size().width * scale_factor_x, image.size().height * scale_factor_y))
+
imageBounds = AppKit.NSMakeRect(0, 0, image.size().width, image.size().height)
+
+
transform = AppKit.NSAffineTransform.alloc().init()
+
transform.scaleXBy_yBy_(scale_factor_x, scale_factor_y)
+
+
scaled_image.lockFocus()
+
transform.concat()
+
image.drawInRect_fromRect_operation_fraction_(imageBounds, Quartz.CGRectZero, AppKit.NSCompositingOperationCopy, 1.0)
+
scaled_image.unlockFocus()
+
scaled_images[index] = scaled_image
+
+
threads = [None] * self.xa_elem.count()
+
for index, image in enumerate(images):
+
threads[index] = self._spawn_thread(scale_image, [image, index])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(scaled_images)
+
return self
+
+
[docs] def resize(self, width: int, height: Union[int, None] = None) -> Self:
+
"""Resizes each image in the list to the specified width and height.
+
+
:param width: The width of the resulting images
+
:type width: int
+
:param height: The height of the resulting images, or None to maintain width:height proportions, defaults to None
+
:type height: Union[int, None]
+
:return: The list of scaled images
+
:rtype: Self
+
+
.. versionadded:: 0.1.1
+
"""
+
images = self.__partial_init()
+
+
scaled_images = [None] * self.xa_elem.count()
+
def scale_image(image, index):
+
nonlocal height
+
img_width = image.size().width
+
img_height = image.size().height
+
+
if height is None:
+
height = img_height
+
+
scaled_image = AppKit.NSImage.alloc().initWithSize_(AppKit.NSMakeSize(img_width * width / img_width, img_height * height / img_height))
+
imageBounds = AppKit.NSMakeRect(0, 0, image.size().width, image.size().height)
+
+
transform = AppKit.NSAffineTransform.alloc().init()
+
transform.scaleXBy_yBy_(width / img_width, height / img_height)
+
+
scaled_image.lockFocus()
+
transform.concat()
+
image.drawInRect_fromRect_operation_fraction_(imageBounds, Quartz.CGRectZero, AppKit.NSCompositingOperationCopy, 1.0)
+
scaled_image.unlockFocus()
+
scaled_images[index] = scaled_image
+
+
threads = [None] * self.xa_elem.count()
+
for index, image in enumerate(images):
+
threads[index] = self._spawn_thread(scale_image, [image, index])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(scaled_images)
+
return self
+
+
[docs] def pad(self, horizontal_border_width: int = 50, vertical_border_width: int = 50, pad_color: Union[XAColor, None] = None) -> 'XAImageList':
+
"""Pads each image in the list with the specified color; add a border around each image in the list with the specified vertical and horizontal width.
+
+
:param horizontal_border_width: The border width, in pixels, in the x-dimension, defaults to 50
+
:type horizontal_border_width: int
+
:param vertical_border_width: The border width, in pixels, in the y-dimension, defaults to 50
+
:type vertical_border_width: int
+
:param pad_color: The color of the border, or None for a white border, defaults to None
+
:type pad_color: Union[XAColor, None]
+
:return: The list of padded images
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
if pad_color is None:
+
# No color provided -- use white by default
+
pad_color = XAColor.white()
+
+
images = self.__partial_init()
+
+
padded_images = [None] * images.count()
+
def pad_image(image, index):
+
new_width = image.size().width + horizontal_border_width * 2
+
new_height = image.size().height + vertical_border_width * 2
+
color_swatch = pad_color.make_swatch(new_width, new_height)
+
+
color_swatch.xa_elem.lockFocus()
+
bounds = AppKit.NSMakeRect(horizontal_border_width, vertical_border_width, image.size().width, image.size().height)
+
image.drawInRect_(bounds)
+
color_swatch.xa_elem.unlockFocus()
+
padded_images[index] = color_swatch.xa_elem
+
+
threads = [None] * images.count()
+
for index, image in enumerate(images):
+
threads[index] = self._spawn_thread(pad_image, [image, index])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(padded_images)
+
return self
+
+
[docs] def overlay_image(self, image: 'XAImage', location: Union[tuple[int, int], None] = None, size: Union[tuple[int, int], None] = None) -> 'XAImageList':
+
"""Overlays an image on top of each image in the list, at the specified location, with the specified size.
+
+
:param image: The image to overlay on top of each image in the list
+
:type image: XAImage
+
:param location: The bottom-left point of the overlaid image in the results, or None to use the bottom-left point of each background image, defaults to None
+
:type location: Union[tuple[int, int], None]
+
:param size: The width and height of the overlaid image, or None to use the overlaid's images existing width and height, or (-1, -1) to use the dimensions of each background images, defaults to None
+
:type size: Union[tuple[int, int], None]
+
:return: The list of images with the specified image overlaid on top of them
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
if location is None:
+
# No location provided -- use the bottom-left point of the background image by default
+
location = (0, 0)
+
+
images = self.__partial_init()
+
overlayed_images = [None] * images.count()
+
def overlay_image(img, index, image, size, location):
+
if size is None:
+
# No dimensions provided -- use size of overlay image by default
+
size = image.size
+
elif size == (-1, -1):
+
# Use remaining width/height of background image
+
size = (img.size().width - location[0], img.size().height - location[1])
+
elif size[0] == -1:
+
# Use remaining width of background image + provided height
+
size = (img.size().width - location[0], size[1])
+
elif size[1] == -1:
+
# Use remaining height of background image + provided width
+
size = (size[1], img.size().width - location[1])
+
+
img.lockFocus()
+
bounds = AppKit.NSMakeRect(location[0], location[1], size[0], size[1])
+
image.xa_elem.drawInRect_(bounds)
+
img.unlockFocus()
+
overlayed_images[index] = img
+
+
threads = [None] * images.count()
+
for index, img in enumerate(images):
+
threads[index] = self._spawn_thread(overlay_image, [img, index, image, size, location])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(overlayed_images)
+
return self
+
+
[docs] def overlay_text(self, text: str, location: Union[tuple[int, int], None] = None, font_size: float = 12, font_color: Union[XAColor, None] = None) -> 'XAImageList':
+
"""Overlays text of the specified size and color at the provided location within each image of the list.
+
+
:param text: The text to overlay onto each image of the list
+
:type text: str
+
:param location: The bottom-left point of the start of the text, or None to use (5, 5), defaults to None
+
:type location: Union[tuple[int, int], None]
+
:param font_size: The font size, in pixels, of the text, defaults to 12
+
:type font_size: float
+
:param font_color: The color of the text, or None to use black, defaults to None
+
:type font_color: XAColor
+
:return: The list of images with the specified text overlaid on top of them
+
:rtype: XAImageList
+
+
.. versionadded:: 0.1.0
+
"""
+
if location is None:
+
# No location provided -- use (5, 5) by default
+
location = (5, 5)
+
+
if font_color is None:
+
# No color provided -- use black by default
+
font_color = XAColor.black()
+
+
font = AppKit.NSFont.userFontOfSize_(font_size)
+
images = self.__partial_init()
+
overlayed_images = [None] * self.xa_elem.count()
+
def overlay_text(image, index):
+
textRect = Quartz.CGRectMake(location[0], 0, image.size().width - location[0], location[1])
+
attributes = {
+
AppKit.NSFontAttributeName: font,
+
AppKit.NSForegroundColorAttributeName: font_color.xa_elem
+
}
+
+
image.lockFocus()
+
AppKit.NSString.alloc().initWithString_(text).drawInRect_withAttributes_(textRect, attributes)
+
image.unlockFocus()
+
overlayed_images[index] = image
+
+
threads = [None] * self.xa_elem.count()
+
for index, image in enumerate(images):
+
threads[index] = self._spawn_thread(overlay_text, [image, index])
+
+
while any([t.is_alive() for t in threads]):
+
time.sleep(0.01)
+
+
self.modified = True
+
self.xa_elem = AppKit.NSMutableArray.alloc().initWithArray_(overlayed_images)
+
return self
+
+
+
+
[docs] def show_in_preview(self):
+
"""Opens each image in the list in Preview.
+
+
.. versionadded:: 0.1.0
+
"""
+
for image in self:
+
image.show_in_preview()
+
+
[docs] def save(self, file_paths: list[Union[XAPath, str]]):
+
"""Saves each image to a file on the disk.
+
+
:param file_path: The path at which to save the image file. Any existing file at that location will be overwritten, defaults to None
+
:type file_path: Union[XAPath, str, None]
+
+
.. versionadded:: 0.1.0
+
"""
+
for index, image in enumerate(self):
+
path = None
+
if len(file_paths) > index:
+
path = file_paths[index]
+
image.save(path)
+
+
[docs] def get_clipboard_representation(self) -> list['AppKit.NSImage']:
+
"""Gets a clipboard-codable representation of each image in the list.
+
+
When the clipboard content is set to a list of image, the raw data of each image is added to the clipboard. You can then
+
+
:return: A list of media item file URLs
+
:rtype: list[NSURL]
+
+
.. versionadded:: 0.0.8
+
"""
+
data = []
+
for image in self.__partial_init():
+
if image.TIFFRepresentation():
+
data.append(image)
+
return data
+
+[docs]class XAImage(XAObject, XAClipboardCodable):
+
"""A wrapper around NSImage with specialized automation methods.
+
+
.. versionadded:: 0.0.2
+
"""
+
+
[docs] def __init__(self, image_reference: Union[str, XAPath, 'AppKit.NSURL', 'AppKit.NSImage', None] = None, data: Union['AppKit.NSData', None] = None):
+
self.size: tuple[int, int] #: The dimensions of the image
+
self.file: Union[XAPath, None] = None #: The path to the image file, if one exists
+
self.data: str #: The TIFF representation of the image
+
self.modified: bool = False #: Whether the image data has been modified since the object was originally created
+
+
self.xa_elem = None
+
+
self.__vibrance = None
+
self.__gamma = None
+
self.__tint = None
+
self.__temperature = None
+
self.__white_point = None
+
self.__highlight = None
+
self.__shadow = None
+
+
if data is not None:
+
# Deprecated as of 0.1.0 -- Pass data as the image_reference instead
+
AppKit.NSLog("Warning: Setting the data parameter when initalizing an XAImage is deprecated functionality and will be removed in a future release")
+
self.xa_elem = AppKit.NSImage.alloc().initWithData_(data)
+
else:
+
self.file = image_reference
+
match image_reference:
+
case None:
+
self.xa_elem = AppKit.NSImage.alloc().init()
+
+
case {"element": str(ref)}:
+
self.file = ref
+
self.xa_elem = XAImage(ref).xa_elem
+
+
case {"element": XAImage() as image}:
+
self.file = image.file
+
self.xa_elem = image.xa_elem
+
+
case {"element": AppKit.NSImage() as image}:
+
self.xa_elem = image
+
+
case str() as ref if "://" in ref:
+
url = XAURL(ref).xa_elem
+
self.xa_elem = AppKit.NSImage.alloc().initWithContentsOfURL_(url)
+
+
case str() as ref if os.path.exists(ref) or os.path.exists(os.getcwd() + "/" + ref):
+
path = XAPath(ref).xa_elem
+
self.xa_elem = AppKit.NSImage.alloc().initWithContentsOfURL_(path)
+
+
case XAPath() as path:
+
self.file = path.path
+
self.xa_elem = AppKit.NSImage.alloc().initWithContentsOfURL_(path.xa_elem)
+
+
case XAURL() as url:
+
self.file = url.url
+
self.xa_elem = AppKit.NSImage.alloc().initWithContentsOfURL_(url.xa_elem)
+
+
case str() as raw_string:
+
font = AppKit.NSFont.monospacedSystemFontOfSize_weight_(15, AppKit.NSFontWeightMedium)
+
text = AppKit.NSString.alloc().initWithString_(raw_string)
+
attributes = {
+
AppKit.NSFontAttributeName: font,
+
AppKit.NSForegroundColorAttributeName: XAColor.black().xa_elem
+
}
+
text_size = text.sizeWithAttributes_(attributes)
+
+
# Make a white background to overlay the text on
+
swatch = XAColor.white().make_swatch(text_size.width + 20, text_size.height + 20)
+
text_rect = AppKit.NSMakeRect(10, 10, text_size.width, text_size.height)
+
+
# Overlay the text
+
swatch.xa_elem.lockFocus()
+
text.drawInRect_withAttributes_(text_rect, attributes)
+
swatch.xa_elem.unlockFocus()
+
self.xa_elem = swatch.xa_elem
+
+
case XAImage() as image:
+
self.file = image.file
+
self.xa_elem = image.xa_elem
+
+
case XAObject():
+
self.xa_elem = XAImage(image_reference.get_image_representation()).xa_elem
+
+
case AppKit.NSData() as data:
+
self.xa_elem = AppKit.NSImage.alloc().initWithData_(data)
+
+
case AppKit.NSImage() as image:
+
self.xa_elem = image
+
+
case _:
+
raise TypeError(f"Error: Cannot initialize XAImage using {type(image_reference)} type.")
+
+
def __update_image(self, modified_image: 'Quartz.CIImage') -> 'XAImage':
+
# Crop the result to the original image size
+
cropped = modified_image.imageByCroppingToRect_(Quartz.CGRectMake(0, 0, self.size[0] * 2, self.size[1] * 2))
+
+
# Convert back to NSImage
+
rep = AppKit.NSCIImageRep.imageRepWithCIImage_(cropped)
+
result = AppKit.NSImage.alloc().initWithSize_(rep.size())
+
result.addRepresentation_(rep)
+
+
# Update internal data
+
self.xa_elem = result
+
self.modified = True
+
return self
+
+
@property
+
def size(self) -> tuple[int, int]:
+
"""The dimensions of the image, in pixels.
+
+
.. versionadded:: 0.1.0
+
"""
+
return tuple(self.xa_elem.size())
+
+
@property
+
def data(self) -> 'AppKit.NSData':
+
return self.xa_elem.TIFFRepresentation()
+
+
@property
+
def has_alpha_channel(self) -> bool:
+
"""Whether the image has an alpha channel or not.
+
+
.. versionadded:: 0.1.0
+
"""
+
reps = self.xa_elem.representations()
+
if len(reps) > 0:
+
return reps[0].hasAlpha()
+
# TODO: Make sure this is never a false negative
+
return False
+
+
@property
+
def is_opaque(self) -> bool:
+
"""Whether the image contains transparent pixels or not.
+
+
.. versionadded:: 0.1.0
+
"""
+
reps = self.xa_elem.representations()
+
if len(reps) > 0:
+
return reps[0].isOpaque()
+
# TODO: Make sure this is never a false negative
+
return False
+
+
@property
+
def color_space_name(self) -> Union[str, None]:
+
"""The name of the color space that the image currently uses.
+
+
.. versionadded:: 0.1.0
+
"""
+
reps = self.xa_elem.representations()
+
if len(reps) > 0:
+
return reps[0].colorSpaceName()
+
# TODO: Make sure this is never a false negative
+
return None
+
+
@property
+
def gamma(self) -> float:
+
"""The gamma value for the image, once it has been manually set. Otherwise, the value is None.
+
+
.. versionadded:: 0.1.0
+
"""
+
if self.__gamma is not None:
+
return self.__gamma
+
return -1
+
+
@gamma.setter
+
def gamma(self, gamma: float):
+
self.__gamma = gamma
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIGammaAdjust")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(gamma, "inputPower")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
self.__update_image(uncropped)
+
+
@property
+
def vibrance(self) -> Union[float, None]:
+
"""The vibrance value for the image, once it has been manually set. Otherwise, the value is None.
+
+
.. versionadded:: 0.1.0
+
"""
+
if self.__vibrance is not None:
+
return self.__vibrance
+
return -1
+
+
@vibrance.setter
+
def vibrance(self, vibrance: float = 1):
+
self.__vibrance = vibrance
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIVibrance")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(vibrance, "inputAmount")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
@property
+
def tint(self) -> Union[float, None]:
+
"""The tint setting for the image, once it has been manually set. Otherwise, the value is None.
+
+
.. versionadded:: 0.1.0
+
"""
+
if self.__tint is not None:
+
return self.__tint
+
return -1
+
+
@tint.setter
+
def tint(self, tint: float):
+
# -100 to 100
+
temp_and_tint = Quartz.CIVector.vectorWithX_Y_(6500, tint)
+
self.__tint = tint
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CITemperatureAndTint")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(temp_and_tint, "inputTargetNeutral")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
self.__update_image(uncropped)
+
+
@property
+
def temperature(self) -> Union[float, None]:
+
"""The temperature setting for the image, once it has been manually set. Otherwise, the value is None.
+
+
.. versionadded:: 0.1.0
+
"""
+
if self.__temperature is not None:
+
return self.__temperature
+
return -1
+
+
@temperature.setter
+
def temperature(self, temperature: float):
+
# 2000 to inf
+
temp_and_tint = Quartz.CIVector.vectorWithX_Y_(temperature, 0)
+
self.__temperature = temperature
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CITemperatureAndTint")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(temp_and_tint, "inputTargetNeutral")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
self.__update_image(uncropped)
+
+
@property
+
def white_point(self) -> Union['XAColor', None]:
+
"""The white point setting for the image, once it has been manually set. Otherwise, the value is None.
+
+
.. versionadded:: 0.1.0
+
"""
+
if self.__white_point is not None:
+
return self.__white_point
+
return -1
+
+
@white_point.setter
+
def white_point(self, white_point: XAColor):
+
self.__white_point = white_point
+
ci_white_point = Quartz.CIColor.alloc().initWithColor_(white_point.xa_elem)
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIWhitePointAdjust")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(ci_white_point, "inputColor")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
self.__update_image(uncropped)
+
+
@property
+
def highlight(self) -> float:
+
"""The highlight setting for the image, once it has been manually set. Otherwise, the value is None.
+
+
.. versionadded:: 0.1.0
+
"""
+
if self.__highlight is not None:
+
return self.__highlight
+
return -1
+
+
@highlight.setter
+
def highlight(self, highlight: float):
+
self.__highlight = highlight
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIHighlightShadowAdjust")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(highlight, "inputHighlightAmount")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
self.__update_image(uncropped)
+
+
@property
+
def shadow(self) -> float:
+
"""The shadow setting for the image, once it has been manually set. Otherwise, the value is None.
+
+
.. versionadded:: 0.1.0
+
"""
+
if self.__shadow is not None:
+
return self.__shadow
+
return -1
+
+
@shadow.setter
+
def shadow(self, shadow: float):
+
self.__shadow = shadow
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIHighlightShadowAdjust")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(self.__highlight or 1, "inputHighlightAmount")
+
filter.setValue_forKey_(shadow, "inputShadowAmount")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
self.__update_image(uncropped)
+
+
[docs] def open(*images: Union[str, XAPath, list[Union[str, XAPath]]]) -> Union['XAImage', XAImageList]:
+
"""Initializes one or more images from files.
+
+
:param images: The image(s) to open
+
:type images: Union[str, XAPath, list[Union[str, XAPath]]]
+
:return: The newly created image object, or a list of image objects
+
:rtype: Union[XAImage, XAImageList]
+
+
.. versionadded:: 0.1.0
+
"""
+
if len(images) == 1:
+
images = images[0]
+
+
if isinstance(images, list) or isinstance(images, tuple):
+
return XAImageList({"element": images})
+
else:
+
return XAImage(images)
+
+
[docs] def symbol(name: str):
+
"""Initializes an image from the SF symbol with the specified name, if such a symbol exists.
+
+
:param name: The system symbol to create an image of; the name of an SF Symbol symbol.
+
:type name: str
+
+
.. versionadded:: 0.1.1
+
"""
+
img = AppKit.NSImage.imageWithSystemSymbolName_accessibilityDescription_(name, None)
+
return XAImage(img)
+
+
[docs] def horizontal_stitch(images: Union[list['XAImage'], XAImageList]) -> 'XAImage':
+
"""Horizontally stacks two or more images.
+
+
The first image in the list is placed at the left side of the resulting image.
+
+
:param images: The list of images to stitch together
+
:type images: Union[list[XAImage], XAImageList]
+
:return: The resulting image after stitching
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.1
+
"""
+
widths = [image.size[0] for image in images]
+
heights = [image.size[1] for image in images]
+
total_width = sum(widths)
+
max_height = max(heights)
+
+
canvas = AppKit.NSImage.alloc().initWithSize_(AppKit.NSMakeSize(total_width, max_height))
+
+
canvas.lockFocus()
+
current_x = 0
+
for image in images:
+
image.xa_elem.drawInRect_(AppKit.NSMakeRect(current_x, 0, image.size[0], image.size[1]))
+
current_x += image.size[0]
+
canvas.unlockFocus()
+
return XAImage(canvas)
+
+
[docs] def vertical_stitch(images: Union[list['XAImage'], XAImageList]) -> 'XAImage':
+
"""Vertically stacks two or more images.
+
+
The first image in the list is placed at the bottom of the resulting image.
+
+
:param images: The list of images to stitch together
+
:type images: Union[list[XAImage], XAImageList]
+
:return: The resulting image after stitching
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.1
+
"""
+
widths = [image.size[0] for image in images]
+
heights = [image.size[1] for image in images]
+
max_width = max(widths)
+
total_height = sum(heights)
+
+
canvas = AppKit.NSImage.alloc().initWithSize_(AppKit.NSMakeSize(max_width, total_height))
+
+
canvas.lockFocus()
+
current_y = 0
+
for image in images:
+
image.xa_elem.drawInRect_(AppKit.NSMakeRect(0, current_y, image.size[0], image.size[1]))
+
current_y += image.size[1]
+
canvas.unlockFocus()
+
return XAImage(canvas)
+
+
[docs] @staticmethod
+
def image_from_text(text: str, font_size: int = 15, font_name: str = "Menlo", font_color: XAColor = None, background_color: XAColor = None, inset: int = 10) -> 'XAImage':
+
"""Initializes an image of the provided text overlaid on the specified background color.
+
+
:param text: The text to create an image of
+
:type text: str
+
:param font_size: The font size of the text, defaults to 15
+
:type font_size: int, optional
+
:param font_name: The color of the text, defaults to XAColor.black()
+
:type font_name: str, optional
+
:param font_color: The name of the font to use for the text, defaults to ".SF NS Mono Light Medium"
+
:type font_color: XAColor, optional
+
:param background_color: The color to overlay the text on top of, defaults to XAColor.white()
+
:type background_color: XAColor, optional
+
:param inset: The width of the space between the text and the edge of the background color in the resulting image, defaults to 10
+
:type inset: int, optional
+
:return: XAImage
+
:rtype: The resulting image object
+
+
.. versionadded:: 0.1.0
+
"""
+
font = AppKit.NSFont.fontWithName_size_(font_name, font_size)
+
text = AppKit.NSString.alloc().initWithString_(text)
+
if font_color is None:
+
font_color = XAColor.black()
+
attributes = {
+
AppKit.NSFontAttributeName: font,
+
AppKit.NSForegroundColorAttributeName: font_color.xa_elem
+
}
+
text_size = text.sizeWithAttributes_(attributes)
+
+
# Make a white background to overlay the text on
+
if background_color is None:
+
background_color = XAColor.white()
+
swatch = background_color.make_swatch(text_size.width + inset * 2, text_size.height + inset * 2)
+
text_rect = AppKit.NSMakeRect(inset, inset, text_size.width, text_size.height)
+
+
# Overlay the text
+
swatch.xa_elem.lockFocus()
+
text.drawInRect_withAttributes_(text_rect, attributes)
+
swatch.xa_elem.unlockFocus()
+
return swatch
+
+
[docs] def edges(self, intensity: float = 1.0) -> 'XAImage':
+
"""Detects the edges in the image and highlights them colorfully, blackening other areas of the image.
+
+
:param intensity: The degree to which edges are highlighted. Higher is brighter. Defaults to 1.0
+
:type intensity: float
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIEdges")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(intensity, "inputIntensity")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def gaussian_blur(self, intensity: float = 10) -> 'XAImage':
+
"""Blurs the image using a Gaussian filter.
+
+
:param intensity: The strength of the blur effect, defaults to 10
+
:type intensity: float
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIGaussianBlur")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(intensity, "inputRadius")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def reduce_noise(self, noise_level: float = 0.02, sharpness: float = 0.4) -> 'XAImage':
+
"""Reduces noise in the image by sharpening areas with a luminance delta below the specified noise level threshold.
+
+
:param noise_level: The threshold for luminance changes in an area below which will be considered noise, defaults to 0.02
+
:type noise_level: float
+
:param sharpness: The sharpness of the resulting image, defaults to 0.4
+
:type sharpness: float
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CINoiseReduction")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(noise_level, "inputNoiseLevel")
+
filter.setValue_forKey_(sharpness, "inputSharpness")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def pixellate(self, pixel_size: float = 8.0) -> 'XAImage':
+
"""Pixellates the image.
+
+
:param pixel_size: The size of the pixels, defaults to 8.0
+
:type pixel_size: float
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIPixellate")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(pixel_size, "inputScale")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def outline(self, threshold: float = 0.1) -> 'XAImage':
+
"""Outlines detected edges within the image in black, leaving the rest transparent.
+
+
:param threshold: The threshold to use when separating edge and non-edge pixels. Larger values produce thinner edge lines. Defaults to 0.1
+
:type threshold: float
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CILineOverlay")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(threshold, "inputThreshold")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def invert(self) -> 'XAImage':
+
"""Inverts the color of the image.
+
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIColorInvert")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def sepia(self, intensity: float = 1.0) -> 'XAImage':
+
"""Applies a sepia filter to the image; maps all colors of the image to shades of brown.
+
+
:param intensity: The opacity of the sepia effect. A value of 0 will have no impact on the image. Defaults to 1.0
+
:type intensity: float
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CISepiaTone")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(intensity, "inputIntensity")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def vignette(self, intensity: float = 1.0) -> 'XAImage':
+
"""Applies vignette shading to the corners of the image.
+
+
:param intensity: The intensity of the vignette effect, defaults to 1.0
+
:type intensity: float
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIVignette")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(intensity, "inputIntensity")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def depth_of_field(self, focal_region: Union[tuple[tuple[int, int], tuple[int, int]], None] = None, intensity: float = 10.0, focal_region_saturation: float = 1.5) -> 'XAImage':
+
"""Applies a depth of field filter to the image, simulating a tilt & shift effect.
+
+
:param focal_region: Two points defining a line within the image to focus the effect around (pixels around the line will be in focus), or None to use the center third of the image, defaults to None
+
:type focal_region: Union[tuple[tuple[int, int], tuple[int, int]], None]
+
:param intensity: Controls the amount of distance around the focal region to keep in focus. Higher values decrease the distance before the out-of-focus effect starts. Defaults to 10.0
+
:type intensity: float
+
:param focal_region_saturation: Adjusts the saturation of the focial region. Higher values increase saturation. Defaults to 1.5 (1.5x default saturation)
+
:type focal_region_saturation: float
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
if focal_region is None:
+
center_top = Quartz.CIVector.vectorWithX_Y_(self.size[0] / 2, self.size[1] / 3)
+
center_bottom = Quartz.CIVector.vectorWithX_Y_(self.size[0] / 2, self.size[1] / 3 * 2)
+
focal_region = (center_top, center_bottom)
+
else:
+
point1 = Quartz.CIVector.vectorWithX_Y_(focal_region[0])
+
point2 = Quartz.CIVector.vectorWithX_Y_(focal_region[1])
+
focal_region = (point1, point2)
+
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIDepthOfField")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(focal_region[0], "inputPoint0")
+
filter.setValue_forKey_(focal_region[1], "inputPoint1")
+
filter.setValue_forKey_(intensity, "inputRadius")
+
filter.setValue_forKey_(focal_region_saturation, "inputSaturation")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def crystallize(self, crystal_size: float = 20.0) -> 'XAImage':
+
"""Applies a crystallization filter to the image. Creates polygon-shaped color blocks by aggregating pixel values.
+
+
:param crystal_size: The radius of the crystals, defaults to 20.0
+
:type crystal_size: float
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CICrystallize")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(crystal_size, "inputRadius")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def comic(self) -> 'XAImage':
+
"""Applies a comic filter to the image. Outlines edges and applies a color halftone effect.
+
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIComicEffect")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def pointillize(self, point_size: float = 20.0) -> 'XAImage':
+
"""Applies a pointillization filter to the image.
+
+
:param crystal_size: The radius of the points, defaults to 20.0
+
:type crystal_size: float
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIPointillize")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(point_size, "inputRadius")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def bloom(self, intensity: float = 0.5) -> 'XAImage':
+
"""Applies a bloom effect to the image. Softens edges and adds a glow.
+
+
:param intensity: The strength of the softening and glow effects, defaults to 0.5
+
:type intensity: float
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIBloom")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(intensity, "inputIntensity")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def monochrome(self, color: XAColor, intensity: float = 1.0) -> 'XAImage':
+
"""Remaps the colors of the image to shades of the specified color.
+
+
:param color: The color of map the image's colors to
+
:type color: XAColor
+
:param intensity: The strength of recoloring effect. Higher values map colors to darker shades of the provided color. Defaults to 1.0
+
:type intensity: float
+
:return: The resulting image after applying the filter
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
ci_color = Quartz.CIColor.alloc().initWithColor_(color.xa_elem)
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIColorMonochrome")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(ci_color, "inputColor")
+
filter.setValue_forKey_(intensity, "inputIntensity")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def bump(self, center: Union[tuple[int, int], None] = None, radius: float = 300.0, curvature: float = 0.5) -> 'XAImage':
+
"""Creates a concave (inward) or convex (outward) bump at the specified location within the image.
+
+
:param center: The center point of the effect, or None to use the center of the image, defaults to None
+
:type center: Union[tuple[int, int], None]
+
:param radius: The radius of the bump in pixels, defaults to 300.0
+
:type radius: float
+
:param curvature: Controls the direction and intensity of the bump's curvature. Positive values create convex bumps while negative values create concave bumps. Defaults to 0.5
+
:type curvature: float
+
:return: The resulting image after applying the distortion
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
if center is None:
+
center = Quartz.CIVector.vectorWithX_Y_(self.size[0] / 2, self.size[1] / 2)
+
else:
+
center = Quartz.CIVector.vectorWithX_Y_(center[0], center[1])
+
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIBumpDistortion")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(center, "inputCenter")
+
filter.setValue_forKey_(radius, "inputRadius")
+
filter.setValue_forKey_(curvature, "inputScale")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def pinch(self, center: Union[tuple[int, int], None] = None, intensity: float = 0.5) -> 'XAImage':
+
"""Creates an inward pinch distortion at the specified location within the image.
+
+
:param center: The center point of the effect, or None to use the center of the image, defaults to None
+
:type center: Union[tuple[int, int], None]
+
:param intensity: Controls the scale of the pinch effect. Higher values stretch pixels away from the specified center to a greater degree. Defaults to 0.5
+
:type intensity: float
+
:return: The resulting image after applying the distortion
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
if center is None:
+
center = Quartz.CIVector.vectorWithX_Y_(self.size[0] / 2, self.size[1] / 2)
+
else:
+
center = Quartz.CIVector.vectorWithX_Y_(center[0], center[1])
+
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CIPinchDistortion")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(center, "inputCenter")
+
filter.setValue_forKey_(intensity, "inputScale")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def twirl(self, center: Union[tuple[int, int], None] = None, radius: float = 300.0, angle: float = 3.14) -> 'XAImage':
+
"""Creates a twirl distortion by rotating pixels around the specified location within the image.
+
+
:param center: The center point of the effect, or None to use the center of the image, defaults to None
+
:type center: Union[tuple[int, int], None]
+
:param radius: The pixel radius around the centerpoint that defines the area to apply the effect to, defaults to 300.0
+
:type radius: float
+
:param angle: The angle of the twirl in radians, defaults to 3.14
+
:type angle: float
+
:return: The resulting image after applying the distortion
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
if center is None:
+
center = Quartz.CIVector.vectorWithX_Y_(self.size[0] / 2, self.size[1] / 2)
+
else:
+
center = Quartz.CIVector.vectorWithX_Y_(center[0], center[1])
+
+
image = Quartz.CIImage.imageWithData_(self.data)
+
filter = Quartz.CIFilter.filterWithName_("CITwirlDistortion")
+
filter.setDefaults()
+
filter.setValue_forKey_(image, "inputImage")
+
filter.setValue_forKey_(center, "inputCenter")
+
filter.setValue_forKey_(radius, "inputRadius")
+
filter.setValue_forKey_(angle, "inputAngle")
+
uncropped = filter.valueForKey_(Quartz.kCIOutputImageKey)
+
return self.__update_image(uncropped)
+
+
[docs] def auto_enhance(self, correct_red_eye: bool = False, crop_to_features: bool = False, correct_rotation: bool = False) -> 'XAImage':
+
"""Attempts to enhance the image by applying suggested filters.
+
+
:param correct_red_eye: Whether to attempt red eye removal, defaults to False
+
:type correct_red_eye: bool, optional
+
:param crop_to_features: Whether to crop the image to focus on the main features with it, defaults to False
+
:type crop_to_features: bool, optional
+
:param correct_rotation: Whether attempt perspective correction by rotating the image, defaults to False
+
:type correct_rotation: bool, optional
+
:return: The resulting image after applying the enchantments
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
ci_image = Quartz.CIImage.imageWithData_(self.data)
+
options = {
+
Quartz.kCIImageAutoAdjustRedEye: correct_red_eye,
+
Quartz.kCIImageAutoAdjustCrop: crop_to_features,
+
Quartz.kCIImageAutoAdjustLevel: correct_rotation
+
}
+
enhancements = ci_image.autoAdjustmentFiltersWithOptions_(options)
+
print(enhancements)
+
for filter in enhancements:
+
filter.setValue_forKey_(ci_image, "inputImage")
+
ci_image = filter.outputImage()
+
return self.__update_image(ci_image)
+
+
[docs] def flip_horizontally(self) -> 'XAImage':
+
"""Flips the image horizontally.
+
+
:return: The image object, modifications included
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
flipped_image = AppKit.NSImage.alloc().initWithSize_(self.xa_elem.size())
+
imageBounds = AppKit.NSMakeRect(0, 0, self.size[0], self.size[1])
+
+
transform = AppKit.NSAffineTransform.alloc().init()
+
transform.translateXBy_yBy_(self.size[0], 0)
+
transform.scaleXBy_yBy_(-1, 1)
+
+
flipped_image.lockFocus()
+
transform.concat()
+
self.xa_elem.drawInRect_fromRect_operation_fraction_(imageBounds, Quartz.CGRectZero, AppKit.NSCompositingOperationCopy, 1.0)
+
flipped_image.unlockFocus()
+
self.xa_elem = flipped_image
+
self.modified = True
+
return self
+
+
[docs] def flip_vertically(self) -> 'XAImage':
+
"""Flips the image vertically.
+
+
:return: The image object, modifications included
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
flipped_image = AppKit.NSImage.alloc().initWithSize_(self.xa_elem.size())
+
imageBounds = AppKit.NSMakeRect(0, 0, self.size[0], self.size[1])
+
+
transform = AppKit.NSAffineTransform.alloc().init()
+
transform.translateXBy_yBy_(0, self.size[1])
+
transform.scaleXBy_yBy_(1, -1)
+
+
flipped_image.lockFocus()
+
transform.concat()
+
self.xa_elem.drawInRect_fromRect_operation_fraction_(imageBounds, Quartz.CGRectZero, AppKit.NSCompositingOperationCopy, 1.0)
+
flipped_image.unlockFocus()
+
self.xa_elem = flipped_image
+
self.modified = True
+
return self
+
+
[docs] def rotate(self, degrees: float) -> 'XAImage':
+
"""Rotates the image clockwise by the specified number of degrees.
+
+
:param degrees: The number of degrees to rotate the image by
+
:type degrees: float
+
:return: The image object, modifications included
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
sinDegrees = abs(math.sin(degrees * math.pi / 180.0))
+
cosDegrees = abs(math.cos(degrees * math.pi / 180.0))
+
newSize = Quartz.CGSizeMake(self.size[1] * sinDegrees + self.size[0] * cosDegrees, self.size[0] * sinDegrees + self.size[1] * cosDegrees)
+
rotated_image = AppKit.NSImage.alloc().initWithSize_(newSize)
+
+
imageBounds = Quartz.CGRectMake((newSize.width - self.size[0]) / 2, (newSize.height - self.size[1]) / 2, self.size[0], self.size[1])
+
+
transform = AppKit.NSAffineTransform.alloc().init()
+
transform.translateXBy_yBy_(newSize.width / 2, newSize.height / 2)
+
transform.rotateByDegrees_(degrees)
+
transform.translateXBy_yBy_(-newSize.width / 2, -newSize.height / 2)
+
+
rotated_image.lockFocus()
+
transform.concat()
+
self.xa_elem.drawInRect_fromRect_operation_fraction_(imageBounds, Quartz.CGRectZero, AppKit.NSCompositingOperationCopy, 1.0)
+
rotated_image.unlockFocus()
+
self.xa_elem = rotated_image
+
self.modified = True
+
return self
+
+
[docs] def crop(self, size: tuple[int, int], corner: Union[tuple[int, int], None] = None) -> 'XAImage':
+
"""Crops the image to the specified dimensions.
+
+
:param size: The width and height of the resulting image
+
:type size: tuple[int, int]
+
:param corner: The bottom-left corner location from which to crop the image, or None to use (0, 0), defaults to None
+
:type corner: Union[tuple[int, int], None]
+
:return: The image object, modifications included
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
if corner is None:
+
# No corner provided -- use (0,0) by default
+
corner = (0, 0)
+
+
cropped_image = AppKit.NSImage.alloc().initWithSize_(AppKit.NSMakeSize(size[0], size[1]))
+
imageBounds = AppKit.NSMakeRect(corner[0], corner[1], self.size[0], self.size[1])
+
+
cropped_image.lockFocus()
+
self.xa_elem.drawInRect_(imageBounds)
+
cropped_image.unlockFocus()
+
self.xa_elem = cropped_image
+
self.modified = True
+
return self
+
+
[docs] def scale(self, scale_factor_x: float, scale_factor_y: Union[float, None] = None) -> 'XAImage':
+
"""Scales the image by the specified horizontal and vertical factors.
+
+
:param scale_factor_x: The factor by which to scale the image in the X dimension
+
:type scale_factor_x: float
+
:param scale_factor_y: The factor by which to scale the image in the Y dimension, or None to match the horizontal factor, defaults to None
+
:type scale_factor_y: Union[float, None]
+
:return: The image object, modifications included
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
if scale_factor_y is None:
+
scale_factor_y = scale_factor_x
+
+
scaled_image = AppKit.NSImage.alloc().initWithSize_(AppKit.NSMakeSize(self.size[0] * scale_factor_x, self.size[1] * scale_factor_y))
+
imageBounds = AppKit.NSMakeRect(0, 0, self.size[0], self.size[1])
+
+
transform = AppKit.NSAffineTransform.alloc().init()
+
transform.scaleXBy_yBy_(scale_factor_x, scale_factor_y)
+
+
scaled_image.lockFocus()
+
transform.concat()
+
self.xa_elem.drawInRect_fromRect_operation_fraction_(imageBounds, Quartz.CGRectZero, AppKit.NSCompositingOperationCopy, 1.0)
+
scaled_image.unlockFocus()
+
self.xa_elem = scaled_image
+
self.modified = True
+
return self
+
+
[docs] def resize(self, width: int, height: Union[int, None] = None) -> Self:
+
"""Resizes the image to the specified width and height.
+
+
:param width: The width of the resulting image, in pixels
+
:type width: int
+
:param height: The height of the resulting image, in pixels, or None to maintain width:height proportions, defaults to None
+
:type height: Union[int, None]
+
:return: The image object, modifications included
+
:rtype: Self
+
+
.. versionadded:: 0.1.1
+
"""
+
img_width = self.size[0]
+
img_height = self.size[1]
+
+
resized_image = AppKit.NSImage.alloc().initWithSize_(AppKit.NSMakeSize(img_width * width / img_width, img_height * height / img_height))
+
imageBounds = AppKit.NSMakeRect(0, 0, self.size[0], self.size[1])
+
+
transform = AppKit.NSAffineTransform.alloc().init()
+
transform.scaleXBy_yBy_(width / img_width, height / img_height)
+
+
resized_image.lockFocus()
+
transform.concat()
+
self.xa_elem.drawInRect_fromRect_operation_fraction_(imageBounds, Quartz.CGRectZero, AppKit.NSCompositingOperationCopy, 1.0)
+
resized_image.unlockFocus()
+
self.xa_elem = resized_image
+
self.modified = True
+
return self
+
+
[docs] def pad(self, horizontal_border_width: int = 50, vertical_border_width: int = 50, pad_color: Union[XAColor, None] = None) -> 'XAImage':
+
"""Pads the image with the specified color; adds a border around the image with the specified vertical and horizontal width.
+
+
:param horizontal_border_width: The border width, in pixels, in the x-dimension, defaults to 50
+
:type horizontal_border_width: int
+
:param vertical_border_width: The border width, in pixels, in the y-dimension, defaults to 50
+
:type vertical_border_width: int
+
:param pad_color: The color of the border, or None for a white border, defaults to None
+
:type pad_color: Union[XAColor, None]
+
:return: The image object, modifications included
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
if pad_color is None:
+
# No color provided -- use white by default
+
pad_color = XAColor.white()
+
+
new_width = self.size[0] + horizontal_border_width * 2
+
new_height = self.size[1] + vertical_border_width * 2
+
color_swatch = pad_color.make_swatch(new_width, new_height)
+
+
color_swatch.xa_elem.lockFocus()
+
bounds = AppKit.NSMakeRect(horizontal_border_width, vertical_border_width, self.size[0], self.size[1])
+
self.xa_elem.drawInRect_(bounds)
+
color_swatch.xa_elem.unlockFocus()
+
self.xa_elem = color_swatch.xa_elem
+
self.modified = True
+
return self
+
+
[docs] def overlay_image(self, image: 'XAImage', location: Union[tuple[int, int], None] = None, size: Union[tuple[int, int], None] = None) -> 'XAImage':
+
"""Overlays an image on top of this image, at the specified location, with the specified size.
+
+
:param image: The image to overlay on top of this image
+
:type image: XAImage
+
:param location: The bottom-left point of the overlaid image in the result, or None to use the bottom-left point of the background image, defaults to None
+
:type location: Union[tuple[int, int], None]
+
:param size: The width and height of the overlaid image, or None to use the overlaid's images existing width and height, or (-1, -1) to use the dimensions of the background image, defaults to None
+
:type size: Union[tuple[int, int], None]
+
:return: The image object, modifications included
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
if location is None:
+
# No location provided -- use the bottom-left point of the background image by default
+
location = (0, 0)
+
+
if size is None:
+
# No dimensions provided -- use size of overlay image by default
+
size = image.size
+
elif size == (-1, -1):
+
# Use remaining width/height of background image
+
size = (self.size[0] - location[0], self.size[1] - location[1])
+
elif size[0] == -1:
+
# Use remaining width of background image + provided height
+
size = (self.size[0] - location[0], size[1])
+
elif size[1] == -1:
+
# Use remaining height of background image + provided width
+
size = (size[1], self.size[1] - location[1])
+
+
self.xa_elem.lockFocus()
+
bounds = AppKit.NSMakeRect(location[0], location[1], size[0], size[1])
+
image.xa_elem.drawInRect_(bounds)
+
self.xa_elem.unlockFocus()
+
self.modified = True
+
return self.xa_elem
+
+
[docs] def overlay_text(self, text: str, location: Union[tuple[int, int], None] = None, font_size: float = 12, font_color: Union[XAColor, None] = None) -> 'XAImage':
+
"""Overlays text of the specified size and color at the provided location within the image.
+
+
:param text: The text to overlay onto the image
+
:type text: str
+
:param location: The bottom-left point of the start of the text, or None to use (5, 5), defaults to None
+
:type location: Union[tuple[int, int], None]
+
:param font_size: The font size, in pixels, of the text, defaults to 12
+
:type font_size: float
+
:param font_color: The color of the text, or None to use black, defaults to None
+
:type font_color: XAColor
+
:return: The image object, modifications included
+
:rtype: XAImage
+
+
.. versionadded:: 0.1.0
+
"""
+
if location is None:
+
# No location provided -- use (5, 5) by default
+
location = (5, 5)
+
+
if font_color is None:
+
# No color provided -- use black by default
+
font_color = XAColor.black()
+
+
font = AppKit.NSFont.userFontOfSize_(font_size)
+
textRect = Quartz.CGRectMake(location[0], 0, self.size[0] - location[0], location[1])
+
attributes = {
+
AppKit.NSFontAttributeName: font,
+
AppKit.NSForegroundColorAttributeName: font_color.xa_elem
+
}
+
+
self.xa_elem.lockFocus()
+
AppKit.NSString.alloc().initWithString_(text).drawInRect_withAttributes_(textRect, attributes)
+
self.xa_elem.unlockFocus()
+
self.modified = True
+
return self
+
+
+
+
[docs] def show_in_preview(self):
+
"""Opens the image in preview.
+
+
.. versionadded:: 0.0.8
+
"""
+
global workspace
+
if workspace is None:
+
workspace = AppKit.NSWorkspace.sharedWorkspace()
+
+
if not self.modified and self.file is not None and isinstance(self.file, XAPath):
+
workspace.openFile_withApplication_(self.file.path, "Preview")
+
else:
+
tmp_file = tempfile.NamedTemporaryFile()
+
with open(tmp_file.name, 'wb') as f:
+
f.write(self.xa_elem.TIFFRepresentation())
+
+
config = AppKit.NSWorkspaceOpenConfiguration.alloc().init()
+
config.setActivates_(True)
+
+
img_url = XAPath(tmp_file.name).xa_elem
+
preview_url = XAPath("/System/Applications/Preview.app/").xa_elem
+
workspace.openURLs_withApplicationAtURL_configuration_completionHandler_([img_url], preview_url, config, None)
+
time.sleep(1)
+
+
[docs] def save(self, file_path: Union[XAPath, str, None] = None):
+
"""Saves the image to a file on the disk. Saves to the original file (if there was one) by default.
+
+
:param file_path: The path at which to save the image file. Any existing file at that location will be overwritten, defaults to None
+
:type file_path: Union[XAPath, str, None]
+
+
.. versionadded:: 0.1.0
+
"""
+
if file_path is None and self.file is not None:
+
file_path = self.file.path
+
elif isinstance(file_path, XAPath):
+
file_path = file_path.path
+
fm = AppKit.NSFileManager.defaultManager()
+
fm.createFileAtPath_contents_attributes_(file_path, self.xa_elem.TIFFRepresentation(), None)
+
+
[docs] def get_clipboard_representation(self) -> 'AppKit.NSImage':
+
"""Gets a clipboard-codable representation of the iimage.
+
+
When the clipboard content is set to an image, the image itself, including any modifications, is added to the clipboard. Pasting will then insert the image into the active document.
+
+
:return: The raw NSImage object for this XAIMage
+
:rtype: AppKit.NSImage
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem
+
+
[docs] def __eq__(self, other):
+
return isinstance(other, XAImage) and self.xa_elem.TIFFRepresentation() == other.xa_elem.TIFFRepresentation()
+
+
+
+
+[docs]class XASoundList(XAList, XAClipboardCodable):
+
"""A wrapper around lists of sounds that employs fast enumeration techniques.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, properties: dict, filter: Union[dict, None] = None):
+
super().__init__(properties, XASound, filter)
+
+
[docs] def file(self) -> list[XAPath]:
+
return [sound.file for sound in self]
+
+
[docs] def num_sample_frames(self) -> list[int]:
+
return [sound.num_sample_frames for sound in self]
+
+
[docs] def sample_rate(self) -> list[float]:
+
return [sound.sample_rate for sound in self]
+
+
[docs] def duration(self) -> list[float]:
+
return [sound.duration for sound in self]
+
+
[docs] def play(self) -> Self:
+
"""Plays all sounds in the list simultaneously.
+
+
:return: The list of sounds.
+
:rtype: Self
+
+
.. versionadded:: 0.1.2
+
"""
+
for sound in self:
+
sound.play()
+
return self
+
+
[docs] def pause(self) -> Self:
+
"""Pauses playback of all sounds in the list.
+
+
:return: The list of sounds.
+
:rtype: Self
+
+
.. versionadded:: 0.1.2
+
"""
+
for sound in self:
+
sound.pause()
+
return self
+
+
[docs] def resume(self) -> Self:
+
"""Resumes playback of all sounds in the list.
+
+
:return: The list of sounds.
+
:rtype: Self
+
+
.. versionadded:: 0.1.2
+
"""
+
for sound in self:
+
sound.resume()
+
return self
+
+
[docs] def stop(self) -> Self:
+
"""Stops playback of all sounds in the list.
+
+
:return: The list of sounds.
+
:rtype: Self
+
+
.. versionadded:: 0.1.2
+
"""
+
for sound in self:
+
sound.stop()
+
return self
+
+
[docs] def trim(self, start_time: float, end_time: float) -> 'XASoundList':
+
"""Trims each sound in the list to the specified start and end time, in seconds.
+
+
:param start_time: The start time in seconds
+
:type start_time: float
+
:param end_time: The end time in seconds
+
:type end_time: float
+
:return: The list of updated sounds
+
:rtype: XASoundList
+
+
.. versionadded:: 0.1.2
+
"""
+
return self._new_element([sound.trim(start_time, end_time) for sound in self], XASoundList)
+
+
[docs] def get_clipboard_representation(self) -> list[Union['AppKit.NSSound', 'AppKit.NSURL', str]]:
+
"""Gets a clipboard-codable representation of each sound in the list.
+
+
When the clipboard content is set to a list of sounds, each sound's raw sound data, its associated file URL, and its file path string are added to the clipboard.
+
+
:return: The clipboard-codable form of the sound
+
:rtype: Any
+
+
.. versionadded:: 0.1.0
+
"""
+
return [self.xa_elem, self.file(), [x.path() for x in self.file()]]
+
+[docs]class XASound(XAObject, XAClipboardCodable):
+
"""A class for playing and interacting with audio files and data.
+
+
.. versionadded:: 0.0.1
+
"""
+
[docs] def __init__(self, sound_reference: Union[str, XAURL, XAPath]):
+
self.file = None
+
+
match sound_reference:
+
case str() as ref if "://" in ref:
+
self.file = XAURL(ref)
+
+
case str() as ref if os.path.exists(ref):
+
self.file = XAPath(sound_reference)
+
+
case str() as ref:
+
self.file = XAPath("/System/Library/Sounds/" + ref + ".aiff")
+
+
case {"element": str() as ref}:
+
self.file = XASound(ref).file
+
+
case {"element": XASound() as ref}:
+
self.file = ref.file
+
+
case XAPath() as ref:
+
self.file = ref
+
+
case XAURL() as ref:
+
self.file = ref
+
+
case XASound() as sound:
+
self.file = sound.file
+
+
self.duration: float #: The duration of the sound in seconds
+
+
import AVFoundation
+
self.__audio_file = AVFoundation.AVAudioFile.alloc().initForReading_error_(self.file.xa_elem if self.file is not None else None, None)[0]
+
+
self.__audio_engine = AVFoundation.AVAudioEngine.alloc().init()
+
self.__player_node = AVFoundation.AVAudioPlayerNode.alloc().init()
+
self.__audio_engine.attachNode_(self.__player_node)
+
+
self.__audio_engine.connect_to_format_(self.__player_node, self.__audio_engine.mainMixerNode(), self.__audio_file.processingFormat())
+
+
self.__player_node.stop()
+
self.__audio_engine.stop()
+
+
self.xa_elem = self.__audio_file
+
+
@property
+
def num_sample_frames(self) -> int:
+
"""The number of sample frames in the audio file.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.length()
+
+
@property
+
def sample_rate(self) -> float:
+
"""The sample rate for the sound format, in hertz.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.xa_elem.processingFormat().sampleRate()
+
+
@property
+
def duration(self) -> float:
+
"""The duration of the sound in seconds.
+
+
.. versionadded:: 0.1.0
+
"""
+
return self.num_sample_frames / self.sample_rate
+
+
[docs] def open(*sound_references: Union[str, XAPath, list[Union[str, XAPath]]]) -> Union['XASound', XASoundList]:
+
"""Initializes one or more sounds from files.
+
+
:param sound_references: The sound(s) to open
+
:type sound_references: Union[str, XAPath, list[Union[str, XAPath]]]
+
:return: The newly created sound object, or a list of sound objects
+
:rtype: Union[XASound, XASoundList]
+
+
.. versionadded:: 0.1.0
+
"""
+
if len(sound_references) == 1:
+
sound_references = sound_references[0]
+
+
if isinstance(sound_references, list) or isinstance(sound_references, tuple):
+
return XASoundList({"element": sound_references})
+
else:
+
return XASound(sound_references)
+
+
[docs] def beep():
+
"""Plays the system Beep sound.
+
+
.. versionadded:: 0.1.0
+
"""
+
AppleScript("""
+
beep
+
delay 0.5
+
""").run()
+
+
[docs] def play(self) -> Self:
+
"""Plays the sound from the beginning.
+
+
Audio playback runs in a separate thread. For the sound the play properly, you must keep the main thread alive over the duration of the desired playback.
+
+
:return: A reference to this sound object.
+
:rtype: Self
+
+
:Example:
+
+
>>> import PyXA
+
>>> import time
+
>>> glass_sound = PyXA.sound("Glass")
+
>>> glass_sound.play()
+
>>> time.sleep(glass_sound.duration)
+
+
.. seealso:: :func:`pause`, :func:`stop`
+
+
.. versionadded:: 0.0.1
+
"""
+
def play_sound(self):
+
self.__player_node.scheduleFile_atTime_completionHandler_(self.xa_elem, None, None)
+
self.__audio_engine.startAndReturnError_(None)
+
self.__player_node.play()
+
while self.__player_node.isPlaying():
+
AppKit.NSRunLoop.currentRunLoop().runUntilDate_(datetime.now() + timedelta(seconds = 0.1))
+
+
self._spawn_thread(play_sound, [self])
+
return self
+
+
[docs] def pause(self) -> Self:
+
"""Pauses the sound.
+
+
:return: A reference to this sound object.
+
:rtype: Self
+
+
:Example:
+
+
>>> import PyXA
+
>>> glass_sound = PyXA.sound("Glass")
+
>>> glass_sound.pause()
+
+
.. seealso:: :func:`resume`, :func:`stop`
+
+
.. versionadded:: 0.0.1
+
"""
+
self.__player_node.pause()
+
return self
+
+
[docs] def resume(self) -> Self:
+
"""Plays the sound starting from the time it was last paused at.
+
+
Audio playback runs in a separate thread. For the sound the play properly, you must keep the main thread alive over the duration of the desired playback.
+
+
:return: A reference to this sound object.
+
:rtype: Self
+
+
:Example:
+
+
>>> import PyXA
+
>>> glass_sound = PyXA.sound("Glass")
+
>>> glass_sound.resume()
+
+
.. seealso:: :func:`pause`, :func:`play`
+
+
.. versionadded:: 0.0.1
+
"""
+
def play_sound(self):
+
self.__player_node.scheduleFile_atTime_completionHandler_(self.xa_elem, None, None)
+
self.__audio_engine.startAndReturnError_(None)
+
self.__player_node.play()
+
while self.__player_node.isPlaying():
+
AppKit.NSRunLoop.currentRunLoop().runUntilDate_(datetime.now() + timedelta(seconds = 0.1))
+
+
self._spawn_thread(play_sound, [self])
+
return self
+
+
[docs] def stop(self) -> 'XASound':
+
"""Stops playback of the sound and rewinds it to the beginning.
+
+
:return: A reference to this sound object.
+
:rtype: XASound
+
+
:Example:
+
+
>>> import PyXA
+
>>> glass_sound = PyXA.sound("Glass")
+
>>> glass_sound.stop()
+
+
.. seealso:: :func:`pause`, :func:`play`
+
+
.. versionadded:: 0.0.1
+
"""
+
self.__audio_engine.stop()
+
return self
+
+
[docs] def set_volume(self, volume: float) -> Self:
+
"""Sets the volume of the sound.
+
+
:param volume: The desired volume of the sound in the range [0.0, 1.0].
+
:type volume: int
+
:return: A reference to this sound object.
+
:rtype: Self
+
+
:Example:
+
+
>>> import PyXA
+
>>> glass_sound = PyXA.sound("Glass")
+
>>> glass_sound.set_volume(1.0)
+
+
.. seealso:: :func:`volume`
+
+
.. versionadded:: 0.0.1
+
"""
+
self.__audio_engine.mainMixerNode().setOutputVolume_(volume)
+
return self
+
+
[docs] def volume(self) -> float:
+
"""Returns the current volume of the sound.
+
+
:return: The volume level of the sound.
+
:rtype: int
+
+
:Example:
+
+
>>> import PyXA
+
>>> glass_sound = PyXA.sound("Glass")
+
>>> print(glass_sound.volume())
+
1.0
+
+
.. seealso:: :func:`set_volume`
+
+
.. versionadded:: 0.0.1
+
"""
+
return self.__audio_engine.mainMixerNode().volume()
+
+
[docs] def loop(self, times: int) -> Self:
+
"""Plays the sound the specified number of times.
+
+
Audio playback runs in a separate thread. For the sound the play properly, you must keep the main thread alive over the duration of the desired playback.
+
+
:param times: The number of times to loop the sound.
+
:type times: int
+
:return: A reference to this sound object.
+
:rtype: Self
+
+
:Example:
+
+
>>> import PyXA
+
>>> import time
+
>>> glass_sound = PyXA.sound("Glass")
+
>>> glass_sound.loop(10)
+
>>> time.sleep(glass_sound.duration * 10)
+
+
.. versionadded:: 0.0.1
+
"""
+
def play_sound():
+
num_plays = 0
+
while num_plays < times:
+
sound = XASound(self.file)
+
sound.play()
+
num_plays += 1
+
time.sleep(self.duration)
+
+
self._spawn_thread(play_sound)
+
return self
+
+
[docs] def trim(self, start_time: float, end_time: float) -> Self:
+
"""Trims the sound to the specified start and end time, in seconds.
+
+
This will create a momentary sound data file in the current working directory for storing the intermediary trimmed sound data.
+
+
:param start_time: The start time in seconds
+
:type start_time: float
+
:param end_time: The end time in seconds
+
:type end_time: float
+
:return: The updated sound object
+
:rtype: Self
+
+
.. versionadded:: 0.1.0
+
"""
+
# Clear the temp data path
+
file_path = "sound_data_tmp.m4a"
+
if os.path.exists(file_path):
+
AppKit.NSFileManager.defaultManager().removeItemAtPath_error_(file_path, None)
+
+
# Configure the export session
+
import AVFoundation
+
asset = AVFoundation.AVAsset.assetWithURL_(self.file.xa_elem)
+
export_session = AVFoundation.AVAssetExportSession.exportSessionWithAsset_presetName_(asset, AVFoundation.AVAssetExportPresetAppleM4A)
+
+
import CoreMedia
+
start_time = CoreMedia.CMTimeMake(start_time * 100, 100)
+
end_time = CoreMedia.CMTimeMake(end_time * 100, 100)
+
time_range = CoreMedia.CMTimeRangeFromTimeToTime(start_time, end_time);
+
+
export_session.setTimeRange_(time_range)
+
export_session.setOutputURL_(XAPath(file_path).xa_elem)
+
export_session.setOutputFileType_(AVFoundation.AVFileTypeAppleM4A)
+
+
# Export to file path
+
waiting = False
+
def handler():
+
nonlocal waiting
+
waiting = True
+
+
export_session.exportAsynchronouslyWithCompletionHandler_(handler)
+
+
while not waiting:
+
time.sleep(0.01)
+
+
# Load the sound file back into active memory
+
self.__audio_file = AVFoundation.AVAudioFile.alloc().initForReading_error_(XAPath(file_path).xa_elem, None)[0]
+
self.xa_elem = self.__audio_file
+
AppKit.NSFileManager.defaultManager().removeItemAtPath_error_(file_path, None)
+
return self
+
+
[docs] def save(self, file_path: Union[XAPath, str]):
+
"""Saves the sound to the specified file path.
+
+
:param file_path: The path to save the sound to
+
:type file_path: Union[XAPath, str]
+
+
.. versionadded:: 0.1.0
+
"""
+
if isinstance(file_path, str):
+
file_path = XAPath(file_path)
+
+
# Configure the export session
+
import AVFoundation
+
asset = AVFoundation.AVAsset.assetWithURL_(self.file.xa_elem)
+
export_session = AVFoundation.AVAssetExportSession.exportSessionWithAsset_presetName_(asset, AVFoundation.AVAssetExportPresetAppleM4A)
+
+
import CoreMedia
+
start_time = CoreMedia.CMTimeMake(0, 100)
+
end_time = CoreMedia.CMTimeMake(self.duration * 100, 100)
+
time_range = CoreMedia.CMTimeRangeFromTimeToTime(start_time, end_time);
+
+
export_session.setTimeRange_(time_range)
+
export_session.setOutputURL_(file_path.xa_elem)
+
# export_session.setOutputFileType_(AVFoundation.AVFileTypeAppleM4A)
+
+
# Export to file path
+
waiting = False
+
def handler():
+
nonlocal waiting
+
waiting = True
+
+
export_session.exportAsynchronouslyWithCompletionHandler_(handler)
+
+
while not waiting:
+
time.sleep(0.01)
+
+
[docs] def get_clipboard_representation(self) -> list[Union['AppKit.NSSound', 'AppKit.NSURL', str]]:
+
"""Gets a clipboard-codable representation of the sound.
+
+
When the clipboard content is set to a sound, the raw sound data, the associated file URL, and the path string of the file are added to the clipboard.
+
+
:return: The clipboard-codable form of the sound
+
:rtype: Any
+
+
.. versionadded:: 0.0.8
+
"""
+
return [self.xa_elem, self.file.xa_elem, self.file.xa_elem.path()]
+
+
+
+[docs]class XAVideo(XAObject):
+
"""A class for interacting with video files and data.
+
+
.. versionadded:: 0.1.0
+
"""
+
[docs] def __init__(self, video_reference: Union[str, XAURL, XAPath]):
+
if isinstance(video_reference, str):
+
# References is to some kind of path or URL
+
if "://" in video_reference:
+
video_reference = XAURL(video_reference)
+
else:
+
video_reference = XAPath(video_reference)
+
+
import AVFoundation
+
self.xa_elem = AVFoundation.AVURLAsset.alloc().initWithURL_options_(video_reference.xa_elem, { AVFoundation.AVURLAssetPreferPreciseDurationAndTimingKey: True })
+
+
[docs] def reverse(self, output_file: Union[XAPath, str]):
+
"""Reverses the video and exports the result to the specified output file path.
+
+
:param output_file: The file to export the reversed video to
+
:type output_file: Union[XAPath, str]
+
+
.. versionadded:: 0.1.0
+
"""
+
if isinstance(output_file, str):
+
output_file = XAPath(output_file)
+
output_url = output_file.xa_elem
+
+
import AVFoundation
+
reader = AVFoundation.AVAssetReader.alloc().initWithAsset_error_(self.xa_elem, None)[0]
+
+
video_track = self.xa_elem.tracksWithMediaType_(AVFoundation.AVMediaTypeVideo)[-1]
+
+
reader_output = AVFoundation.AVAssetReaderTrackOutput.alloc().initWithTrack_outputSettings_(video_track, { Quartz.CoreVideo.kCVPixelBufferPixelFormatTypeKey: Quartz.CoreVideo.kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange })
+
+
reader.addOutput_(reader_output)
+
reader.startReading()
+
+
samples = []
+
while sample := reader_output.copyNextSampleBuffer():
+
samples.append(sample)
+
+
writer = AVFoundation.AVAssetWriter.alloc().initWithURL_fileType_error_(output_url, AVFoundation.AVFileTypeMPEG4, None)[0]
+
+
writer_settings = {
+
AVFoundation.AVVideoCodecKey: AVFoundation.AVVideoCodecTypeH264,
+
AVFoundation.AVVideoWidthKey: video_track.naturalSize().width,
+
AVFoundation.AVVideoHeightKey: video_track.naturalSize().height,
+
AVFoundation.AVVideoCompressionPropertiesKey: { AVFoundation.AVVideoAverageBitRateKey: video_track.estimatedDataRate() }
+
}
+
+
format_hint = video_track.formatDescriptions()[-1]
+
writer_input = AVFoundation.AVAssetWriterInput.alloc().initWithMediaType_outputSettings_sourceFormatHint_(AVFoundation.AVMediaTypeVideo, writer_settings, format_hint)
+
+
writer_input.setExpectsMediaDataInRealTime_(False)
+
+
import CoreMedia
+
pixel_buffer_adaptor = AVFoundation.AVAssetWriterInputPixelBufferAdaptor.alloc().initWithAssetWriterInput_sourcePixelBufferAttributes_(writer_input, None)
+
writer.addInput_(writer_input)
+
writer.startWriting()
+
writer.startSessionAtSourceTime_(CoreMedia.CMSampleBufferGetPresentationTimeStamp(samples[0]))
+
+
for index, sample in enumerate(samples):
+
presentation_time = CoreMedia.CMSampleBufferGetPresentationTimeStamp(sample)
+
+
image_buffer_ref = CoreMedia.CMSampleBufferGetImageBuffer(samples[len(samples) - index - 1])
+
if image_buffer_ref is not None:
+
pixel_buffer_adaptor.appendPixelBuffer_withPresentationTime_(image_buffer_ref, presentation_time)
+
+
while not writer_input.isReadyForMoreMediaData():
+
time.sleep(0.1)
+
+
self._spawn_thread(writer.finishWriting)
+
return AVFoundation.AVAsset.assetWithURL_(output_url)
+
+
[docs] def show_in_quicktime(self):
+
"""Shows the video in QuickTime Player.
+
+
This will create a momentary video data file in the current working directory to store intermediary video data.
+
+
.. versionadded:: 0.1.0
+
"""
+
global workspace
+
if workspace is None:
+
workspace = AppKit.NSWorkspace.sharedWorkspace()
+
+
self.save("video-data-tmp.mp4")
+
+
video_url = XAPath(os.getcwd() + "/video-data-tmp.mp4").xa_elem
+
quicktime_url = XAPath("/System/Applications/QuickTime Player.app").xa_elem
+
workspace.openURLs_withApplicationAtURL_configuration_completionHandler_([video_url], quicktime_url, None, None)
+
time.sleep(1)
+
+
AppKit.NSFileManager.defaultManager().removeItemAtPath_error_(video_url.path(), None)
+
+
[docs] def save(self, file_path: Union[XAPath, str]):
+
"""Saves the video at the specified file path.
+
+
:param file_path: The path to save the video at
+
:type file_path: Union[XAPath, str]
+
+
.. versionadded:: 0.1.0
+
"""
+
if isinstance(file_path, str):
+
file_path = XAPath(file_path)
+
+
# Configure the export session
+
import AVFoundation
+
export_session = AVFoundation.AVAssetExportSession.exportSessionWithAsset_presetName_(self.xa_elem, AVFoundation.AVAssetExportPresetHighestQuality)
+
+
import CoreMedia
+
start_time = CoreMedia.CMTimeMake(0, 100)
+
end_time = CoreMedia.CMTimeMake(self.xa_elem.duration().value * self.xa_elem.duration().timescale, 100)
+
time_range = CoreMedia.CMTimeRangeFromTimeToTime(start_time, end_time);
+
+
export_session.setTimeRange_(time_range)
+
export_session.setOutputURL_(file_path.xa_elem)
+
+
# Export to file path
+
waiting = False
+
def handler():
+
nonlocal waiting
+
waiting = True
+
+
export_session.exportAsynchronouslyWithCompletionHandler_(handler)
+
+
while not waiting:
+
time.sleep(0.01)
+