GUIElements.py 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220
  1. from PyQt5 import QtGui, QtCore, QtWidgets
  2. from PyQt5.QtCore import pyqtSignal, pyqtSlot
  3. from copy import copy
  4. import re
  5. import logging
  6. log = logging.getLogger('base')
  7. EDIT_SIZE_HINT = 70
  8. class RadioSet(QtWidgets.QWidget):
  9. activated_custom = QtCore.pyqtSignal()
  10. def __init__(self, choices, orientation='horizontal', parent=None, stretch=None):
  11. """
  12. The choices are specified as a list of dictionaries containing:
  13. * 'label': Shown in the UI
  14. * 'value': The value returned is selected
  15. :param choices: List of choices. See description.
  16. :param orientation: 'horizontal' (default) of 'vertical'.
  17. :param parent: Qt parent widget.
  18. :type choices: list
  19. """
  20. super(RadioSet, self).__init__(parent)
  21. self.choices = copy(choices)
  22. if orientation == 'horizontal':
  23. layout = QtWidgets.QHBoxLayout()
  24. else:
  25. layout = QtWidgets.QVBoxLayout()
  26. group = QtWidgets.QButtonGroup(self)
  27. for choice in self.choices:
  28. choice['radio'] = QtWidgets.QRadioButton(choice['label'])
  29. group.addButton(choice['radio'])
  30. layout.addWidget(choice['radio'], stretch=0)
  31. choice['radio'].toggled.connect(self.on_toggle)
  32. layout.setContentsMargins(0, 0, 0, 0)
  33. if stretch is False:
  34. pass
  35. else:
  36. layout.addStretch()
  37. self.setLayout(layout)
  38. self.group_toggle_fn = lambda: None
  39. def on_toggle(self):
  40. # log.debug("Radio toggled")
  41. radio = self.sender()
  42. if radio.isChecked():
  43. self.group_toggle_fn()
  44. self.activated_custom.emit()
  45. return
  46. def get_value(self):
  47. for choice in self.choices:
  48. if choice['radio'].isChecked():
  49. return choice['value']
  50. log.error("No button was toggled in RadioSet.")
  51. return None
  52. def set_value(self, val):
  53. for choice in self.choices:
  54. if choice['value'] == val:
  55. choice['radio'].setChecked(True)
  56. return
  57. log.error("Value given is not part of this RadioSet: %s" % str(val))
  58. # class RadioGroupChoice(QtWidgets.QWidget):
  59. # def __init__(self, label_1, label_2, to_check, hide_list, show_list, parent=None):
  60. # """
  61. # The choices are specified as a list of dictionaries containing:
  62. #
  63. # * 'label': Shown in the UI
  64. # * 'value': The value returned is selected
  65. #
  66. # :param choices: List of choices. See description.
  67. # :param orientation: 'horizontal' (default) of 'vertical'.
  68. # :param parent: Qt parent widget.
  69. # :type choices: list
  70. # """
  71. # super().__init__(parent)
  72. #
  73. # group = QtGui.QButtonGroup(self)
  74. #
  75. # self.lbl1 = label_1
  76. # self.lbl2 = label_2
  77. # self.hide_list = hide_list
  78. # self.show_list = show_list
  79. #
  80. # self.btn1 = QtGui.QRadioButton(str(label_1))
  81. # self.btn2 = QtGui.QRadioButton(str(label_2))
  82. # group.addButton(self.btn1)
  83. # group.addButton(self.btn2)
  84. #
  85. # if to_check == 1:
  86. # self.btn1.setChecked(True)
  87. # else:
  88. # self.btn2.setChecked(True)
  89. #
  90. # self.btn1.toggled.connect(lambda: self.btn_state(self.btn1))
  91. # self.btn2.toggled.connect(lambda: self.btn_state(self.btn2))
  92. #
  93. # def btn_state(self, btn):
  94. # if btn.text() == self.lbl1:
  95. # if btn.isChecked() is True:
  96. # self.show_widgets(self.show_list)
  97. # self.hide_widgets(self.hide_list)
  98. # else:
  99. # self.show_widgets(self.hide_list)
  100. # self.hide_widgets(self.show_list)
  101. #
  102. # def hide_widgets(self, lst):
  103. # for wgt in lst:
  104. # wgt.hide()
  105. #
  106. # def show_widgets(self, lst):
  107. # for wgt in lst:
  108. # wgt.show()
  109. class LengthEntry(QtWidgets.QLineEdit):
  110. def __init__(self, output_units='IN', parent=None):
  111. super(LengthEntry, self).__init__(parent)
  112. self.output_units = output_units
  113. self.format_re = re.compile(r"^([^\s]+)(?:\s([a-zA-Z]+))?$")
  114. # Unit conversion table OUTPUT-INPUT
  115. self.scales = {
  116. 'IN': {'IN': 1.0,
  117. 'MM': 1/25.4},
  118. 'MM': {'IN': 25.4,
  119. 'MM': 1.0}
  120. }
  121. self.readyToEdit = True
  122. def mousePressEvent(self, e, Parent=None):
  123. super(LengthEntry, self).mousePressEvent(e) # required to deselect on 2e click
  124. if self.readyToEdit:
  125. self.selectAll()
  126. self.readyToEdit = False
  127. def focusOutEvent(self, e):
  128. super(LengthEntry, self).focusOutEvent(e) # required to remove cursor on focusOut
  129. self.deselect()
  130. self.readyToEdit = True
  131. def returnPressed(self, *args, **kwargs):
  132. val = self.get_value()
  133. if val is not None:
  134. self.set_text(str(val))
  135. else:
  136. log.warning("Could not interpret entry: %s" % self.get_text())
  137. def get_value(self):
  138. raw = str(self.text()).strip(' ')
  139. # match = self.format_re.search(raw)
  140. try:
  141. units = raw[-2:]
  142. units = self.scales[self.output_units][units.upper()]
  143. value = raw[:-2]
  144. return float(eval(value))*units
  145. except IndexError:
  146. value = raw
  147. return float(eval(value))
  148. except KeyError:
  149. value = raw
  150. return float(eval(value))
  151. except:
  152. log.warning("Could not parse value in entry: %s" % str(raw))
  153. return None
  154. def set_value(self, val):
  155. self.setText(str('%.4f' % val))
  156. def sizeHint(self):
  157. default_hint_size = super(LengthEntry, self).sizeHint()
  158. return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
  159. class FloatEntry(QtWidgets.QLineEdit):
  160. def __init__(self, parent=None):
  161. super(FloatEntry, self).__init__(parent)
  162. self.readyToEdit = True
  163. def mousePressEvent(self, e, Parent=None):
  164. super(FloatEntry, self).mousePressEvent(e) # required to deselect on 2e click
  165. if self.readyToEdit:
  166. self.selectAll()
  167. self.readyToEdit = False
  168. def focusOutEvent(self, e):
  169. super(FloatEntry, self).focusOutEvent(e) # required to remove cursor on focusOut
  170. self.deselect()
  171. self.readyToEdit = True
  172. def returnPressed(self, *args, **kwargs):
  173. val = self.get_value()
  174. if val is not None:
  175. self.set_text(str(val))
  176. else:
  177. log.warning("Could not interpret entry: %s" % self.text())
  178. def get_value(self):
  179. raw = str(self.text()).strip(' ')
  180. evaled = 0.0
  181. try:
  182. evaled = eval(raw)
  183. except:
  184. if evaled is not None:
  185. log.error("Could not evaluate: %s" % str(raw))
  186. return None
  187. return float(evaled)
  188. def set_value(self, val):
  189. if val is not None:
  190. self.setText("%.6f" % val)
  191. else:
  192. self.setText("")
  193. def sizeHint(self):
  194. default_hint_size = super(FloatEntry, self).sizeHint()
  195. return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
  196. class FloatEntry2(QtWidgets.QLineEdit):
  197. def __init__(self, parent=None):
  198. super(FloatEntry2, self).__init__(parent)
  199. self.readyToEdit = True
  200. def mousePressEvent(self, e, Parent=None):
  201. super(FloatEntry2, self).mousePressEvent(e) # required to deselect on 2e click
  202. if self.readyToEdit:
  203. self.selectAll()
  204. self.readyToEdit = False
  205. def focusOutEvent(self, e):
  206. super(FloatEntry2, self).focusOutEvent(e) # required to remove cursor on focusOut
  207. self.deselect()
  208. self.readyToEdit = True
  209. def get_value(self):
  210. raw = str(self.text()).strip(' ')
  211. evaled = 0.0
  212. try:
  213. evaled = eval(raw)
  214. except:
  215. if evaled is not None:
  216. log.error("Could not evaluate: %s" % str(raw))
  217. return None
  218. return float(evaled)
  219. def set_value(self, val):
  220. self.setText("%.6f" % val)
  221. def sizeHint(self):
  222. default_hint_size = super(FloatEntry2, self).sizeHint()
  223. return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
  224. class IntEntry(QtWidgets.QLineEdit):
  225. def __init__(self, parent=None, allow_empty=False, empty_val=None):
  226. super(IntEntry, self).__init__(parent)
  227. self.allow_empty = allow_empty
  228. self.empty_val = empty_val
  229. self.readyToEdit = True
  230. def mousePressEvent(self, e, Parent=None):
  231. super(IntEntry, self).mousePressEvent(e) # required to deselect on 2e click
  232. if self.readyToEdit:
  233. self.selectAll()
  234. self.readyToEdit = False
  235. def focusOutEvent(self, e):
  236. super(IntEntry, self).focusOutEvent(e) # required to remove cursor on focusOut
  237. self.deselect()
  238. self.readyToEdit = True
  239. def get_value(self):
  240. if self.allow_empty:
  241. if str(self.text()) == "":
  242. return self.empty_val
  243. # make the text() first a float and then int because if text is a float type,
  244. # the int() can't convert directly a "text float" into a int type.
  245. ret_val = float(self.text())
  246. ret_val = int(ret_val)
  247. return ret_val
  248. def set_value(self, val):
  249. if val == self.empty_val and self.allow_empty:
  250. self.setText("")
  251. return
  252. self.setText(str(val))
  253. def sizeHint(self):
  254. default_hint_size = super(IntEntry, self).sizeHint()
  255. return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
  256. class FCEntry(QtWidgets.QLineEdit):
  257. def __init__(self, parent=None):
  258. super(FCEntry, self).__init__(parent)
  259. self.readyToEdit = True
  260. def mousePressEvent(self, e, Parent=None):
  261. super(FCEntry, self).mousePressEvent(e) # required to deselect on 2e click
  262. if self.readyToEdit:
  263. self.selectAll()
  264. self.readyToEdit = False
  265. def focusOutEvent(self, e):
  266. super(FCEntry, self).focusOutEvent(e) # required to remove cursor on focusOut
  267. self.deselect()
  268. self.readyToEdit = True
  269. def get_value(self):
  270. return str(self.text())
  271. def set_value(self, val):
  272. self.setText(str(val))
  273. def sizeHint(self):
  274. default_hint_size = super(FCEntry, self).sizeHint()
  275. return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
  276. class FCEntry2(FCEntry):
  277. def __init__(self, parent=None):
  278. super(FCEntry2, self).__init__(parent)
  279. self.readyToEdit = True
  280. def set_value(self, val):
  281. self.setText('%.5f' % float(val))
  282. class EvalEntry(QtWidgets.QLineEdit):
  283. def __init__(self, parent=None):
  284. super(EvalEntry, self).__init__(parent)
  285. self.readyToEdit = True
  286. def mousePressEvent(self, e, Parent=None):
  287. super(EvalEntry, self).mousePressEvent(e) # required to deselect on 2e click
  288. if self.readyToEdit:
  289. self.selectAll()
  290. self.readyToEdit = False
  291. def focusOutEvent(self, e):
  292. super(EvalEntry, self).focusOutEvent(e) # required to remove cursor on focusOut
  293. self.deselect()
  294. self.readyToEdit = True
  295. def returnPressed(self, *args, **kwargs):
  296. val = self.get_value()
  297. if val is not None:
  298. self.setText(str(val))
  299. else:
  300. log.warning("Could not interpret entry: %s" % self.get_text())
  301. def get_value(self):
  302. raw = str(self.text()).strip(' ')
  303. evaled = 0.0
  304. try:
  305. evaled = eval(raw)
  306. except:
  307. if evaled is not None:
  308. log.error("Could not evaluate: %s" % str(raw))
  309. return None
  310. return evaled
  311. def set_value(self, val):
  312. self.setText(str(val))
  313. def sizeHint(self):
  314. default_hint_size = super(EvalEntry, self).sizeHint()
  315. return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
  316. class EvalEntry2(QtWidgets.QLineEdit):
  317. def __init__(self, parent=None):
  318. super(EvalEntry2, self).__init__(parent)
  319. self.readyToEdit = True
  320. def mousePressEvent(self, e, Parent=None):
  321. super(EvalEntry2, self).mousePressEvent(e) # required to deselect on 2e click
  322. if self.readyToEdit:
  323. self.selectAll()
  324. self.readyToEdit = False
  325. def focusOutEvent(self, e):
  326. super(EvalEntry2, self).focusOutEvent(e) # required to remove cursor on focusOut
  327. self.deselect()
  328. self.readyToEdit = True
  329. def get_value(self):
  330. raw = str(self.text()).strip(' ')
  331. evaled = 0.0
  332. try:
  333. evaled = eval(raw)
  334. except:
  335. if evaled is not None:
  336. log.error("Could not evaluate: %s" % str(raw))
  337. return None
  338. return evaled
  339. def set_value(self, val):
  340. self.setText(str(val))
  341. def sizeHint(self):
  342. default_hint_size = super(EvalEntry2, self).sizeHint()
  343. return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
  344. class FCCheckBox(QtWidgets.QCheckBox):
  345. def __init__(self, label='', parent=None):
  346. super(FCCheckBox, self).__init__(str(label), parent)
  347. def get_value(self):
  348. return self.isChecked()
  349. def set_value(self, val):
  350. self.setChecked(val)
  351. def toggle(self):
  352. self.set_value(not self.get_value())
  353. class FCTextArea(QtWidgets.QPlainTextEdit):
  354. def __init__(self, parent=None):
  355. super(FCTextArea, self).__init__(parent)
  356. def set_value(self, val):
  357. self.setPlainText(val)
  358. def get_value(self):
  359. return str(self.toPlainText())
  360. def sizeHint(self):
  361. default_hint_size = super(FCTextArea, self).sizeHint()
  362. return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
  363. class FCTextAreaRich(QtWidgets.QTextEdit):
  364. def __init__(self, parent=None):
  365. super(FCTextAreaRich, self).__init__(parent)
  366. def set_value(self, val):
  367. self.setText(val)
  368. def get_value(self):
  369. return str(self.toPlainText())
  370. def sizeHint(self):
  371. default_hint_size = super(FCTextAreaRich, self).sizeHint()
  372. return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
  373. class FCComboBox(QtWidgets.QComboBox):
  374. def __init__(self, parent=None):
  375. super(FCComboBox, self).__init__(parent)
  376. self.setFocusPolicy(QtCore.Qt.StrongFocus)
  377. def wheelEvent(self, *args, **kwargs):
  378. pass
  379. def get_value(self):
  380. return str(self.currentText())
  381. def set_value(self, val):
  382. self.setCurrentIndex(self.findText(str(val)))
  383. class FCInputDialog(QtWidgets.QInputDialog):
  384. def __init__(self, parent=None, ok=False, val=None, title=None, text=None, min=None, max=None, decimals=None):
  385. super(FCInputDialog, self).__init__(parent)
  386. self.allow_empty = ok
  387. self.empty_val = val
  388. if title is None:
  389. self.title = 'title'
  390. else:
  391. self.title = title
  392. if text is None:
  393. self.text = 'text'
  394. else:
  395. self.text = text
  396. if min is None:
  397. self.min = 0
  398. else:
  399. self.min = min
  400. if max is None:
  401. self.max = 0
  402. else:
  403. self.max = max
  404. if decimals is None:
  405. self.decimals = 6
  406. else:
  407. self.decimals = decimals
  408. def get_value(self):
  409. self.val,self.ok = self.getDouble(self, self.title, self.text, min=self.min,
  410. max=self.max, decimals=self.decimals)
  411. return [self.val, self.ok]
  412. # "Transform", "Enter the Angle value:"
  413. def set_value(self, val):
  414. pass
  415. class FCButton(QtWidgets.QPushButton):
  416. def __init__(self, parent=None):
  417. super(FCButton, self).__init__(parent)
  418. def get_value(self):
  419. return self.isChecked()
  420. def set_value(self, val):
  421. self.setText(str(val))
  422. class FCTab(QtWidgets.QTabWidget):
  423. def __init__(self, parent=None):
  424. super(FCTab, self).__init__(parent)
  425. self.setTabsClosable(True)
  426. self.tabCloseRequested.connect(self.closeTab)
  427. def deleteTab(self, currentIndex):
  428. widget = self.widget(currentIndex)
  429. if widget is not None:
  430. widget.deleteLater()
  431. self.removeTab(currentIndex)
  432. def closeTab(self, currentIndex):
  433. self.removeTab(currentIndex)
  434. def protectTab(self, currentIndex):
  435. self.tabBar().setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
  436. class FCDetachableTab(QtWidgets.QTabWidget):
  437. # From here: https://stackoverflow.com/questions/47267195/in-pyqt4-is-it-possible-to-detach-tabs-from-a-qtabwidget
  438. def __init__(self, protect=None, protect_by_name=None, parent=None):
  439. super().__init__()
  440. self.tabBar = self.FCTabBar(self)
  441. self.tabBar.onDetachTabSignal.connect(self.detachTab)
  442. self.tabBar.onMoveTabSignal.connect(self.moveTab)
  443. self.tabBar.detachedTabDropSignal.connect(self.detachedTabDrop)
  444. self.setTabBar(self.tabBar)
  445. # Used to keep a reference to detached tabs since their QMainWindow
  446. # does not have a parent
  447. self.detachedTabs = {}
  448. # a way to make sure that tabs can't be closed after they attach to the parent tab
  449. self.protect_tab = True if protect is not None and protect is True else False
  450. self.protect_by_name = protect_by_name if isinstance(protect_by_name, list) else None
  451. # Close all detached tabs if the application is closed explicitly
  452. QtWidgets.qApp.aboutToQuit.connect(self.closeDetachedTabs) # @UndefinedVariable
  453. # used by the property self.useOldIndex(param)
  454. self.use_old_index = None
  455. self.old_index = None
  456. self.setTabsClosable(True)
  457. self.tabCloseRequested.connect(self.closeTab)
  458. def useOldIndex(self, param):
  459. if param:
  460. self.use_old_index = True
  461. else:
  462. self.use_old_index = False
  463. def deleteTab(self, currentIndex):
  464. widget = self.widget(currentIndex)
  465. if widget is not None:
  466. widget.deleteLater()
  467. self.removeTab(currentIndex)
  468. def closeTab(self, currentIndex):
  469. self.removeTab(currentIndex)
  470. def protectTab(self, currentIndex):
  471. # self.FCTabBar().setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
  472. self.tabBar.setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
  473. ##
  474. # The default movable functionality of QTabWidget must remain disabled
  475. # so as not to conflict with the added features
  476. def setMovable(self, movable):
  477. pass
  478. ##
  479. # Move a tab from one position (index) to another
  480. #
  481. # @param fromIndex the original index location of the tab
  482. # @param toIndex the new index location of the tab
  483. @pyqtSlot(int, int)
  484. def moveTab(self, fromIndex, toIndex):
  485. widget = self.widget(fromIndex)
  486. icon = self.tabIcon(fromIndex)
  487. text = self.tabText(fromIndex)
  488. self.removeTab(fromIndex)
  489. self.insertTab(toIndex, widget, icon, text)
  490. self.setCurrentIndex(toIndex)
  491. ##
  492. # Detach the tab by removing it's contents and placing them in
  493. # a DetachedTab window
  494. #
  495. # @param index the index location of the tab to be detached
  496. # @param point the screen position for creating the new DetachedTab window
  497. @pyqtSlot(int, QtCore.QPoint)
  498. def detachTab(self, index, point):
  499. self.old_index = index
  500. # Get the tab content
  501. name = self.tabText(index)
  502. icon = self.tabIcon(index)
  503. if icon.isNull():
  504. icon = self.window().windowIcon()
  505. contentWidget = self.widget(index)
  506. try:
  507. contentWidgetRect = contentWidget.frameGeometry()
  508. except AttributeError:
  509. return
  510. # Create a new detached tab window
  511. detachedTab = self.FCDetachedTab(name, contentWidget)
  512. detachedTab.setWindowModality(QtCore.Qt.NonModal)
  513. detachedTab.setWindowIcon(icon)
  514. detachedTab.setGeometry(contentWidgetRect)
  515. detachedTab.onCloseSignal.connect(self.attachTab)
  516. detachedTab.onDropSignal.connect(self.tabBar.detachedTabDrop)
  517. detachedTab.move(point)
  518. detachedTab.show()
  519. # Create a reference to maintain access to the detached tab
  520. self.detachedTabs[name] = detachedTab
  521. ##
  522. # Re-attach the tab by removing the content from the DetachedTab window,
  523. # closing it, and placing the content back into the DetachableTabWidget
  524. #
  525. # @param contentWidget the content widget from the DetachedTab window
  526. # @param name the name of the detached tab
  527. # @param icon the window icon for the detached tab
  528. # @param insertAt insert the re-attached tab at the given index
  529. def attachTab(self, contentWidget, name, icon, insertAt=None):
  530. # Make the content widget a child of this widget
  531. contentWidget.setParent(self)
  532. # Remove the reference
  533. del self.detachedTabs[name]
  534. # helps in restoring the tab to the same index that it was before was detached
  535. insert_index = self.old_index if self.use_old_index is True else insertAt
  536. # Create an image from the given icon (for comparison)
  537. if not icon.isNull():
  538. try:
  539. tabIconPixmap = icon.pixmap(icon.availableSizes()[0])
  540. tabIconImage = tabIconPixmap.toImage()
  541. except IndexError:
  542. tabIconImage = None
  543. else:
  544. tabIconImage = None
  545. # Create an image of the main window icon (for comparison)
  546. if not icon.isNull():
  547. try:
  548. windowIconPixmap = self.window().windowIcon().pixmap(icon.availableSizes()[0])
  549. windowIconImage = windowIconPixmap.toImage()
  550. except IndexError:
  551. windowIconImage = None
  552. else:
  553. windowIconImage = None
  554. # Determine if the given image and the main window icon are the same.
  555. # If they are, then do not add the icon to the tab
  556. if tabIconImage == windowIconImage:
  557. if insert_index is None:
  558. index = self.addTab(contentWidget, name)
  559. else:
  560. index = self.insertTab(insert_index, contentWidget, name)
  561. else:
  562. if insert_index is None:
  563. index = self.addTab(contentWidget, icon, name)
  564. else:
  565. index = self.insertTab(insert_index, contentWidget, icon, name)
  566. # on reattaching the tab if protect is true then the closure button is not added
  567. if self.protect_tab is True:
  568. self.protectTab(index)
  569. # on reattaching the tab disable the closure button for the tabs with the name in the self.protect_by_name list
  570. if self.protect_by_name is not None:
  571. for tab_name in self.protect_by_name:
  572. for index in range(self.count()):
  573. if str(tab_name) == str(self.tabText(index)):
  574. self.protectTab(index)
  575. # Make this tab the current tab
  576. if index > -1:
  577. self.setCurrentIndex(insert_index) if self.use_old_index else self.setCurrentIndex(index)
  578. ##
  579. # Remove the tab with the given name, even if it is detached
  580. #
  581. # @param name the name of the tab to be removed
  582. def removeTabByName(self, name):
  583. # Remove the tab if it is attached
  584. attached = False
  585. for index in range(self.count()):
  586. if str(name) == str(self.tabText(index)):
  587. self.removeTab(index)
  588. attached = True
  589. break
  590. # If the tab is not attached, close it's window and
  591. # remove the reference to it
  592. if not attached:
  593. for key in self.detachedTabs:
  594. if str(name) == str(key):
  595. self.detachedTabs[key].onCloseSignal.disconnect()
  596. self.detachedTabs[key].close()
  597. del self.detachedTabs[key]
  598. break
  599. ##
  600. # Handle dropping of a detached tab inside the DetachableTabWidget
  601. #
  602. # @param name the name of the detached tab
  603. # @param index the index of an existing tab (if the tab bar
  604. # determined that the drop occurred on an
  605. # existing tab)
  606. # @param dropPos the mouse cursor position when the drop occurred
  607. @QtCore.pyqtSlot(str, int, QtCore.QPoint)
  608. def detachedTabDrop(self, name, index, dropPos):
  609. # If the drop occurred on an existing tab, insert the detached
  610. # tab at the existing tab's location
  611. if index > -1:
  612. # Create references to the detached tab's content and icon
  613. contentWidget = self.detachedTabs[name].contentWidget
  614. icon = self.detachedTabs[name].windowIcon()
  615. # Disconnect the detached tab's onCloseSignal so that it
  616. # does not try to re-attach automatically
  617. self.detachedTabs[name].onCloseSignal.disconnect()
  618. # Close the detached
  619. self.detachedTabs[name].close()
  620. # Re-attach the tab at the given index
  621. self.attachTab(contentWidget, name, icon, index)
  622. # If the drop did not occur on an existing tab, determine if the drop
  623. # occurred in the tab bar area (the area to the side of the QTabBar)
  624. else:
  625. # Find the drop position relative to the DetachableTabWidget
  626. tabDropPos = self.mapFromGlobal(dropPos)
  627. # If the drop position is inside the DetachableTabWidget...
  628. if self.rect().contains(tabDropPos):
  629. # If the drop position is inside the tab bar area (the
  630. # area to the side of the QTabBar) or there are not tabs
  631. # currently attached...
  632. if tabDropPos.y() < self.tabBar.height() or self.count() == 0:
  633. # Close the detached tab and allow it to re-attach
  634. # automatically
  635. self.detachedTabs[name].close()
  636. ##
  637. # Close all tabs that are currently detached.
  638. def closeDetachedTabs(self):
  639. listOfDetachedTabs = []
  640. for key in self.detachedTabs:
  641. listOfDetachedTabs.append(self.detachedTabs[key])
  642. for detachedTab in listOfDetachedTabs:
  643. detachedTab.close()
  644. ##
  645. # When a tab is detached, the contents are placed into this QMainWindow. The tab
  646. # can be re-attached by closing the dialog or by dragging the window into the tab bar
  647. class FCDetachedTab(QtWidgets.QMainWindow):
  648. onCloseSignal = pyqtSignal(QtWidgets.QWidget, str, QtGui.QIcon)
  649. onDropSignal = pyqtSignal(str, QtCore.QPoint)
  650. def __init__(self, name, contentWidget):
  651. QtWidgets.QMainWindow.__init__(self, None)
  652. self.setObjectName(name)
  653. self.setWindowTitle(name)
  654. self.contentWidget = contentWidget
  655. self.setCentralWidget(self.contentWidget)
  656. self.contentWidget.show()
  657. self.windowDropFilter = self.WindowDropFilter()
  658. self.installEventFilter(self.windowDropFilter)
  659. self.windowDropFilter.onDropSignal.connect(self.windowDropSlot)
  660. ##
  661. # Handle a window drop event
  662. #
  663. # @param dropPos the mouse cursor position of the drop
  664. @QtCore.pyqtSlot(QtCore.QPoint)
  665. def windowDropSlot(self, dropPos):
  666. self.onDropSignal.emit(self.objectName(), dropPos)
  667. ##
  668. # If the window is closed, emit the onCloseSignal and give the
  669. # content widget back to the DetachableTabWidget
  670. #
  671. # @param event a close event
  672. def closeEvent(self, event):
  673. self.onCloseSignal.emit(self.contentWidget, self.objectName(), self.windowIcon())
  674. ##
  675. # An event filter class to detect a QMainWindow drop event
  676. class WindowDropFilter(QtCore.QObject):
  677. onDropSignal = pyqtSignal(QtCore.QPoint)
  678. def __init__(self):
  679. QtCore.QObject.__init__(self)
  680. self.lastEvent = None
  681. ##
  682. # Detect a QMainWindow drop event by looking for a NonClientAreaMouseMove (173)
  683. # event that immediately follows a Move event
  684. #
  685. # @param obj the object that generated the event
  686. # @param event the current event
  687. def eventFilter(self, obj, event):
  688. # If a NonClientAreaMouseMove (173) event immediately follows a Move event...
  689. if self.lastEvent == QtCore.QEvent.Move and event.type() == 173:
  690. # Determine the position of the mouse cursor and emit it with the
  691. # onDropSignal
  692. mouseCursor = QtGui.QCursor()
  693. dropPos = mouseCursor.pos()
  694. self.onDropSignal.emit(dropPos)
  695. self.lastEvent = event.type()
  696. return True
  697. else:
  698. self.lastEvent = event.type()
  699. return False
  700. class FCTabBar(QtWidgets.QTabBar):
  701. onDetachTabSignal = pyqtSignal(int, QtCore.QPoint)
  702. onMoveTabSignal = pyqtSignal(int, int)
  703. detachedTabDropSignal = pyqtSignal(str, int, QtCore.QPoint)
  704. def __init__(self, parent=None):
  705. QtWidgets.QTabBar.__init__(self, parent)
  706. self.setAcceptDrops(True)
  707. self.setElideMode(QtCore.Qt.ElideRight)
  708. self.setSelectionBehaviorOnRemove(QtWidgets.QTabBar.SelectLeftTab)
  709. self.dragStartPos = QtCore.QPoint()
  710. self.dragDropedPos = QtCore.QPoint()
  711. self.mouseCursor = QtGui.QCursor()
  712. self.dragInitiated = False
  713. # Send the onDetachTabSignal when a tab is double clicked
  714. #
  715. # @param event a mouse double click event
  716. def mouseDoubleClickEvent(self, event):
  717. event.accept()
  718. self.onDetachTabSignal.emit(self.tabAt(event.pos()), self.mouseCursor.pos())
  719. # Set the starting position for a drag event when the mouse button is pressed
  720. #
  721. # @param event a mouse press event
  722. def mousePressEvent(self, event):
  723. if event.button() == QtCore.Qt.LeftButton:
  724. self.dragStartPos = event.pos()
  725. self.dragDropedPos.setX(0)
  726. self.dragDropedPos.setY(0)
  727. self.dragInitiated = False
  728. QtWidgets.QTabBar.mousePressEvent(self, event)
  729. # Determine if the current movement is a drag. If it is, convert it into a QDrag. If the
  730. # drag ends inside the tab bar, emit an onMoveTabSignal. If the drag ends outside the tab
  731. # bar, emit an onDetachTabSignal.
  732. #
  733. # @param event a mouse move event
  734. def mouseMoveEvent(self, event):
  735. # Determine if the current movement is detected as a drag
  736. if not self.dragStartPos.isNull() and ((event.pos() - self.dragStartPos).manhattanLength() < QtWidgets.QApplication.startDragDistance()):
  737. self.dragInitiated = True
  738. # If the current movement is a drag initiated by the left button
  739. if (((event.buttons() & QtCore.Qt.LeftButton)) and self.dragInitiated):
  740. # Stop the move event
  741. finishMoveEvent = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), QtCore.Qt.NoButton, QtCore.Qt.NoButton, QtCore.Qt.NoModifier)
  742. QtWidgets.QTabBar.mouseMoveEvent(self, finishMoveEvent)
  743. # Convert the move event into a drag
  744. drag = QtGui.QDrag(self)
  745. mimeData = QtCore.QMimeData()
  746. # mimeData.setData('action', 'application/tab-detach')
  747. drag.setMimeData(mimeData)
  748. # screen = QScreen(self.parentWidget().currentWidget().winId())
  749. # Create the appearance of dragging the tab content
  750. try:
  751. pixmap = self.parent().widget(self.tabAt(self.dragStartPos)).grab()
  752. except Exception as e:
  753. log.debug("GUIElements.FCDetachable. FCTabBar.mouseMoveEvent() --> %s" % str(e))
  754. return
  755. targetPixmap = QtGui.QPixmap(pixmap.size())
  756. targetPixmap.fill(QtCore.Qt.transparent)
  757. painter = QtGui.QPainter(targetPixmap)
  758. painter.setOpacity(0.85)
  759. painter.drawPixmap(0, 0, pixmap)
  760. painter.end()
  761. drag.setPixmap(targetPixmap)
  762. # Initiate the drag
  763. dropAction = drag.exec_(QtCore.Qt.MoveAction | QtCore.Qt.CopyAction)
  764. # For Linux: Here, drag.exec_() will not return MoveAction on Linux. So it
  765. # must be set manually
  766. if self.dragDropedPos.x() != 0 and self.dragDropedPos.y() != 0:
  767. dropAction = QtCore.Qt.MoveAction
  768. # If the drag completed outside of the tab bar, detach the tab and move
  769. # the content to the current cursor position
  770. if dropAction == QtCore.Qt.IgnoreAction:
  771. event.accept()
  772. self.onDetachTabSignal.emit(self.tabAt(self.dragStartPos), self.mouseCursor.pos())
  773. # Else if the drag completed inside the tab bar, move the selected tab to the new position
  774. elif dropAction == QtCore.Qt.MoveAction:
  775. if not self.dragDropedPos.isNull():
  776. event.accept()
  777. self.onMoveTabSignal.emit(self.tabAt(self.dragStartPos), self.tabAt(self.dragDropedPos))
  778. else:
  779. QtWidgets.QTabBar.mouseMoveEvent(self, event)
  780. # Determine if the drag has entered a tab position from another tab position
  781. #
  782. # @param event a drag enter event
  783. def dragEnterEvent(self, event):
  784. mimeData = event.mimeData()
  785. # formats = mcd imeData.formats()
  786. # if formats.contains('action') and mimeData.data('action') == 'application/tab-detach':
  787. # event.acceptProposedAction()
  788. QtWidgets.QTabBar.dragMoveEvent(self, event)
  789. # Get the position of the end of the drag
  790. #
  791. # @param event a drop event
  792. def dropEvent(self, event):
  793. self.dragDropedPos = event.pos()
  794. QtWidgets.QTabBar.dropEvent(self, event)
  795. # Determine if the detached tab drop event occurred on an existing tab,
  796. # then send the event to the DetachableTabWidget
  797. def detachedTabDrop(self, name, dropPos):
  798. tabDropPos = self.mapFromGlobal(dropPos)
  799. index = self.tabAt(tabDropPos)
  800. self.detachedTabDropSignal.emit(name, index, dropPos)
  801. class VerticalScrollArea(QtWidgets.QScrollArea):
  802. """
  803. This widget extends QtGui.QScrollArea to make a vertical-only
  804. scroll area that also expands horizontally to accomodate
  805. its contents.
  806. """
  807. def __init__(self, parent=None):
  808. QtWidgets.QScrollArea.__init__(self, parent=parent)
  809. self.setWidgetResizable(True)
  810. self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  811. self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
  812. def eventFilter(self, source, event):
  813. """
  814. The event filter gets automatically installed when setWidget()
  815. is called.
  816. :param source:
  817. :param event:
  818. :return:
  819. """
  820. if event.type() == QtCore.QEvent.Resize and source == self.widget():
  821. # log.debug("VerticalScrollArea: Widget resized:")
  822. # log.debug(" minimumSizeHint().width() = %d" % self.widget().minimumSizeHint().width())
  823. # log.debug(" verticalScrollBar().width() = %d" % self.verticalScrollBar().width())
  824. self.setMinimumWidth(self.widget().sizeHint().width() +
  825. self.verticalScrollBar().sizeHint().width())
  826. # if self.verticalScrollBar().isVisible():
  827. # log.debug(" Scroll bar visible")
  828. # self.setMinimumWidth(self.widget().minimumSizeHint().width() +
  829. # self.verticalScrollBar().width())
  830. # else:
  831. # log.debug(" Scroll bar hidden")
  832. # self.setMinimumWidth(self.widget().minimumSizeHint().width())
  833. return QtWidgets.QWidget.eventFilter(self, source, event)
  834. class OptionalInputSection:
  835. def __init__(self, cb, optinputs, logic=True):
  836. """
  837. Associates the a checkbox with a set of inputs.
  838. :param cb: Checkbox that enables the optional inputs.
  839. :param optinputs: List of widgets that are optional.
  840. :param logic: When True the logic is normal, when False the logic is in reverse
  841. It means that for logic=True, when the checkbox is checked the widgets are Enabled, and
  842. for logic=False, when the checkbox is checked the widgets are Disabled
  843. :return:
  844. """
  845. assert isinstance(cb, FCCheckBox), \
  846. "Expected an FCCheckBox, got %s" % type(cb)
  847. self.cb = cb
  848. self.optinputs = optinputs
  849. self.logic = logic
  850. self.on_cb_change()
  851. self.cb.stateChanged.connect(self.on_cb_change)
  852. def on_cb_change(self):
  853. if self.cb.checkState():
  854. for widget in self.optinputs:
  855. if self.logic is True:
  856. widget.setEnabled(True)
  857. else:
  858. widget.setEnabled(False)
  859. else:
  860. for widget in self.optinputs:
  861. if self.logic is True:
  862. widget.setEnabled(False)
  863. else:
  864. widget.setEnabled(True)
  865. class FCTable(QtWidgets.QTableWidget):
  866. def __init__(self, parent=None):
  867. super(FCTable, self).__init__(parent)
  868. def sizeHint(self):
  869. default_hint_size = super(FCTable, self).sizeHint()
  870. return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
  871. def getHeight(self):
  872. height = self.horizontalHeader().height()
  873. for i in range(self.rowCount()):
  874. height += self.rowHeight(i)
  875. return height
  876. def getWidth(self):
  877. width = self.verticalHeader().width()
  878. for i in range(self.columnCount()):
  879. width += self.columnWidth(i)
  880. return width
  881. # color is in format QtGui.Qcolor(r, g, b, alfa) with or without alfa
  882. def setColortoRow(self, rowIndex, color):
  883. for j in range(self.columnCount()):
  884. self.item(rowIndex, j).setBackground(color)
  885. # if user is clicking an blank area inside the QTableWidget it will deselect currently selected rows
  886. def mousePressEvent(self, event):
  887. if self.itemAt(event.pos()) is None:
  888. self.clearSelection()
  889. else:
  890. QtWidgets.QTableWidget.mousePressEvent(self, event)
  891. def setupContextMenu(self):
  892. self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
  893. def addContextMenu(self, entry, call_function, icon=None):
  894. action_name = str(entry)
  895. action = QtWidgets.QAction(self)
  896. action.setText(action_name)
  897. if icon:
  898. assert isinstance(icon, QtGui.QIcon), \
  899. "Expected the argument to be QtGui.QIcon. Instead it is %s" % type(icon)
  900. action.setIcon(icon)
  901. self.addAction(action)
  902. action.triggered.connect(call_function)
  903. class FCSpinner(QtWidgets.QSpinBox):
  904. def __init__(self, parent=None):
  905. super(FCSpinner, self).__init__(parent)
  906. def get_value(self):
  907. return str(self.value())
  908. def set_value(self, val):
  909. try:
  910. k = int(val)
  911. except Exception as e:
  912. log.debug(str(e))
  913. return
  914. self.setValue(k)
  915. # def sizeHint(self):
  916. # default_hint_size = super(FCSpinner, self).sizeHint()
  917. # return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
  918. class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
  919. def __init__(self, parent=None):
  920. super(FCDoubleSpinner, self).__init__(parent)
  921. self.readyToEdit = True
  922. def mousePressEvent(self, e, parent=None):
  923. super(FCDoubleSpinner, self).mousePressEvent(e) # required to deselect on 2e click
  924. if self.readyToEdit:
  925. self.lineEdit().selectAll()
  926. self.readyToEdit = False
  927. def focusOutEvent(self, e):
  928. super(FCDoubleSpinner, self).focusOutEvent(e) # required to remove cursor on focusOut
  929. self.lineEdit().deselect()
  930. self.readyToEdit = True
  931. def get_value(self):
  932. return str(self.value())
  933. def set_value(self, val):
  934. try:
  935. k = int(val)
  936. except Exception as e:
  937. log.debug(str(e))
  938. return
  939. self.setValue(k)
  940. def set_precision(self, val):
  941. self.setDecimals(val)
  942. def set_range(self, min_val, max_val):
  943. self.setRange(self, min_val, max_val)
  944. class Dialog_box(QtWidgets.QWidget):
  945. def __init__(self, title=None, label=None):
  946. """
  947. :param title: string with the window title
  948. :param label: string with the message inside the dialog box
  949. """
  950. super(Dialog_box, self).__init__()
  951. self.location = (0, 0)
  952. self.ok = False
  953. dialog_box = QtWidgets.QInputDialog()
  954. dialog_box.setFixedWidth(270)
  955. self.location, self.ok = dialog_box.getText(self, title, label)