gui_actions.py 19 KB

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