datalad_ui.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import threading
  2. from textwrap import wrap
  3. from typing import (
  4. List,
  5. Tuple,
  6. )
  7. from queue import Queue
  8. from PySide6.QtCore import (
  9. QObject,
  10. Signal,
  11. Slot,
  12. )
  13. from PySide6.QtWidgets import (
  14. QInputDialog,
  15. QLineEdit,
  16. )
  17. from datalad.ui.dialog import DialogUI
  18. class _DataladQtUIBridge(QObject):
  19. """Private class handling the DataladUI->QtUI bridging
  20. This is meant to be used by the GooeyUI singleton.
  21. """
  22. # signal to be emmitted when message() was called
  23. message_received = Signal(str)
  24. question_asked = Signal(dict)
  25. def __init__(self, app):
  26. super().__init__()
  27. self._app = app
  28. self._conlog = app.get_widget('logViewer')
  29. # establishing this signal/slot connection is the vehicle
  30. # with which worker threads can thread-safely send messages
  31. # to the UI for display
  32. self.message_received.connect(self.show_message)
  33. # do not use this queue without an additional global lock
  34. # that covers the entire time from emitting the signal
  35. # that will lead to queue usage, until the queue is emptied
  36. # again
  37. self.messageq = Queue(maxsize=1)
  38. self.question_asked.connect(self.get_answer)
  39. @Slot(str)
  40. def show_message(self, msg):
  41. self._conlog.appendPlainText(msg)
  42. @Slot(str)
  43. def get_answer(self, props: dict):
  44. if props.get('choices') is None:
  45. # we are asking for a string
  46. response, ok = self._get_text_answer(
  47. props['title'],
  48. props['text'],
  49. props.get('default'),
  50. props.get('hidden', False),
  51. )
  52. # TODO implement internal repeat on request
  53. else:
  54. response, ok = self._get_choice(
  55. props['title'],
  56. props['text'],
  57. props['choices'],
  58. props.get('default'),
  59. )
  60. # place in message Q for the asking thread to retrieve
  61. self.messageq.put((ok, response))
  62. def _get_text_answer(self, title: str, label: str, default: str = None,
  63. hidden: bool = False) -> Tuple:
  64. return QInputDialog.getText(
  65. # parent
  66. self._app.main_window,
  67. # dialog title
  68. title,
  69. # input widget label
  70. # we have to perform manual wrapping, QInputDialog won't do it
  71. '\n'.join(wrap(label, 70)),
  72. # input widget echo mode
  73. # this could also be QLineEdit.Password for more hiding
  74. QLineEdit.Password if hidden else QLineEdit.Normal,
  75. # input widget default text
  76. default or '',
  77. # TODO look into the following for UI-internal input validation
  78. #inputMethodHints=
  79. )
  80. def _get_choice(self, title: str, label: str, choices: List,
  81. default: str = None) -> Tuple:
  82. return QInputDialog.getItem(
  83. # parent
  84. self._app.main_window,
  85. # dialog title
  86. title,
  87. # input widget label
  88. # we have to perform manual wrapping, QInputDialog won't do it
  89. '\n'.join(wrap(label, 70)),
  90. choices,
  91. # input widget default choice id
  92. choices.index(default) if default else 0,
  93. )
  94. class GooeyUI(DialogUI):
  95. # It may be possible to not derive from datalad class here, but for datalad
  96. # commands to not fail to talk to the UI (specially progressbars),
  97. # need to figure a bit more details of what to implement here. So, just derive
  98. # to not have most commands fail with AttributeError etc.
  99. #class GooeyUI:
  100. """Adapter between the Gooey Qt UI and DataLad's UI API"""
  101. _singleton = None
  102. _threadlock = threading.Lock()
  103. def __new__(cls, *args, **kwargs):
  104. if not cls._singleton:
  105. with cls._threadlock:
  106. if not cls._singleton:
  107. cls._singleton = super().__new__(cls)
  108. return cls._singleton
  109. def __init__(self):
  110. super().__init__()
  111. self._app = None
  112. def set_app(self, gooey_app) -> None:
  113. """Connect the UI to a Gooey app providing the UI to use"""
  114. self._uibridge = _DataladQtUIBridge(gooey_app)
  115. #
  116. # the datalad API needed
  117. #
  118. def is_interactive(self):
  119. return True
  120. def message(self, msg: str, cr: str = '\n'):
  121. self._uibridge.message_received.emit(msg)
  122. # TODO handle `cr`, but this could be trickier...
  123. # handling a non-block addition, may require custom insertion
  124. # via cursor, see below for code that would need
  125. # to go in DataladQtUIBridge.show_message()
  126. #cursor = self._conlog.textCursor()
  127. #cursor.insertText(msg)
  128. #self._conlog.cursorPositionChanged.emit()
  129. #self._conlog.textChanged.emit()
  130. def question(self, text,
  131. title=None, choices=None,
  132. default=None,
  133. hidden=False,
  134. repeat=None):
  135. with self._threadlock:
  136. assert self._uibridge.messageq.empty()
  137. # acquire the lock before we emit the signal
  138. # to make sure that our signal is the only one putting an answer in
  139. # the queue
  140. self._uibridge.question_asked.emit(dict(
  141. title=title,
  142. text=text,
  143. choices=choices,
  144. default=default,
  145. hidden=hidden,
  146. repeat=repeat,
  147. ))
  148. # this will block until the queue has the answer
  149. ok, answer = self._uibridge.messageq.get()
  150. if not ok:
  151. # This would happen if the user has pressed the CANCEL button.
  152. # DataLadUI seems to have no means to deal with this other than
  153. # exception, so here we behave as if the user had Ctrl+C'ed the
  154. # CLI.
  155. # MIH is not confident that this is how it is supposed to be
  156. raise KeyboardInterrupt
  157. return answer
  158. #def error
  159. #def input
  160. #def yesno
  161. #def get_progressbar