gtkhelpers.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. #!/usr/bin/python
  2. #
  3. # The Qubes OS Project, https://www.qubes-os.org/
  4. #
  5. # Copyright (C) 2017 boring-stuff <boring-stuff@users.noreply.github.com>
  6. #
  7. # This library is free software; you can redistribute it and/or
  8. # modify it under the terms of the GNU Lesser General Public
  9. # License as published by the Free Software Foundation; either
  10. # version 2.1 of the License, or (at your option) any later version.
  11. #
  12. # This library is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  15. # Lesser General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Lesser General Public
  18. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  19. #
  20. import itertools
  21. # pylint: disable=import-error,wrong-import-position
  22. import gi
  23. gi.require_version('Gtk', '3.0')
  24. from gi.repository import Gtk, Gdk, GdkPixbuf, GObject, GLib
  25. # pylint: enable=import-error
  26. from qubespolicy.utils import sanitize_domain_name
  27. # pylint: enable=wrong-import-position
  28. class VMListModeler:
  29. def __init__(self, domains_info=None):
  30. self._entries = {}
  31. self._domains_info = domains_info
  32. self._icons = {}
  33. self._icon_size = 16
  34. self._theme = Gtk.IconTheme.get_default()
  35. self._create_entries()
  36. def _get_icon(self, name):
  37. if name not in self._icons:
  38. try:
  39. icon = self._theme.load_icon(name, self._icon_size, 0)
  40. except GLib.Error: # pylint: disable=catching-non-exception
  41. icon = self._theme.load_icon("edit-find", self._icon_size, 0)
  42. self._icons[name] = icon
  43. return self._icons[name]
  44. def _create_entries(self):
  45. for name, vm in self._domains_info.items():
  46. if name.startswith('@dispvm:'):
  47. vm_name = name[len('@dispvm:'):]
  48. dispvm = True
  49. else:
  50. vm_name = name
  51. dispvm = False
  52. sanitize_domain_name(vm_name, assert_sanitized=True)
  53. icon = self._get_icon(vm.get('icon', None))
  54. if dispvm:
  55. display_name = 'Disposable VM ({})'.format(vm_name)
  56. else:
  57. display_name = vm_name
  58. self._entries[display_name] = {
  59. 'api_name': name,
  60. 'icon': icon,
  61. 'vm': vm}
  62. def _get_valid_qube_name(self, combo, entry_box, whitelist):
  63. name = None
  64. if combo and combo.get_active_id():
  65. selected = combo.get_active_id()
  66. if selected in self._entries and \
  67. self._entries[selected]['api_name'] in whitelist:
  68. name = selected
  69. if not name and entry_box:
  70. typed = entry_box.get_text()
  71. if typed in self._entries and \
  72. self._entries[typed]['api_name'] in whitelist:
  73. name = typed
  74. return name
  75. def _combo_change(self, selection_trigger, combo, entry_box, whitelist):
  76. data = None
  77. name = self._get_valid_qube_name(combo, entry_box, whitelist)
  78. if name:
  79. entry = self._entries[name]
  80. data = entry['api_name']
  81. if entry_box:
  82. entry_box.set_icon_from_pixbuf(
  83. Gtk.EntryIconPosition.PRIMARY, entry['icon'])
  84. else:
  85. if entry_box:
  86. entry_box.set_icon_from_stock(
  87. Gtk.EntryIconPosition.PRIMARY, "gtk-find")
  88. if selection_trigger:
  89. selection_trigger(data)
  90. def _entry_activate(self, activation_trigger, combo, entry_box, whitelist):
  91. name = self._get_valid_qube_name(combo, entry_box, whitelist)
  92. if name:
  93. activation_trigger(entry_box)
  94. def apply_model(self, destination_object, vm_list,
  95. selection_trigger=None, activation_trigger=None):
  96. if isinstance(destination_object, Gtk.ComboBox):
  97. list_store = Gtk.ListStore(int, str, GdkPixbuf.Pixbuf, str)
  98. for entry_no, display_name in zip(itertools.count(),
  99. sorted(self._entries)):
  100. entry = self._entries[display_name]
  101. if entry['api_name'] in vm_list:
  102. list_store.append([
  103. entry_no,
  104. display_name,
  105. entry['icon'],
  106. entry['api_name'],
  107. ])
  108. destination_object.set_model(list_store)
  109. destination_object.set_id_column(1)
  110. icon_column = Gtk.CellRendererPixbuf()
  111. destination_object.pack_start(icon_column, False)
  112. destination_object.add_attribute(icon_column, "pixbuf", 2)
  113. destination_object.set_entry_text_column(1)
  114. if destination_object.get_has_entry():
  115. entry_box = destination_object.get_child()
  116. area = Gtk.CellAreaBox()
  117. area.pack_start(icon_column, False, False, False)
  118. area.add_attribute(icon_column, "pixbuf", 2)
  119. completion = Gtk.EntryCompletion.new_with_area(area)
  120. completion.set_inline_selection(True)
  121. completion.set_inline_completion(True)
  122. completion.set_popup_completion(True)
  123. completion.set_popup_single_match(False)
  124. completion.set_model(list_store)
  125. completion.set_text_column(1)
  126. entry_box.set_completion(completion)
  127. if activation_trigger:
  128. entry_box.connect("activate",
  129. lambda entry: self._entry_activate(
  130. activation_trigger,
  131. destination_object,
  132. entry,
  133. vm_list))
  134. # A Combo with an entry has a text column already
  135. text_column = destination_object.get_cells()[0]
  136. destination_object.reorder(text_column, 1)
  137. else:
  138. entry_box = None
  139. text_column = Gtk.CellRendererText()
  140. destination_object.pack_start(text_column, False)
  141. destination_object.add_attribute(text_column, "text", 1)
  142. changed_function = lambda combo: self._combo_change(
  143. selection_trigger,
  144. combo,
  145. entry_box,
  146. vm_list)
  147. destination_object.connect("changed", changed_function)
  148. changed_function(destination_object)
  149. else:
  150. raise TypeError(
  151. "Only expecting Gtk.ComboBox objects to want our model.")
  152. def apply_icon(self, entry, qube_name):
  153. if isinstance(entry, Gtk.Entry):
  154. if qube_name in self._entries:
  155. entry.set_icon_from_pixbuf(
  156. Gtk.EntryIconPosition.PRIMARY,
  157. self._entries[qube_name]['icon'])
  158. else:
  159. raise ValueError("The specified source qube does not exist!")
  160. else:
  161. raise TypeError(
  162. "Only expecting Gtk.Entry objects to want our icon.")
  163. class GtkOneTimerHelper:
  164. # pylint: disable=too-few-public-methods
  165. def __init__(self, wait_seconds):
  166. self._wait_seconds = wait_seconds
  167. self._current_timer_id = 0
  168. self._timer_completed = False
  169. def _invalidate_timer_completed(self):
  170. self._timer_completed = False
  171. def _invalidate_current_timer(self):
  172. self._current_timer_id += 1
  173. def _timer_check_run(self, timer_id):
  174. if self._current_timer_id == timer_id:
  175. self._timer_run(timer_id)
  176. self._timer_completed = True
  177. else:
  178. pass
  179. def _timer_run(self, timer_id):
  180. raise NotImplementedError("Not yet implemented")
  181. def _timer_schedule(self):
  182. self._invalidate_current_timer()
  183. GObject.timeout_add(int(round(self._wait_seconds * 1000)),
  184. self._timer_check_run,
  185. self._current_timer_id)
  186. def _timer_has_completed(self):
  187. return self._timer_completed
  188. class FocusStealingHelper(GtkOneTimerHelper):
  189. def __init__(self, window, target_button, wait_seconds=1):
  190. GtkOneTimerHelper.__init__(self, wait_seconds)
  191. self._window = window
  192. self._target_button = target_button
  193. self._window.connect("window-state-event", self._window_state_event)
  194. self._target_sensitivity = False
  195. self._target_button.set_sensitive(self._target_sensitivity)
  196. def _window_changed_focus(self, window_is_focused):
  197. self._target_button.set_sensitive(False)
  198. self._invalidate_timer_completed()
  199. if window_is_focused:
  200. self._timer_schedule()
  201. else:
  202. self._invalidate_current_timer()
  203. def _window_state_event(self, window, event):
  204. assert window == self._window, \
  205. 'Window state callback called with wrong window'
  206. changed_focus = event.changed_mask & Gdk.WindowState.FOCUSED
  207. window_focus = event.new_window_state & Gdk.WindowState.FOCUSED
  208. if changed_focus:
  209. self._window_changed_focus(window_focus != 0)
  210. # Propagate event further
  211. return False
  212. def _timer_run(self, timer_id):
  213. self._target_button.set_sensitive(self._target_sensitivity)
  214. def request_sensitivity(self, sensitivity):
  215. if self._timer_has_completed() or not sensitivity:
  216. self._target_button.set_sensitive(sensitivity)
  217. self._target_sensitivity = sensitivity
  218. def can_perform_action(self):
  219. return self._timer_has_completed()