GTK+ GUI for configuring PD Buddy Sink devices
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.

pd-buddy-gtk.py 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. #!/usr/bin/env python3
  2. import sys
  3. import pdbuddy
  4. import gi
  5. gi.require_version('Gtk', '3.0')
  6. from gi.repository import Gtk, Gio, GObject, GLib
  7. def comms_error_dialog(parent, e):
  8. dialog = Gtk.MessageDialog(window, 0, Gtk.MessageType.ERROR,
  9. Gtk.ButtonsType.CLOSE, "Error communicating with device")
  10. dialog.format_secondary_text(e.strerror)
  11. dialog.run()
  12. dialog.destroy()
  13. class SelectListRowModel(GObject.GObject):
  14. def __init__(self, serport):
  15. GObject.GObject.__init__(self)
  16. self.serport = serport
  17. class SelectListStore(Gio.ListStore):
  18. def update_items(self):
  19. # Get a list of serial ports
  20. serports = list(pdbuddy.Sink.get_devices())
  21. # Mark ports to remove or add
  22. remove_list = []
  23. list_len = self.get_n_items()
  24. for i in range(list_len):
  25. remove = True
  26. for j in range(len(serports)):
  27. if serports[j] is not None and self.get_item(i).serport == serports[j]:
  28. serports[j] = None
  29. remove = False
  30. if remove:
  31. remove_list.append(i)
  32. # Remove the missing ones
  33. for i in remove_list:
  34. self.remove(i)
  35. # Add any new ports
  36. for port in serports:
  37. if port is not None:
  38. self.append(SelectListRowModel(port))
  39. def list_box_update_header_func(row, before, data):
  40. """Add a separator header to all rows but the first one"""
  41. name = Gtk.Buildable.get_name(row)
  42. # No header over the vrange-row to make it look like part of the row above
  43. if before is None or name == "vrange-row":
  44. row.set_header(None)
  45. return
  46. current = row.get_header()
  47. if current is None:
  48. current = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL)
  49. row.set_header(current)
  50. class SelectList(Gtk.Box):
  51. __gsignals__ = {
  52. 'row-activated': (GObject.SIGNAL_RUN_FIRST, None,
  53. (object,))
  54. }
  55. def __init__(self):
  56. Gtk.Box.__init__(self)
  57. self._model = None
  58. self._builder = Gtk.Builder()
  59. self._builder.add_from_file("data/select-stack.ui")
  60. self._builder.connect_signals(self)
  61. sl = self._builder.get_object("select-list")
  62. # Add separators to the list
  63. sl.set_header_func(list_box_update_header_func, None)
  64. self.pack_start(self._builder.get_object("select-stack"), True, True, 0)
  65. self.show_all()
  66. def bind_model(self, model, func):
  67. self._builder.get_object("select-list").bind_model(model, func)
  68. self._model = model
  69. self.reload()
  70. GLib.timeout_add(1000, self.reload)
  71. def reload(self):
  72. self._model.update_items()
  73. # Set the visible child
  74. stack = self._builder.get_object("select-stack")
  75. if self._model.get_n_items():
  76. stack.set_visible_child(self._builder.get_object("select-frame"))
  77. else:
  78. stack.set_visible_child(self._builder.get_object("select-none"))
  79. return True
  80. def on_select_list_row_activated(self, box, row):
  81. self.emit("row-activated", row.model.serport)
  82. class SelectListRow(Gtk.ListBoxRow):
  83. def __init__(self, model):
  84. Gtk.EventBox.__init__(self)
  85. self.model = model
  86. self._builder = Gtk.Builder()
  87. self._builder.add_from_file("data/select-list-row.ui")
  88. self._builder.connect_signals(self)
  89. name = self._builder.get_object("name")
  90. name.set_text('{} {} {}'.format(self.model.serport.manufacturer,
  91. self.model.serport.product,
  92. self.model.serport.serial_number))
  93. device = self._builder.get_object("device")
  94. device.set_text(self.model.serport.device)
  95. self.add(self._builder.get_object("grid"))
  96. self.show_all()
  97. def on_identify_clicked(self, button):
  98. window = self.get_toplevel()
  99. try:
  100. with pdbuddy.Sink(self.model.serport) as pdbs:
  101. pdbs.identify()
  102. except OSError as e:
  103. comms_error_dialog(window, e)
  104. return
  105. class PDOListRowModel(GObject.GObject):
  106. def __init__(self, pdo):
  107. GObject.GObject.__init__(self)
  108. self.pdo = pdo
  109. class PDOListStore(Gio.ListStore):
  110. def update_items(self, pdo_list):
  111. # Clear the list
  112. self.remove_all()
  113. # Add everything from the new list
  114. for pdo in pdo_list:
  115. self.append(PDOListRowModel(pdo))
  116. class PDOListRow(Gtk.ListBoxRow):
  117. oc_tooltips = [
  118. "I<sub>Peak</sub> = I<sub>OC</sub> (default)",
  119. """Overload Capabilities:
  120. 1. I<sub>Peak</sub> = 150% I<sub>OC</sub> for 1 ms @ 5% duty cycle (I<sub>Low</sub> = 97% I<sub>OC</sub> for 19 ms)
  121. 2. I<sub>Peak</sub> = 125% I<sub>OC</sub> for 2 ms @ 10% duty cycle (I<sub>Low</sub> = 97% I<sub>OC</sub> for 18 ms)
  122. 3. I<sub>Peak</sub> = 110% I<sub>OC</sub> for 10 ms @ 50% duty cycle (I<sub>Low</sub> = 90% I<sub>OC</sub> for 10 ms)""",
  123. """Overload Capabilities:
  124. 1. I<sub>Peak</sub> = 200% I<sub>OC</sub> for 1 ms @ 5% duty cycle (I<sub>Low</sub> = 95% I<sub>OC</sub> for 19 ms)
  125. 2. I<sub>Peak</sub> = 150% I<sub>OC</sub> for 2 ms @ 10% duty cycle (I<sub>Low</sub> = 94% I<sub>OC</sub> for 18 ms)
  126. 3. I<sub>Peak</sub> = 125% I<sub>OC</sub> for 10 ms @ 50% duty cycle (I<sub>Low</sub> = 75% I<sub>OC</sub> for 10 ms)""",
  127. """Overload Capabilities:
  128. 1. I<sub>Peak</sub> = 200% I<sub>OC</sub> for 1 ms @ 5% duty cycle (I<sub>Low</sub> = 95% I<sub>OC</sub> for 19 ms)
  129. 2. I<sub>Peak</sub> = 175% I<sub>OC</sub> for 2 ms @ 10% duty cycle (I<sub>Low</sub> = 92% I<sub>OC</sub> for 18 ms)
  130. 3. I<sub>Peak</sub> = 150% I<sub>OC</sub> for 10 ms @ 50% duty cycle (I<sub>Low</sub> = 50% I<sub>OC</sub> for 10 ms)"""
  131. ]
  132. def __init__(self, model):
  133. Gtk.ListBoxRow.__init__(self)
  134. self.model = model
  135. self.set_activatable(False)
  136. self.set_selectable(False)
  137. self.set_can_focus(False)
  138. # Make the widgets and populate them with info from the model
  139. # Main box
  140. box = Gtk.Box(Gtk.Orientation.HORIZONTAL, 12)
  141. box.set_homogeneous(True)
  142. box.set_margin_left(12)
  143. box.set_margin_right(12)
  144. box.set_margin_top(6)
  145. box.set_margin_bottom(6)
  146. # Type label
  147. if model.pdo.pdo_type == "fixed":
  148. type_text = "Fixed"
  149. elif model.pdo.pdo_type == "pps":
  150. type_text = "Programmable"
  151. elif model.pdo.pdo_type == "unknown":
  152. type_text = "Unknown"
  153. elif model.pdo.pdo_type == "typec_virtual":
  154. type_text = "Type-C Current"
  155. type_label = Gtk.Label(type_text)
  156. type_label.set_halign(Gtk.Align.START)
  157. box.pack_start(type_label, True, True, 0)
  158. # Voltage label
  159. if model.pdo.pdo_type == "fixed":
  160. voltage_label = Gtk.Label("{:g} V".format(model.pdo.v / 1000.0))
  161. voltage_label.set_halign(Gtk.Align.END)
  162. box.pack_start(voltage_label, True, True, 0)
  163. elif model.pdo.pdo_type == "pps":
  164. voltage_label = Gtk.Label("{:g}\u2013{:g} V".format(
  165. model.pdo.vmin / 1000.0, model.pdo.vmax / 1000.0))
  166. voltage_label.set_halign(Gtk.Align.END)
  167. box.pack_start(voltage_label, True, True, 0)
  168. # Right box
  169. right_box = Gtk.Box(Gtk.Orientation.HORIZONTAL, 6)
  170. right_box.set_halign(Gtk.Align.END)
  171. if model.pdo.pdo_type != "unknown":
  172. # Current label
  173. current_label = Gtk.Label("{:g} A".format(model.pdo.i / 1000.0))
  174. current_label.set_halign(Gtk.Align.END)
  175. right_box.pack_end(current_label, True, False, 0)
  176. # Over-current image(?)
  177. try:
  178. if model.pdo.peak_i > 0:
  179. oc_image = Gtk.Image.new_from_icon_name(
  180. "dialog-information-symbolic", Gtk.IconSize.BUTTON)
  181. oc_image.set_tooltip_markup(
  182. PDOListRow.oc_tooltips[model.pdo.peak_i])
  183. right_box.pack_end(oc_image, True, False, 0)
  184. except AttributeError:
  185. # If this isn't a fixed PDO, there's no peak_i attribute.
  186. # Not a problem, so just ignore the error.
  187. pass
  188. else:
  189. # PDO value
  190. text_label = Gtk.Label()
  191. text_label.set_markup("<tt>{}</tt>".format(model.pdo))
  192. right_box.pack_end(text_label, True, False, 0)
  193. box.pack_end(right_box, True, True, 0)
  194. self.add(box)
  195. self.show_all()
  196. class Handler:
  197. def __init__(self, builder):
  198. self.builder = builder
  199. self.serial_port = None
  200. self.vrange_set = False
  201. self.selectlist = None
  202. def on_pdb_window_realize(self, *args):
  203. # Get the list
  204. sb = self.builder.get_object("select-box")
  205. self.selectlist = SelectList()
  206. sb.pack_start(self.selectlist, True, True, 0)
  207. liststore = SelectListStore()
  208. self.selectlist.bind_model(liststore, SelectListRow)
  209. self.selectlist.connect("row-activated", self.on_select_list_row_activated)
  210. # Add separators to the configuration page lists
  211. sc_list = self.builder.get_object("sink-config-list")
  212. sc_list.set_header_func(list_box_update_header_func, None)
  213. pd_list = self.builder.get_object("power-delivery-list")
  214. pd_list.set_header_func(list_box_update_header_func, None)
  215. def on_pdb_window_delete_event(self, *args):
  216. Gtk.main_quit(*args)
  217. def on_select_list_row_activated(self, selectlist, serport):
  218. # Get relevant widgets
  219. voltage = self.builder.get_object("voltage-adjustment")
  220. vr_switch = self.builder.get_object("vrange-switch")
  221. vmin_adj = self.builder.get_object("vmin-adjustment")
  222. vmax_adj = self.builder.get_object("vmax-adjustment")
  223. current = self.builder.get_object("current-adjustment")
  224. current_dim = self.builder.get_object("current-dimension")
  225. giveback = self.builder.get_object("giveback-switch")
  226. pd_frame = self.builder.get_object("power-delivery-frame")
  227. output = self.builder.get_object("output-switch")
  228. cap_row = self.builder.get_object("source-cap-row")
  229. cap_warning = self.builder.get_object("source-cap-warning")
  230. cap_label = self.builder.get_object("short-source-cap-label")
  231. cap_arrow = self.builder.get_object("source-cap-arrow")
  232. self.serial_port = serport
  233. window = self.builder.get_object("pdb-window")
  234. try:
  235. with pdbuddy.Sink(self.serial_port) as pdbs:
  236. try:
  237. pdbs.load()
  238. except KeyError:
  239. # If there's no configuration, we don't want to fail. We
  240. # do want to display no configuration though
  241. self.cfg = pdbuddy.SinkConfig(
  242. status=pdbuddy.SinkStatus.VALID,
  243. flags=pdbuddy.SinkFlags.NONE, v=0, vmin=0, vmax=0,
  244. i=0, idim=pdbuddy.SinkDimension.CURRENT)
  245. else:
  246. self.cfg = pdbs.get_tmpcfg()
  247. if self.cfg.vmin is None:
  248. self.cfg = self.cfg._replace(vmin=0)
  249. if self.cfg.vmax is None:
  250. self.cfg = self.cfg._replace(vmax=0)
  251. except OSError as e:
  252. comms_error_dialog(window, e)
  253. return
  254. self._store_device_settings()
  255. self._set_save_button_visibility()
  256. # Set giveback switch state
  257. giveback.set_active(bool(self.cfg.flags & pdbuddy.SinkFlags.GIVEBACK))
  258. # Get voltage and current from device and load them into the GUI
  259. voltage.set_value(self.cfg.v/1000)
  260. vr_switch.set_active(self.cfg.vmin != 0 or self.cfg.vmax != 0)
  261. self.vrange_set = True
  262. vmin_adj.set_value(self.cfg.vmin/1000)
  263. self._set_hv_pref_image()
  264. vmax_adj.set_value(self.cfg.vmax/1000)
  265. self.vrange_set = False
  266. if self.cfg.idim == pdbuddy.SinkDimension.CURRENT:
  267. current_dim.set_active_id("idim-current")
  268. elif self.cfg.idim == pdbuddy.SinkDimension.POWER:
  269. current_dim.set_active_id("idim-power")
  270. elif self.cfg.idim == pdbuddy.SinkDimension.RESISTANCE:
  271. current_dim.set_active_id("idim-resistance")
  272. current.set_value(self.cfg.i/1000)
  273. # Set PD frame visibility and output switch state
  274. try:
  275. with pdbuddy.Sink(self.serial_port) as pdbs:
  276. output.set_state(pdbs.output)
  277. except KeyError:
  278. pd_frame.set_visible(False)
  279. else:
  280. pd_frame.set_visible(True)
  281. # TODO: do these next things repeatedly
  282. # Get the Source_Capabilities
  283. with pdbuddy.Sink(self.serial_port) as pdbs:
  284. caps = pdbs.get_source_cap()
  285. # Update the warning icon
  286. cap_warning.set_visible(not pdbuddy.follows_power_rules(caps))
  287. # Update the text in the capability label
  288. if caps:
  289. cap_label.set_text('{:g} W'.format(pdbuddy.calculate_pdp(caps)))
  290. else:
  291. cap_label.set_text('None')
  292. # Make the row insensitive if there are no capabilities
  293. cap_row.set_activatable(caps)
  294. cap_arrow.set_visible(caps)
  295. # Show the Sink page
  296. hst = self.builder.get_object("header-stack")
  297. hsink = self.builder.get_object("header-sink")
  298. hsink.set_title('{} {} {}'.format(serport.manufacturer,
  299. serport.product,
  300. serport.serial_number))
  301. hsink.set_subtitle(serport.device)
  302. hst.set_visible_child(hsink)
  303. st = self.builder.get_object("stack")
  304. sink = self.builder.get_object("sink")
  305. st.set_visible_child(sink)
  306. # Ping the Sink repeatedly
  307. GLib.timeout_add(1000, self._ping)
  308. def _ping(self):
  309. """Ping the device we're configuring, showing to the list on failure"""
  310. if self.serial_port is None:
  311. self.selectlist.reload()
  312. self.on_header_sink_back_clicked(None)
  313. return False
  314. try:
  315. with pdbuddy.Sink(self.serial_port) as pdbs:
  316. pdbs.send_command("")
  317. return True
  318. except:
  319. self.selectlist.reload()
  320. self.on_header_sink_back_clicked(None)
  321. return False
  322. def on_header_sink_back_clicked(self, data):
  323. self.serial_port = None
  324. # Show the Select page
  325. hst = self.builder.get_object("header-stack")
  326. hselect = self.builder.get_object("header-select")
  327. hst.set_visible_child(hselect)
  328. st = self.builder.get_object("stack")
  329. select = self.builder.get_object("select")
  330. st.set_visible_child(select)
  331. def on_sink_save_clicked(self, button):
  332. window = self.builder.get_object("pdb-window")
  333. try:
  334. with pdbuddy.Sink(self.serial_port) as pdbs:
  335. pdbs.set_tmpcfg(self.cfg)
  336. pdbs.write()
  337. self._store_device_settings()
  338. self._set_save_button_visibility()
  339. except OSError as e:
  340. comms_error_dialog(window, e)
  341. self.on_header_sink_back_clicked(None)
  342. def _store_device_settings(self):
  343. """Store the settings that were loaded from the device"""
  344. self.cfg_clean = self.cfg
  345. def _set_save_button_visibility(self):
  346. """Show the save button if there are new settings to save"""
  347. # Get relevant widgets
  348. rev = self.builder.get_object("sink-save-revealer")
  349. # Set visibility
  350. rev.set_reveal_child(self.cfg != self.cfg_clean)
  351. def on_voltage_adjustment_value_changed(self, adj):
  352. self.cfg = self.cfg._replace(v=int(adj.get_value() * 1000))
  353. self._set_save_button_visibility()
  354. def on_vrange_switch_state_set(self, switch, state):
  355. row = self.builder.get_object("vrange-row")
  356. vmin_adj = self.builder.get_object("vmin-adjustment")
  357. vmax_adj = self.builder.get_object("vmax-adjustment")
  358. row.set_visible(state)
  359. if not state:
  360. self.cfg = self.cfg._replace(vmin=0, vmax=0)
  361. self.vrange_set = True
  362. vmin_adj.set_value(self.cfg.vmin/1000)
  363. vmax_adj.set_value(self.cfg.vmax/1000)
  364. self.vrange_set = False
  365. self._set_save_button_visibility()
  366. def on_vmin_adjustment_value_changed(self, adj):
  367. if not self.vrange_set:
  368. self.cfg = self.cfg._replace(vmin=int(adj.get_value() * 1000))
  369. # Update vmax if necessary
  370. vmax_adj = self.builder.get_object("vmax-adjustment")
  371. if adj.get_value() > vmax_adj.get_value():
  372. vmax_adj.set_value(adj.get_value())
  373. self._set_save_button_visibility()
  374. def on_vmax_adjustment_value_changed(self, adj):
  375. if not self.vrange_set:
  376. self.cfg = self.cfg._replace(vmax=int(adj.get_value() * 1000))
  377. # Update vmin if necessary
  378. vmin_adj = self.builder.get_object("vmin-adjustment")
  379. if adj.get_value() < vmin_adj.get_value():
  380. vmin_adj.set_value(adj.get_value())
  381. self._set_save_button_visibility()
  382. def on_hv_preferred_button_clicked(self, button):
  383. self.cfg = self.cfg._replace(
  384. flags=self.cfg.flags^pdbuddy.SinkFlags.HV_PREFERRED)
  385. self._set_hv_pref_image()
  386. self._set_save_button_visibility()
  387. def _set_hv_pref_image(self):
  388. hv_pref = self.builder.get_object("hv-preferred-button")
  389. image = self.builder.get_object("order-image")
  390. if self.cfg.flags & pdbuddy.SinkFlags.HV_PREFERRED:
  391. image.set_from_icon_name("go-previous-symbolic",
  392. Gtk.IconSize.BUTTON)
  393. else:
  394. image.set_from_icon_name("go-next-symbolic", Gtk.IconSize.BUTTON)
  395. def on_current_dimension_changed(self, cb):
  396. item = cb.get_active_id()
  397. value = self.builder.get_object("current-adjustment")
  398. unit = self.builder.get_object("current-unit")
  399. if item == "idim-current":
  400. if self.cfg.idim == pdbuddy.SinkDimension.POWER:
  401. self.cfg = self.cfg._replace(i=self.cfg.i/self.cfg.v*1000.0)
  402. elif self.cfg.idim == pdbuddy.SinkDimension.RESISTANCE:
  403. self.cfg = self.cfg._replace(i=self.cfg.v/self.cfg.i*1000.0)
  404. value.configure(self.cfg.i / 1000.0, 0, 5, 0.1, 1, 0)
  405. idim = pdbuddy.SinkDimension.CURRENT
  406. unit.set_text("A")
  407. if item == "idim-power":
  408. if self.cfg.idim == pdbuddy.SinkDimension.CURRENT:
  409. self.cfg = self.cfg._replace(i=self.cfg.i*self.cfg.v/1000.0)
  410. elif self.cfg.idim == pdbuddy.SinkDimension.RESISTANCE:
  411. self.cfg = self.cfg._replace(
  412. i=self.cfg.v*self.cfg.v/self.cfg.i)
  413. idim = pdbuddy.SinkDimension.POWER
  414. value.configure(self.cfg.i / 1000.0, 0, 100, 1, 10, 0)
  415. unit.set_text("W")
  416. if item == "idim-resistance":
  417. if self.cfg.idim == pdbuddy.SinkDimension.CURRENT:
  418. self.cfg = self.cfg._replace(i=self.cfg.v/self.cfg.i*1000.0)
  419. elif self.cfg.idim == pdbuddy.SinkDimension.POWER:
  420. self.cfg = self.cfg._replace(
  421. i=self.cfg.v*self.cfg.v/self.cfg.i)
  422. idim = pdbuddy.SinkDimension.RESISTANCE
  423. value.configure(self.cfg.i / 1000.0, 0, 655.35, 1, 10, 0)
  424. unit.set_text("\u03a9")
  425. self.cfg = self.cfg._replace(idim=idim)
  426. self._set_save_button_visibility()
  427. def on_current_adjustment_value_changed(self, adj):
  428. self.cfg = self.cfg._replace(i=int(adj.get_value() * 1000))
  429. self._set_save_button_visibility()
  430. def on_giveback_switch_state_set(self, switch, state):
  431. if state:
  432. self.cfg = self.cfg._replace(flags=self.cfg.flags|pdbuddy.SinkFlags.GIVEBACK)
  433. else:
  434. self.cfg = self.cfg._replace(flags=self.cfg.flags&~pdbuddy.SinkFlags.GIVEBACK)
  435. self._set_save_button_visibility()
  436. def on_output_switch_state_set(self, switch, state):
  437. with pdbuddy.Sink(self.serial_port) as pdbs:
  438. pdbs.output = state
  439. def on_source_cap_row_activated(self, box, row):
  440. # Find which row was clicked
  441. sc_row = self.builder.get_object("source-cap-row")
  442. if row != sc_row:
  443. # If it's not the source-cap-row, leave
  444. return
  445. # Get the source capabilities
  446. with pdbuddy.Sink(self.serial_port) as pdbs:
  447. caps = pdbs.get_source_cap()
  448. if not caps:
  449. # If there are no capabilities, don't show a dialog
  450. return
  451. # Create the dialog
  452. window = self.builder.get_object("pdb-window")
  453. dialog_builder = Gtk.Builder.new_from_file("data/src-cap-dialog.ui")
  454. dialog = dialog_builder.get_object("src-cap-dialog")
  455. dialog.set_transient_for(window)
  456. dialog.get_content_area().set_border_width(0)
  457. # Populate PD Power
  458. d_power = dialog_builder.get_object("power-label")
  459. d_power.set_text("{:g} W".format(pdbuddy.calculate_pdp(caps)))
  460. # Warning icon
  461. cap_warning = dialog_builder.get_object("source-cap-warning")
  462. cap_warning.set_visible(not pdbuddy.follows_power_rules(caps))
  463. # Populate Information
  464. d_info_header = dialog_builder.get_object("info-header")
  465. d_info = dialog_builder.get_object("info-label")
  466. # Make the string to display
  467. info_str = ""
  468. try:
  469. if caps[0].dual_role_pwr:
  470. info_str += "Dual-Role Power\n"
  471. if caps[0].usb_suspend:
  472. info_str += "USB Suspend Supported\n"
  473. if caps[0].unconstrained_pwr:
  474. info_str += "Unconstrained Power\n"
  475. if caps[0].usb_comms:
  476. info_str += "USB Communications Capable\n"
  477. if caps[0].dual_role_data:
  478. info_str += "Dual-Role Data\n"
  479. info_str = info_str[:-1]
  480. except AttributeError:
  481. # If we have a typec_virtual PDO, there will be AttributeErrors
  482. # from the above. Not a problem, so just pass.
  483. pass
  484. # Set the text and label visibility
  485. d_info.set_text(info_str)
  486. d_info_header.set_visible(info_str)
  487. d_info.set_visible(info_str)
  488. # PDO list
  489. d_list = dialog_builder.get_object("src-cap-list")
  490. d_list.set_header_func(list_box_update_header_func, None)
  491. model = PDOListStore()
  492. d_list.bind_model(model, PDOListRow)
  493. model.update_items(caps)
  494. # Show the dialog
  495. dialog.run()
  496. dialog.destroy()
  497. class Application(Gtk.Application):
  498. def __init__(self, *args, **kwargs):
  499. super().__init__(*args, application_id="com.clayhobbs.pd-buddy-gtk",
  500. **kwargs)
  501. self.window = None
  502. def do_startup(self):
  503. Gtk.Application.do_startup(self)
  504. self.builder = Gtk.Builder.new_from_file("data/pd-buddy-gtk.ui")
  505. self.builder.connect_signals(Handler(self.builder))
  506. def do_activate(self):
  507. # We only allow a single window and raise any existing ones
  508. if not self.window:
  509. # Windows are associated with the application
  510. # when the last one is closed the application shuts down
  511. self.window = self.builder.get_object("pdb-window")
  512. self.add_window(self.window)
  513. self.window.set_wmclass("PD Buddy Configuration",
  514. "PD Buddy Configuration")
  515. self.window.present()
  516. def run():
  517. app = Application()
  518. app.run(sys.argv)
  519. if __name__ == "__main__":
  520. run()