You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

462 lines
12 KiB

  1. '''
  2. Created on Jan 1, 2014
  3. @author: Chris
  4. TODO:
  5. Sanitize all GetValue inputs
  6. (to check that there's actual data there.
  7. '''
  8. import wx
  9. from abc import ABCMeta
  10. from abc import abstractmethod
  11. from gooey.gui import styling
  12. EMPTY = ''
  13. class BuildException(RuntimeError):
  14. pass
  15. class AbstractGuiComponent(object):
  16. '''
  17. Template pattern-y abstract class for the gui.
  18. Children must all implement the BuildWidget and getValue
  19. methods.
  20. '''
  21. __metaclass__ = ABCMeta
  22. def __init__(self):
  23. self._widget = None
  24. self.msg = EMPTY
  25. def Build(self, parent):
  26. self._widget = self.BuildInputWidget(parent, self._action)
  27. if self.HasHelpMsg(self._action):
  28. self._msg = self.CreateHelpMsgWidget(parent, self._action)
  29. else:
  30. self._msg = None
  31. sizer = wx.BoxSizer(wx.VERTICAL)
  32. sizer.Add(self.CreateNameLabelWidget(parent, self._action))
  33. sizer.AddSpacer(2)
  34. if self._msg:
  35. sizer.Add(self._msg, 0, wx.EXPAND)
  36. sizer.AddSpacer(2)
  37. else:
  38. sizer.AddStretchSpacer(1)
  39. sizer.AddStretchSpacer(1)
  40. sizer.Add(self._widget, 0, wx.EXPAND)
  41. return sizer
  42. @abstractmethod
  43. def BuildInputWidget(self, parent, action):
  44. ''' Must construct the main widget type for the Action '''
  45. pass
  46. def HasHelpMsg(self, action):
  47. return action.help is not None
  48. def CreateHelpMsgWidget(self, parent, action):
  49. base_text = wx.StaticText(parent, label=action.help)
  50. if self.HasNargs(action):
  51. base_text.SetLabelText(base_text.GetLabelText() + self.CreateNargsMsg(action))
  52. styling.MakeDarkGrey(base_text)
  53. return base_text
  54. def HasNargs(self, action):
  55. return action.nargs is not None and action.nargs is not 0
  56. def CreateNargsMsg(self, action):
  57. if isinstance(action.nargs, int):
  58. return '\n(Note: exactly {0} arguments are required)'.format(action.nargs)
  59. elif action.nargs == '+':
  60. return '\n(Note: at least 1 or more arguments are required)'
  61. return ''
  62. def CreateNameLabelWidget(self, parent, action):
  63. label = str(action.dest).title()
  64. if len(action.option_strings) > 1:
  65. label += ' (%s)' % action.option_strings[0]
  66. text = wx.StaticText(parent, label=label)
  67. styling.MakeBold(text)
  68. return text
  69. def AssertInitialization(self, clsname):
  70. if not self._widget:
  71. raise BuildException('%s was not correctly initialized' % clsname)
  72. def __str__(self):
  73. return str(self._action)
  74. @abstractmethod
  75. def GetValue(self):
  76. ''' Returns the state of the given widget '''
  77. pass
  78. def Update(self, size):
  79. '''
  80. Manually word wraps the StaticText help objects which would
  81. otherwise not wrap on resize
  82. Content area is based on each grid having two equally sized
  83. columns, where the content area is defined as 87% of the halved
  84. windows width. The wiggle room is the distance +- 10% of the
  85. content_area.
  86. Wrap calculation is run only when the size of the help_msg
  87. extends outside of the wiggle_room. This was done to avoid
  88. the "flickering" that comes from constantly resizing a
  89. StaticText object.
  90. '''
  91. if self._msg is None:
  92. return
  93. help_msg = self._msg
  94. width, height = size
  95. content_area = int((width / 2) * .87)
  96. wiggle_room = range(int(content_area - content_area * .05), int(content_area + content_area * .05))
  97. if help_msg.Size[0] not in wiggle_room:
  98. self._msg.SetLabel(self._msg.GetLabelText().replace('\n', ' '))
  99. self._msg.Wrap(content_area)
  100. class AbstractComponent(object):
  101. '''
  102. Template pattern-y abstract class for the gui.
  103. Children must all implement the BuildWidget and getValue
  104. methods.
  105. '''
  106. __metaclass__ = ABCMeta
  107. def __init__(self):
  108. self._widget = None
  109. self.msg = EMPTY
  110. def Build(self, parent):
  111. self._widget = self.BuildInputWidget(parent, self._action)
  112. if self.HasHelpMsg(self._action):
  113. self._msg = self.CreateHelpMsgWidget(parent, self._action)
  114. else:
  115. self._msg = None
  116. sizer = wx.BoxSizer(wx.VERTICAL)
  117. sizer.Add(self.CreateNameLabelWidget(parent, self._action))
  118. sizer.AddSpacer(2)
  119. if self._msg:
  120. sizer.Add(self._msg, 0, wx.EXPAND)
  121. sizer.AddSpacer(2)
  122. else:
  123. sizer.AddStretchSpacer(1)
  124. sizer.AddStretchSpacer(1)
  125. sizer.Add(self._widget, 0, wx.EXPAND)
  126. return sizer
  127. @abstractmethod
  128. def BuildInputWidget(self, parent, action):
  129. ''' Must construct the main widget type for the Action '''
  130. pass
  131. def HasHelpMsg(self, action):
  132. return action.help is not None
  133. def CreateHelpMsgWidget(self, parent, action):
  134. base_text = wx.StaticText(parent, label=action.help)
  135. if self.HasNargs(action):
  136. base_text.SetLabelText(base_text.GetLabelText() + self.CreateNargsMsg(action))
  137. styling.MakeDarkGrey(base_text)
  138. return base_text
  139. def HasNargs(self, action):
  140. return action.nargs is not None and action.nargs is not 0
  141. def CreateNargsMsg(self, action):
  142. if isinstance(action.nargs, int):
  143. return '\n(Note: exactly {} arguments are required)'.format(action.nargs)
  144. elif action.nargs == '+':
  145. return '\n(Note: at least 1 or more arguments are required)'
  146. return ''
  147. def CreateNameLabelWidget(self, parent, action):
  148. label = str(action.dest).title()
  149. if len(action.option_strings) > 1:
  150. label += ' (%s)' % action.option_strings[0]
  151. text = wx.StaticText(parent, label=label)
  152. styling.MakeBold(text)
  153. return text
  154. def AssertInitialization(self, clsname):
  155. if not self._widget:
  156. raise BuildException('%s was not correctly initialized' % clsname)
  157. def __str__(self):
  158. return str(self._action)
  159. @abstractmethod
  160. def GetValue(self):
  161. ''' Returns the state of the given widget '''
  162. pass
  163. def Update(self, size):
  164. '''
  165. Manually word wraps the StaticText help objects which would
  166. otherwise not wrap on resize
  167. Content area is based on each grid having two equally sized
  168. columns, where the content area is defined as 87% of the halved
  169. windows width. The wiggle room is the distance +- 10% of the
  170. content_area.
  171. Wrap calculation is run only when the size of the help_msg
  172. extends outside of the wiggle_room. This was done to avoid
  173. the "flickering" that comes from constantly resizing a
  174. StaticText object.
  175. '''
  176. if self._msg is None:
  177. return
  178. help_msg = self._msg
  179. width, height = size
  180. content_area = int((width / 2) * .87)
  181. wiggle_room = range(int(content_area - content_area * .05), int(content_area + content_area * .05))
  182. if help_msg.Size[0] not in wiggle_room:
  183. self._msg.SetLabel(self._msg.GetLabelText().replace('\n', ' '))
  184. self._msg.Wrap(content_area)
  185. class Positional(AbstractComponent):
  186. """
  187. Represents a positional argument in a program
  188. e.g.
  189. mypyfile.py param1 <-- this guy
  190. """
  191. def __init__(self, action):
  192. self._action = action
  193. self._widget = None
  194. self.contents = None
  195. def BuildInputWidget(self, parent, action):
  196. return wx.TextCtrl(parent)
  197. def GetValue(self):
  198. '''
  199. Positionals have no associated options_string,
  200. so only the supplied arguments are returned.
  201. The order is assumed to be the same as the order
  202. of declaration in the client code
  203. Returns
  204. "argument_value"
  205. '''
  206. self.AssertInitialization('Positional')
  207. if str(self._widget.GetValue()) == EMPTY:
  208. return None
  209. return self._widget.GetValue()
  210. class Choice(AbstractComponent):
  211. """ A dropdown box """
  212. _DEFAULT_VALUE = 'Select Option'
  213. def __init__(self, action):
  214. self._action = action
  215. self._widget = None
  216. self.contents = None
  217. def GetValue(self):
  218. '''
  219. Returns
  220. "--option_name argument"
  221. '''
  222. self.AssertInitialization('Choice')
  223. if self._widget.GetValue() == self._DEFAULT_VALUE:
  224. return None
  225. return ' '.join(
  226. [self._action.option_strings[0] if self._action.option_strings else '', # get the verbose copy if available
  227. self._widget.GetValue()])
  228. def BuildInputWidget(self, parent, action):
  229. return wx.ComboBox(
  230. parent=parent,
  231. id=-1,
  232. value=self._DEFAULT_VALUE,
  233. choices=action.choices,
  234. style=wx.CB_DROPDOWN
  235. )
  236. class Optional(AbstractComponent):
  237. def __init__(self, action):
  238. self._action = action
  239. self._widget = None
  240. self.contents = None
  241. def BuildInputWidget(self, parent, action):
  242. return wx.TextCtrl(parent)
  243. def GetValue(self):
  244. '''
  245. General options are key/value style pairs (conceptually).
  246. Thus the name of the option, as well as the argument to it
  247. are returned
  248. e.g.
  249. >>> myscript --outfile myfile.txt
  250. returns
  251. "--Option Value"
  252. '''
  253. self.AssertInitialization('Optional')
  254. value = self._widget.GetValue()
  255. if not value:
  256. return None
  257. return ' '.join(
  258. [self._action.option_strings[0], # get the verbose copy if available
  259. value])
  260. class Flag(AbstractComponent):
  261. def __init__(self, action):
  262. self._action = action
  263. self._widget = None
  264. self.contents = None
  265. def Build(self, parent):
  266. self._widget = self.BuildInputWidget(parent, self._action)
  267. self._msg = (self.CreateHelpMsgWidget(parent, self._action)
  268. if self.HasHelpMsg(self._action)
  269. else None)
  270. sizer = wx.BoxSizer(wx.VERTICAL)
  271. sizer.Add(self.CreateNameLabelWidget(parent, self._action))
  272. sizer.AddSpacer(6)
  273. if self.HasNargs(self._action):
  274. sizer.Add(self.CreateNargsMsg(parent, self._action))
  275. if self._msg:
  276. hsizer = self.buildHorizonalMsgSizer(parent)
  277. sizer.Add(hsizer, 1, wx.EXPAND)
  278. else:
  279. sizer.AddStretchSpacer(1)
  280. sizer.Add(self._widget, 0, wx.EXPAND)
  281. return sizer
  282. def BuildInputWidget(self, parent, action):
  283. return wx.CheckBox(parent, -1, label='')
  284. def buildHorizonalMsgSizer(self, panel):
  285. if not self._msg:
  286. return None
  287. sizer = wx.BoxSizer(wx.HORIZONTAL)
  288. sizer.Add(self._widget, 0)
  289. sizer.AddSpacer(6)
  290. sizer.Add(self._msg, 1, wx.EXPAND)
  291. return sizer
  292. def GetValue(self):
  293. '''
  294. Flag options have no param associated with them.
  295. Thus we only need the name of the option.
  296. e.g
  297. >>> Python -v myscript
  298. returns
  299. Options name for argument (-v)
  300. '''
  301. if self._widget.GetValue():
  302. return self._action.option_strings[0]
  303. def Update(self, size):
  304. '''
  305. Custom wrapper calculator to account for the
  306. increased size of the _msg widget after being
  307. inlined with the wx.CheckBox
  308. '''
  309. if self._msg is None:
  310. return
  311. help_msg = self._msg
  312. width, height = size
  313. content_area = int((width / 3) * .70)
  314. wiggle_room = range(int(content_area - content_area * .05), int(content_area + content_area * .05))
  315. if help_msg.Size[0] not in wiggle_room:
  316. self._msg.SetLabel(self._msg.GetLabelText().replace('\n', ' '))
  317. self._msg.Wrap(content_area)
  318. class Counter(AbstractComponent):
  319. def __init__(self, action):
  320. self._action = action
  321. self._widget = None
  322. self.contents = None
  323. def BuildInputWidget(self, parent, action):
  324. levels = [str(x) for x in range(1, 7)]
  325. return wx.ComboBox(
  326. parent=parent,
  327. id=-1,
  328. value='',
  329. choices=levels,
  330. style=wx.CB_DROPDOWN
  331. )
  332. def GetValue(self):
  333. '''
  334. NOTE: Added on plane. Cannot remember exact implementation
  335. of counter objects. I believe that they count sequentail
  336. pairings of options
  337. e.g.
  338. -vvvvv
  339. But I'm not sure. That's what I'm going with for now.
  340. Returns
  341. str(action.options_string[0]) * DropDown Value
  342. '''
  343. dropdown_value = self._widget.GetValue()
  344. if not str(dropdown_value).isdigit():
  345. return None
  346. arg = str(self._action.option_strings[0]).replace('-', '')
  347. repeated_args = arg * int(dropdown_value)
  348. return '-' + repeated_args
  349. class Group(AbstractComponent):
  350. def __init__(self, action):
  351. self._action = action
  352. self._widget = None
  353. self.contents = None
  354. if __name__ == '__main__':
  355. pass