gui_actions.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. # Copyright 2021-2025 Avaiga Private Limited
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  4. # the License. You may obtain a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  9. # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  10. # specific language governing permissions and limitations under the License.
  11. import threading
  12. import typing as t
  13. from ._warnings import _warn
  14. from .gui import Gui
  15. from .state import State
  16. from .utils.callable import _is_function
  17. def download(
  18. state: State, content: t.Any, name: t.Optional[str] = "", on_action: t.Optional[t.Union[str, t.Callable]] = ""
  19. ):
  20. """Download content to the client.
  21. Arguments:
  22. state (State^): The current user state as received in any callback.
  23. content: File path or file content. See below.
  24. name: File name for the content on the client browser (defaults to content name).
  25. on_action: Callback function (or callback name) to call when the download ends. See below.
  26. <h4>Notes:</h4>
  27. - *content*: this parameter can hold several values depending on your use case:
  28. - a string: the value must be an existing path name to the file that gets downloaded or
  29. the URL to the resource you want to download.
  30. - a buffer (such as a `bytes` object): if the size of the buffer is smaller than the
  31. [*data_url_max_size*](../../../../../userman/advanced_features/configuration/gui-config.md#p-data_url_max_size)
  32. configuration setting, then the [`python-magic`](https://pypi.org/project/python-magic/)
  33. package is used to determine the [MIME type](https://en.wikipedia.org/wiki/Media_type)
  34. of the buffer content, and the download is performed using a generated "data:" URL with
  35. the relevant type, and a base64-encoded version of the buffer content.<br/>
  36. If the buffer is too large, its content is transferred after saving it in a temporary
  37. server file.
  38. - *on_action*: this callback is triggered when the transfer of the content is achieved.</br>
  39. In this function, you can perform any clean-up operation that could be required after
  40. the download is completed.<br/>
  41. This callback can use three optional parameters:
  42. - *state*: the `State^` instance of the caller.
  43. - *id* (optional): a string representing the identifier of the caller. If this function
  44. is called directly, this will always be "Gui.download". Some controls may also trigger
  45. download actions, and then *id* would reflect the identifier of those controls.
  46. - *payload* (optional): an optional payload from the caller.<br/>
  47. This is a dictionary with the following keys:
  48. - *action*: the name of the callback;
  49. - *args*: an array of two strings. The first element reflects the *name* parameter,
  50. and the second element reflects the server-side URL where the file is located.
  51. """
  52. if state and isinstance(state._gui, Gui):
  53. state._gui._download(content, name, on_action)
  54. else:
  55. _warn("'download()' must be called in the context of a callback.")
  56. def notify(
  57. state: State,
  58. notification_type: str = "info",
  59. message: str = "",
  60. system_notification: t.Optional[bool] = None,
  61. duration: t.Optional[int] = None,
  62. id: str = "",
  63. ) -> t.Optional[str]:
  64. """Send a notification to the user interface.
  65. Arguments:
  66. state (State^): The current user state as received in any callback.
  67. notification_type: The notification type. This can be one of "success", "info",
  68. "warning", or "error".
  69. message: The text message to display.
  70. system_notification: If True, the system will also show the notification.<br/>
  71. If not specified or set to None, this parameter will use the value of
  72. *configuration[system_notification]*.
  73. duration: The time, in milliseconds, that the notification is displayed.<br/>
  74. If not specified or set to None, this parameter will use the value of
  75. *configuration[notification_duration]*.<br/>
  76. If *duration* is 0, the notification remains visible indefinitely until closed. If *id*
  77. is set to a non-empty string, the application can call `close_notification(id)^` to
  78. close the notification. The user can always manually close the notification.
  79. id: An optional identifier for this notification, so the application can close it explicitly
  80. using `close_notification()^` before the *duration* delay has passed.
  81. Note that you can also call this function with *notification_type* set to the first letter
  82. or the notification type (i.e. setting *notification_type* to "i" is equivalent to setting it to
  83. "info").
  84. If *system_notification* is set to True, then the browser requests the system to display a
  85. notification as well. They usually appear in small windows that fly out of the system tray.<br/>
  86. When a Taipy application requests a system notification for the first time, the browser may
  87. prompt the user for permission. The browser documentation will describe how to allow or prevent
  88. this feature.<br/>
  89. If the user denies system notification permissions, the system notifications will not be
  90. displayed, but the in-app notification will still function.
  91. """
  92. if state and isinstance(state._gui, Gui):
  93. return state._gui._notify(notification_type, message, system_notification, duration, id)
  94. else:
  95. _warn("'notify()' must be called in the context of a callback.")
  96. return None
  97. def close_notification(state: State, id: str) -> None:
  98. """Close a specific notification.
  99. This function closes a persistent notification by using the same identifier that was provided to
  100. `notify()^`.<br/>
  101. If multiple notifications were created with the same identifier, they will all be closed
  102. simultaneously.
  103. If no notification with this identifier exists, no action is taken.
  104. Arguments:
  105. state (State^): The current user state as received in any callback.
  106. id: The identifier of the notification(s) that must be closed.
  107. """
  108. if state and isinstance(state._gui, Gui):
  109. # Send the close command with the notification_id
  110. state._gui._close_notification(id)
  111. else:
  112. _warn("'close_notification()' must be called in the context of a callback.")
  113. def hold_control(
  114. state: State,
  115. callback: t.Optional[t.Union[str, t.Callable]] = None,
  116. message: t.Optional[str] = "Work in Progress...",
  117. ):
  118. """Hold the User Interface actions.
  119. When the User Interface is held, users cannot interact with visual elements.<br/>
  120. The application must call `resume_control()^` so that users can interact again
  121. with the visual elements.
  122. You can set a callback function (or the name of a function) in the *callback* parameter. Then,
  123. a "Cancel" button will be displayed so the user can cancel whatever is happening in the
  124. application. When pressed, the callback is invoked.
  125. Arguments:
  126. state (State^): The current user state received in any callback.
  127. callback (Optional[Union[str, Callable]]): The function to be called if the user
  128. chooses to cancel.<br/>
  129. If empty or None, no cancel action is provided to the user.<br/>
  130. The signature of this function is:
  131. - state (`State^`): The user state;
  132. - id (str): the id of the button that triggered the callback. That will always be
  133. "UIBlocker" since it is created and managed internally;
  134. If this parameter is None, no "Cancel" button is displayed.
  135. message: The message to show. The default value is the string "Work in Progress...".
  136. """
  137. if state and isinstance(state._gui, Gui):
  138. state._gui._hold_actions(callback, message)
  139. else:
  140. _warn("'hold_actions()' must be called in the context of a callback.")
  141. def resume_control(state: State):
  142. """Resume the User Interface actions.
  143. This function must be called after `hold_control()^` was invoked, when interaction
  144. must be allowed again for the user.
  145. Arguments:
  146. state (State^): The current user state as received in any callback.
  147. """
  148. if state and isinstance(state._gui, Gui):
  149. state._gui._resume_actions()
  150. else:
  151. _warn("'resume_actions()' must be called in the context of a callback.")
  152. def navigate(
  153. state: State,
  154. to: t.Optional[str] = "",
  155. params: t.Optional[t.Dict[str, str]] = None,
  156. tab: t.Optional[str] = None,
  157. force: t.Optional[bool] = False,
  158. ):
  159. """Navigate to a page.
  160. Arguments:
  161. state (State^): The current user state as received in any callback.
  162. to: The name of the page to navigate to. This can be a page identifier (as created by
  163. `Gui.add_page()^` with no leading '/') or a URL.<br/>
  164. If omitted, the application navigates to the root page.
  165. params: A dictionary of query parameters.
  166. tab: When navigating to a page that is not a known page, the page is opened in a tab identified by
  167. *tab* (as in [window.open](https://developer.mozilla.org/en-US/docs/Web/API/Window/open)).<br/>
  168. The default value creates a new tab for the page (which is equivalent to setting *tab* to "_blank").
  169. force: When navigating to a known page, the content is refreshed even it the page is already shown.
  170. """
  171. if state and isinstance(state._gui, Gui):
  172. state._gui._navigate(to, params, tab, force)
  173. else:
  174. _warn("'navigate()' must be called in the context of a callback.")
  175. def get_user_content_url(
  176. state: State, path: t.Optional[str] = None, params: t.Optional[t.Dict[str, str]] = None
  177. ) -> t.Optional[str]:
  178. """Get a user content URL.
  179. This function can be used if you need to deliver dynamic content to your page: you can create
  180. a path at run-time that, when queried, will deliver some user-defined content defined in the
  181. *on_user_content* callback (see the description of the `Gui^` class for more information).
  182. Arguments:
  183. state (State^): The current user state as received in any callback.
  184. path: An optional additional path to the URL.
  185. params: An optional dictionary sent to the *on_user_content* callback.<br/>
  186. These arguments are added as query parameters to the generated URL and converted into
  187. strings.
  188. Returns:
  189. An URL that, when queried, triggers the *on_user_content* callback.
  190. """
  191. if state and isinstance(state._gui, Gui):
  192. return state._gui._get_user_content_url(path, params)
  193. _warn("'get_user_content_url()' must be called in the context of a callback.")
  194. return None
  195. def get_state_id(state: State) -> t.Optional[str]:
  196. """Get the identifier of a state.
  197. The state identifier is a string generated by Taipy GUI for a given `State^` that is used
  198. to serialize callbacks.
  199. See the
  200. [User Manual section on Long Running Callbacks](../../../../../userman/gui/callbacks.md#long-running-callbacks)
  201. for details on when and how this function can be used.
  202. Arguments:
  203. state (State^): The current user state as received in any callback.
  204. Returns:
  205. A string that uniquely identifies the state.<br/>
  206. If this value None, it indicates that *state* is not handled by a `Gui^` instance.
  207. """
  208. if state and isinstance(state._gui, Gui):
  209. return state._gui._get_client_id()
  210. return None
  211. def get_module_context(state: State) -> t.Optional[str]:
  212. """Get the name of the module currently in used when using page scopes
  213. Arguments:
  214. state (State^): The current user state as received in any callback.
  215. Returns:
  216. The name of the current module.
  217. """
  218. if state and isinstance(state._gui, Gui):
  219. return state._gui._get_locals_context()
  220. return None
  221. def get_context_id(state: State) -> t.Any:
  222. """NOT DOCUMENTED"""
  223. _warn("'get_context_id()' was deprecated in Taipy GUI 2.0. Use 'get_state_id()' instead.")
  224. return get_state_id(state)
  225. def get_module_name_from_state(state: State) -> t.Optional[str]:
  226. """Get the module name that triggered a callback.
  227. Pages can be defined in different modules yet refer to callback functions declared elsewhere
  228. (typically, the application's main module).
  229. This function returns the name of the module where the page that holds the control that
  230. triggered the callback was declared. This lets applications implement different behaviors
  231. depending on what page is involved.
  232. This function must be called only in the body of a callback function.
  233. Arguments:
  234. state (State^): The `State^` instance, as received in any callback.
  235. Returns:
  236. The name of the module that holds the definition of the page containing the control
  237. that triggered the callback that was provided the *state* object.
  238. """
  239. if state and isinstance(state._gui, Gui):
  240. return state._gui._get_locals_context()
  241. return None
  242. def invoke_callback(
  243. gui: Gui,
  244. state_id: str,
  245. callback: t.Callable,
  246. args: t.Optional[t.Sequence[t.Any]] = None,
  247. module_context: t.Optional[str] = None,
  248. ) -> t.Any:
  249. """Invoke a user callback for a given state.
  250. Calling this function is equivalent to calling
  251. *gui*.`(Gui.)invoke_callback(state_id, callback, args, module_context)^`.
  252. Arguments:
  253. gui (Gui^): The current Gui instance.
  254. state_id: The identifier of the state to use, as returned by `get_state_id()^`.
  255. callback (Callable[[State^, ...], None]): The user-defined function that is invoked.<br/>
  256. The first parameter of this function **must** be a `State^`.
  257. args (Optional[Sequence]): The remaining arguments, as a List or a Tuple.
  258. module_context (Optional[str]): the name of the module that will be used.
  259. """
  260. if isinstance(gui, Gui):
  261. return gui.invoke_callback(state_id, callback, args, module_context)
  262. _warn("'invoke_callback()' must be called with a valid Gui instance.")
  263. def broadcast_callback(
  264. gui: Gui,
  265. callback: t.Callable,
  266. args: t.Optional[t.Sequence[t.Any]] = None,
  267. module_context: t.Optional[str] = None,
  268. ) -> t.Dict[str, t.Any]:
  269. """Invoke a callback for every client.
  270. Calling this function is equivalent to calling the method
  271. *gui*.`(Gui.)broadcast_callback(callback, args)^`.
  272. Arguments:
  273. gui (Gui^): The current Gui instance.
  274. callback: The user-defined function to be invoked.<br/>
  275. The first parameter of this function must be a `State^` object representing the
  276. client for which it is invoked.<br/>
  277. The other parameters should reflect the ones provided in the *args* collection.
  278. args: The parameters to send to *callback*, if any.
  279. """
  280. if isinstance(gui, Gui):
  281. return gui.broadcast_callback(callback, args, module_context)
  282. _warn("'broadcast_callback()' must be called with a valid Gui instance.")
  283. def invoke_state_callback(gui: Gui, state_id: str, callback: t.Callable, args: t.Union[t.Tuple, t.List]) -> t.Any:
  284. """NOT DOCUMENTED"""
  285. _warn("'invoke_state_callback()' was deprecated in Taipy GUI 2.0. Use 'Gui.invoke_callback()' instead.")
  286. return gui.invoke_callback(state_id, callback, args)
  287. def invoke_long_callback(
  288. state: State,
  289. user_function: t.Callable,
  290. user_function_args: t.Optional[t.Union[t.Tuple, t.List]] = None,
  291. user_status_function: t.Optional[t.Callable] = None,
  292. user_status_function_args: t.Optional[t.Union[t.Tuple, t.List]] = None,
  293. period=0,
  294. ):
  295. """Invoke a long-running user callback.
  296. Long-running callbacks are run in a separate thread to not block the application itself.
  297. This function expects to be provided a function to run in the background (in *user_function*).<br/>
  298. It can also be specified a *status function* that is called when the operation performed by
  299. *user_function* is finished (successfully or not), or periodically (using the *period* parameter).
  300. See the
  301. [User Manual section on Long Running Callbacks](../../../../../userman/gui/callbacks.md#long-running-callbacks)
  302. for details on when and how this function can be used.
  303. Arguments:
  304. state (State^): The `State^` instance, as received in any callback.
  305. user_function (Callable[[...], None]): The function that will be run independently of Taipy GUI. Note
  306. that this function must not use *state*, which is not persisted across threads.
  307. user_function_args (Optional[List|Tuple]): The arguments to send to *user_function*.
  308. user_status_function (Optional(Callable[[State^, bool, ...], None])): The optional user-defined status
  309. function that is invoked at the end of and possibly during the runtime of *user_function*:
  310. - The first parameter of this function is set to a `State^` instance.
  311. - The second parameter of this function is set to a bool or an int, depending on the
  312. conditions under which it is called:
  313. - If this parameter is set to a bool value, then:
  314. - If True, this indicates that *user_function* has finished properly.
  315. The last argument passed will be the result of the user_function call.
  316. - If False, this indicates that *user_function* failed.
  317. - If this parameter is set to an int value, then this value indicates
  318. how many periods (as lengthy as indicated in *period*) have elapsed since *user_function* was
  319. started.
  320. user_status_function_args (Optional[List|Tuple]): The remaining arguments of the user status function.
  321. period (int): The interval, in milliseconds, at which *user_status_function* is called.<br/>
  322. The default value is 0, meaning no call to *user_status_function* will happen until *user_function*
  323. terminates (then the second parameter of that call will be ).</br>
  324. When set to a value smaller than 500, *user_status_function* is called only when *user_function*
  325. terminates (as if *period* was set to 0).
  326. """
  327. if not state or not isinstance(state._gui, Gui):
  328. _warn("'invoke_long_callback()' must be called in the context of a callback.")
  329. return
  330. if user_status_function_args is None:
  331. user_status_function_args = []
  332. if user_function_args is None:
  333. user_function_args = []
  334. this_gui = state.get_gui()
  335. state_id = this_gui._get_client_id()
  336. module_context = this_gui._get_locals_context()
  337. if not isinstance(state_id, str) or not isinstance(module_context, str):
  338. return
  339. def callback_on_exception(state: State, function_name: str, e: Exception):
  340. if not this_gui._call_on_exception(function_name, e):
  341. _warn(f"invoke_long_callback(): Exception raised in function {function_name}()", e)
  342. def callback_on_status(
  343. status: t.Union[int, bool],
  344. e: t.Optional[Exception] = None,
  345. function_name: t.Optional[str] = None,
  346. function_result: t.Optional[t.Any] = None,
  347. ):
  348. if _is_function(user_status_function):
  349. this_gui.invoke_callback(
  350. str(state_id),
  351. t.cast(t.Callable, user_status_function),
  352. [status] + list(user_status_function_args) + [function_result], # type: ignore
  353. str(module_context),
  354. )
  355. if e:
  356. this_gui.invoke_callback(
  357. str(state_id),
  358. callback_on_exception,
  359. (
  360. str(function_name),
  361. e,
  362. ),
  363. str(module_context),
  364. )
  365. def user_function_in_thread(*uf_args):
  366. try:
  367. res = user_function(*uf_args)
  368. callback_on_status(True, function_result=res)
  369. except Exception as e:
  370. callback_on_status(False, e, user_function.__name__)
  371. def thread_status(name: str, period_s: float, count: int):
  372. active_thread = next((t for t in threading.enumerate() if t.name == name), None)
  373. if active_thread:
  374. callback_on_status(count)
  375. threading.Timer(period_s, thread_status, (name, period_s, count + 1)).start()
  376. thread = threading.Thread(target=user_function_in_thread, args=user_function_args)
  377. thread.start()
  378. if isinstance(period, int) and period >= 500 and _is_function(user_status_function):
  379. thread_status(thread.name, period / 1000.0, 0)
  380. def query_local_storage(state: State, *keys: str) -> t.Optional[t.Union[str, t.Dict[str, str]]]:
  381. """Retrieve values from the browser's local storage.
  382. This function queries the local storage of the client identified by *state* and returns the
  383. values associated with the specified keys. Local storage is a key-value store available in the
  384. user's browser, typically manipulated by client-side code.
  385. Arguments:
  386. state (State^): The current user state as received in any callback.
  387. *keys (string): One or more keys to retrieve values for from the client's local storage.
  388. Returns:
  389. The requested values from the browser's local storage.
  390. - If a single key is provided (*keys* has a single element), this function returns the
  391. corresponding value as a string.
  392. - If multiple keys are provided, this function returns a dictionary mapping each key to
  393. its value in the client's local storage.
  394. - If no value is found for a key, that key will not appear in the dictionary.
  395. """
  396. if state and isinstance(state._gui, Gui):
  397. return state._gui._query_local_storage(*keys)
  398. _warn("'query_local_storage()' must be called in the context of a callback.")
  399. return None