quickmacapp 2025.4.4__tar.gz → 2025.6.24__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickmacapp
3
- Version: 2025.4.4
3
+ Version: 2025.6.24
4
4
  Summary: Make it easier to write Mac apps in Python
5
5
  Description-Content-Type: text/x-rst
6
6
  License-File: LICENSE
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
  name = "quickmacapp"
10
10
  description = "Make it easier to write Mac apps in Python"
11
11
  readme = "README.rst"
12
- version = "2025.04.04"
12
+ version = "2025.06.24"
13
13
  dependencies = [
14
14
  "pyobjc-framework-Cocoa",
15
15
  "pyobjc-framework-ExceptionHandling",
@@ -1,9 +1,10 @@
1
- from ._quickapp import Actionable, Status, mainpoint, menu, quit
1
+ from ._quickapp import Actionable, ItemState, Status, mainpoint, menu, quit
2
2
  from ._interactions import ask, choose, answer, getpass
3
3
  from ._background import dockIconWhenVisible
4
4
 
5
5
  __all__ = [
6
6
  "Actionable",
7
+ "ItemState",
7
8
  "Status",
8
9
  "mainpoint",
9
10
  "menu",
@@ -1,36 +1,43 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
- from zoneinfo import ZoneInfo
5
4
  from types import TracebackType
6
5
  from typing import Any, Awaitable, Callable, Protocol, TypeAlias
6
+ from zoneinfo import ZoneInfo
7
7
 
8
8
  from datetype import DateTime
9
-
10
- from Foundation import NSError, NSLog, NSObject, NSDateComponents
9
+ from Foundation import (
10
+ NSTimeZone,
11
+ NSDateComponents,
12
+ NSError,
13
+ NSLog,
14
+ NSObject,
15
+ NSCalendar,
16
+ NSCalendarIdentifierGregorian,
17
+ )
11
18
  from objc import object_property
12
19
  from twisted.internet.defer import Deferred
13
20
  from UserNotifications import (
14
- UNNotificationCategoryOptions,
15
21
  UNAuthorizationOptionNone,
22
+ UNCalendarNotificationTrigger,
16
23
  UNMutableNotificationContent,
17
24
  UNNotification,
18
25
  UNNotificationAction,
19
- UNNotificationActionOptions,
20
26
  UNNotificationActionOptionAuthenticationRequired,
21
27
  UNNotificationActionOptionDestructive,
22
28
  UNNotificationActionOptionForeground,
29
+ UNNotificationActionOptions,
23
30
  UNNotificationCategory,
24
31
  UNNotificationCategoryOptionAllowInCarPlay,
25
32
  UNNotificationCategoryOptionHiddenPreviewsShowSubtitle,
26
33
  UNNotificationCategoryOptionHiddenPreviewsShowTitle,
34
+ UNNotificationCategoryOptions,
27
35
  UNNotificationPresentationOptionBanner,
28
36
  UNNotificationPresentationOptions,
29
37
  UNNotificationRequest,
30
38
  UNNotificationResponse,
31
39
  UNNotificationSettings,
32
40
  UNNotificationTrigger,
33
- UNCalendarNotificationTrigger,
34
41
  UNTextInputNotificationAction,
35
42
  UNUserNotificationCenter,
36
43
  )
@@ -178,7 +185,7 @@ class NotificationTranslator[T](Protocol):
178
185
  The application has requested to send a notification to the operating
179
186
  system, serialize the Python object represneting this category of
180
187
  notification into a 2-tuple of C{notificatcionID}, C{userData} that can
181
- be encapsulated in a L{UNNotificationRequest}.
188
+ be encapsulated in a C{UNNotificationRequest}.
182
189
  """
183
190
 
184
191
 
@@ -208,7 +215,19 @@ class Notifier[NotifT]:
208
215
  async def notifyAt(
209
216
  self, when: DateTime[ZoneInfo], notification: NotifT, title: str, body: str
210
217
  ) -> None:
218
+
211
219
  components: NSDateComponents = NSDateComponents.alloc().init()
220
+ components.setCalendar_(
221
+ NSCalendar.calendarWithIdentifier_(NSCalendarIdentifierGregorian)
222
+ )
223
+ components.setTimeZone_(NSTimeZone.timeZoneWithName_(when.tzinfo.key))
224
+ components.setYear_(when.year)
225
+ components.setMonth_(when.month)
226
+ components.setDay_(when.day)
227
+ components.setHour_(when.hour)
228
+ components.setMinute_(when.minute)
229
+ components.setSecond_(when.second)
230
+
212
231
  repeats: bool = False
213
232
  trigger: UNNotificationTrigger = (
214
233
  UNCalendarNotificationTrigger.triggerWithDateMatchingComponents_repeats_(
@@ -3,31 +3,48 @@ from __future__ import annotations
3
3
  import os
4
4
  import sys
5
5
  import traceback
6
- from typing import Callable, Protocol, Any
7
-
8
- from objc import ivar, IBAction, super
9
-
10
- from Foundation import (
11
- NSObject,
12
- NSException,
13
- )
6
+ from dataclasses import dataclass
7
+ from types import FunctionType
8
+ from typing import Any, Callable, Iterable, Protocol, Sequence, Literal
14
9
 
15
10
  from AppKit import (
16
11
  NSApp,
17
12
  NSApplication,
18
13
  NSEvent,
19
- NSResponder,
20
- NSMenu,
21
14
  NSImage,
15
+ NSMenu,
22
16
  NSMenuItem,
17
+ NSResponder,
23
18
  NSStatusBar,
24
19
  NSVariableStatusItemLength,
20
+ NSControlStateValueOn,
21
+ NSControlStateValueOff,
25
22
  )
26
-
23
+ from ExceptionHandling import NSStackTraceKey # type:ignore
24
+ from Foundation import NSException, NSObject
25
+ from objc import IBAction, ivar, super
27
26
  from PyObjCTools.Debugging import _run_atos, isPythonException
28
- from ExceptionHandling import ( # type:ignore
29
- NSStackTraceKey,
30
- )
27
+
28
+
29
+ def asSelectorString(f: FunctionType) -> str:
30
+ """
31
+ Convert a method on a PyObjC class into a selector string.
32
+ """
33
+ return f.__name__.replace("_", ":")
34
+
35
+
36
+ @dataclass(kw_only=True)
37
+ class ItemState:
38
+ """
39
+ The state of a menu item.
40
+ """
41
+
42
+ enabled: bool = True
43
+ "Should the menu item be disabled? True if not, False if so."
44
+ checked: bool = False
45
+ "Should the menu item display a check-mark next to itself? True if so, False if not."
46
+ key: str | None = None
47
+ "Should the menu shortcut mnemonic key be set, blank, or derived from the item's title?"
31
48
 
32
49
 
33
50
  class Actionable(NSObject):
@@ -35,15 +52,20 @@ class Actionable(NSObject):
35
52
  Wrap a Python no-argument function call in an NSObject with a C{doIt:}
36
53
  method.
37
54
  """
38
- _thunk: Callable[[], None]
39
55
 
40
- def initWithFunction_(self, thunk: Callable[[], None]) -> Actionable:
56
+ _thunk: Callable[[], object]
57
+ _state: ItemState
58
+
59
+ def initWithFunction_andState_(
60
+ self, thunk: Callable[[], None], state: ItemState
61
+ ) -> Actionable:
41
62
  """
42
63
  Remember the given callable.
43
64
 
44
65
  @param thunk: the callable to run in L{doIt_}.
45
66
  """
46
67
  self._thunk = thunk
68
+ self._state = state
47
69
  return self
48
70
 
49
71
  @IBAction
@@ -52,10 +74,41 @@ class Actionable(NSObject):
52
74
  Call the given callable; exposed as an C{IBAction} in case you want IB
53
75
  to be able to see it.
54
76
  """
55
- self._thunk()
77
+ result = self._thunk()
78
+ if isinstance(result, ItemState):
79
+ self._state = result
80
+
81
+ def validateMenuItem_(self, item: NSMenuItem) -> bool:
82
+ item.setState_(
83
+ NSControlStateValueOn if self._state.checked else NSControlStateValueOff
84
+ )
85
+ return self._state.enabled
86
+
87
+
88
+ ACTION_METHOD = asSelectorString(Actionable.doIt_)
89
+
90
+
91
+ def _adjust(
92
+ items: Iterable[
93
+ tuple[str, Callable[[], object]] | tuple[str, Callable[[], object], ItemState]
94
+ ],
95
+ ) -> Iterable[tuple[str, Callable[[], object], ItemState]]:
96
+ for item in items:
97
+ if len(item) == 3:
98
+ yield item
99
+ else:
100
+ yield (*item, ItemState())
101
+
56
102
 
103
+ ItemSeq = Sequence[
104
+ tuple[str, Callable[[], object]] | tuple[str, Callable[[], object], ItemState]
105
+ ]
57
106
 
58
- def menu(title: str, items: list[tuple[str, Callable[[], object]]]) -> NSMenu:
107
+
108
+ def menu(
109
+ title: str,
110
+ items: ItemSeq,
111
+ ) -> NSMenu:
59
112
  """
60
113
  Construct an NSMenu from a list of tuples describing it.
61
114
 
@@ -68,11 +121,16 @@ def menu(title: str, items: list[tuple[str, Callable[[], object]]]) -> NSMenu:
68
121
  @return: a new Menu tha is not attached to anything.
69
122
  """
70
123
  result = NSMenu.alloc().initWithTitle_(title)
71
- for (subtitle, thunk) in items:
124
+ for subtitle, thunk, state in _adjust(items):
125
+ initialKeyEquivalent = subtitle[0].lower()
72
126
  item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
73
- subtitle, "doIt:", subtitle[0].lower()
127
+ subtitle,
128
+ ACTION_METHOD,
129
+ initialKeyEquivalent if state.key is None else state.key,
130
+ )
131
+ item.setTarget_(
132
+ Actionable.alloc().initWithFunction_andState_(thunk, state).retain()
74
133
  )
75
- item.setTarget_(Actionable.alloc().initWithFunction_(thunk).retain())
76
134
  result.addItem_(item)
77
135
  result.update()
78
136
  return result
@@ -97,11 +155,12 @@ class Status:
97
155
  self.item.button().setImage_(image)
98
156
  elif text is None:
99
157
  from __main__ import __file__ as default
158
+
100
159
  text = os.path.basename(default)
101
160
  if text is not None:
102
161
  self.item.button().setTitle_(text)
103
162
 
104
- def menu(self, items: list[tuple[str, Callable[[], object]]]) -> None:
163
+ def menu(self, items: ItemSeq) -> None:
105
164
  """
106
165
  Set the status drop-down menu.
107
166
 
@@ -177,12 +236,13 @@ class QuickApplication(NSApplication):
177
236
  """
178
237
  QuickMacApp's main application class.
179
238
 
180
- @ivar keyEquivalentHandler: Set this attribute to a custom L{NSResponder}
239
+ @ivar keyEquivalentHandler: Set this attribute to a custom C{NSResponder}
181
240
  if you want to handle key equivalents outside the responder chain. (I
182
241
  believe this is necessary in some apps because the responder chain can
183
242
  be more complicated in LSUIElement apps, but there might be a better
184
243
  way to do this.)
185
244
  """
245
+
186
246
  keyEquivalentHandler: NSResponder = ivar()
187
247
 
188
248
  def sendEvent_(self, event: NSEvent) -> None:
@@ -210,6 +270,7 @@ class MainRunner(Protocol):
210
270
  """
211
271
  A function which has been decorated with a runMain attribute.
212
272
  """
273
+
213
274
  def __call__(self, reactor: Any) -> None:
214
275
  """
215
276
  @param reactor: A Twisted reactor, which provides the usual suspects of
@@ -218,6 +279,7 @@ class MainRunner(Protocol):
218
279
 
219
280
  runMain: Callable[[], None]
220
281
 
282
+
221
283
  def mainpoint() -> Callable[[Callable[[Any], None]], MainRunner]:
222
284
  """
223
285
  Add a .runMain attribute to function
@@ -227,10 +289,11 @@ def mainpoint() -> Callable[[Callable[[Any], None]], MainRunner]:
227
289
  The runMain attribute starts a reactor and calls the original function
228
290
  with a running, initialized, reactor.
229
291
  """
292
+
230
293
  def wrapup(appmain: Callable[[Any], None]) -> MainRunner:
231
294
  def doIt() -> None:
232
- from twisted.internet import cfreactor
233
295
  import PyObjCTools.AppHelper
296
+ from twisted.internet import cfreactor
234
297
 
235
298
  QuickApplication.sharedApplication()
236
299
 
@@ -12,18 +12,18 @@ from zoneinfo import ZoneInfo
12
12
  from datetype import DateTime
13
13
  from UserNotifications import (
14
14
  UNNotificationCategoryOptionCustomDismissAction as _UNNotificationCategoryOptionCustomDismissAction,
15
- UNUserNotificationCenter as _UNUserNotificationCenter,
16
15
  UNNotificationDefaultActionIdentifier as _UNNotificationDefaultActionIdentifier,
17
16
  UNNotificationDismissActionIdentifier as _UNNotificationDismissActionIdentifier,
18
17
  )
18
+ from UserNotifications import UNUserNotificationCenter as _UNUserNotificationCenter
19
19
 
20
20
  from quickmacapp._notifications import (
21
21
  NotificationTranslator,
22
+ _AppNotificationsCtxBuilder,
22
23
  _BuiltinActionInfo,
23
24
  _PlainNotificationActionInfo,
24
25
  _setActionInfo,
25
26
  _TextNotificationActionInfo,
26
- _AppNotificationsCtxBuilder,
27
27
  )
28
28
 
29
29
  __all__ = [
@@ -139,6 +139,9 @@ class Notifier[NotifT](Protocol):
139
139
 
140
140
 
141
141
  class NotificationConfig(Protocol):
142
+ """
143
+ The application-wide configuration for a notification.
144
+ """
142
145
 
143
146
  def add[NotifT](
144
147
  self,
@@ -147,7 +150,35 @@ class NotificationConfig(Protocol):
147
150
  allowInCarPlay: bool = False,
148
151
  hiddenPreviewsShowTitle: bool = False,
149
152
  hiddenPreviewsShowSubtitle: bool = False,
150
- ) -> Notifier[NotifT]: ...
153
+ ) -> Notifier[NotifT]:
154
+ """
155
+ Add a new category (represented by a plain Python class, which may have
156
+ some methods that were decorated with L{response}C{(...)},
157
+ L{response.text}C{(...)}).
158
+
159
+ @param category: A Python type that contains the internal state for the
160
+ notifications that will be emitted, that will be relayed back to
161
+ its responses (e.g. the response methods on the category).
162
+
163
+ @param translator: a translator that can serialize and deserialize
164
+ python objects to C{UNUserNotificationCenter} values.
165
+
166
+ @param allowInCarPlay: Should the notification be allowed to show in
167
+ CarPlay?
168
+
169
+ @param hiddenPreviewsShowTitle: Should lock-screen previews for this
170
+ notification show the unredacted title?
171
+
172
+ @param hiddenPreviewsShowSubtitle: Should lock-screen previews for this
173
+ notification show the unredacted subtitle?
174
+
175
+ @return: A L{Notifier} that can deliver notifications to macOS.
176
+
177
+ @note: This method can I{only} be called within the C{with} statement
178
+ for the context manager beneath C{configureNotifications}, and can
179
+ only do this once per process. Otherwise it will raise an
180
+ exception.
181
+ """
151
182
 
152
183
 
153
184
  def configureNotifications() -> _AbstractAsyncContextManager[NotificationConfig]:
@@ -208,7 +239,7 @@ def configureNotifications() -> _AbstractAsyncContextManager[NotificationConfig]
208
239
  uselessness, and in this oversimplified case, it is! However, if you are
209
240
  sending notifications to a user, you really need to be able to I{respond}
210
241
  to notifications from a user, and that's where your notification data class
211
- as well as L{responder} comes in. To respond to a notification when the
242
+ as well as L{response} comes in. To respond to a notification when the
212
243
  user clicks on it, you can add a method like so::
213
244
 
214
245
  class MyNotificationData:
@@ -226,7 +257,7 @@ def configureNotifications() -> _AbstractAsyncContextManager[NotificationConfig]
226
257
  be serialized and deserialized with C{MyNotificationLoader.toNotification}
227
258
  (converting your Python class into a macOS notification, to send along to
228
259
  the OS) and C{MyNotificationLoader.fromNotification} (converting the data
229
- sent along with the user's response back into a L{MyNotificationData}).
260
+ sent along with the user's response back into a C{MyNotificationData}).
230
261
 
231
262
  @note: If your app schedules a notification, then quits, when the user
232
263
  responds (clicks on it, uses a button, dismisses it, etc) then the OS
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickmacapp
3
- Version: 2025.4.4
3
+ Version: 2025.6.24
4
4
  Summary: Make it easier to write Mac apps in Python
5
5
  Description-Content-Type: text/x-rst
6
6
  License-File: LICENSE
File without changes