gtkhelpers.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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 program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program 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
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License along
  18. # with this program; if not, write to the Free Software Foundation, Inc.,
  19. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  20. #
  21. import gi
  22. import os
  23. gi.require_version('Gtk', '3.0')
  24. from gi.repository import Gtk, Gdk, GdkPixbuf, GObject, GLib
  25. import qubes
  26. from qubespolicy.utils import sanitize_domain_name
  27. glade_directory = os.path.join(os.path.dirname(__file__), "glade")
  28. class GtkIconGetter:
  29. def __init__(self, size):
  30. self._icons = {}
  31. self._size = size
  32. self._theme = Gtk.IconTheme.get_default()
  33. def get_icon(self, name):
  34. if name not in self._icons:
  35. try:
  36. icon = self._theme.load_icon(name, self._size, 0)
  37. except GLib.Error:
  38. icon = self._theme.load_icon("edit-find", self._size, 0)
  39. self._icons[name] = icon
  40. return self._icons[name]
  41. class VMListModeler:
  42. def __init__(self):
  43. self._icon_getter = GtkIconGetter(16)
  44. self._entries = {}
  45. self._create_entries()
  46. def _get_icon(self, vm):
  47. return self._icon_getter.get_icon(vm.label.icon)
  48. def _get_list(self):
  49. collection = qubes.QubesVmCollection()
  50. try:
  51. collection.lock_db_for_reading()
  52. collection.load()
  53. return [vm for vm in collection.values()]
  54. finally:
  55. collection.unlock_db()
  56. def _create_entries(self):
  57. for vm in self._get_list():
  58. sanitize_domain_name(vm.name, assert_sanitized=True)
  59. icon = self._get_icon(vm)
  60. self._entries[vm.name] = {'qid': vm.qid,
  61. 'icon': icon,
  62. 'vm': vm}
  63. def _get_valid_qube_name(self, combo, entry_box, exclusions):
  64. name = None
  65. if combo and combo.get_active_id():
  66. selected = combo.get_active_id()
  67. if selected in self._entries and selected not in exclusions:
  68. name = selected
  69. if not name and entry_box:
  70. typed = entry_box.get_text()
  71. if typed in self._entries and typed not in exclusions:
  72. name = typed
  73. return name
  74. def _combo_change(self, selection_trigger, combo, entry_box, exclusions):
  75. data = None
  76. name = self._get_valid_qube_name(combo, entry_box, exclusions)
  77. if name:
  78. entry = self._entries[name]
  79. data = (entry['qid'], name)
  80. if entry_box:
  81. entry_box.set_icon_from_pixbuf(
  82. Gtk.EntryIconPosition.PRIMARY, entry['icon'])
  83. else:
  84. if entry_box:
  85. entry_box.set_icon_from_stock(
  86. Gtk.EntryIconPosition.PRIMARY, "gtk-find")
  87. if selection_trigger:
  88. selection_trigger(data)
  89. def _entry_activate(self, activation_trigger, combo, entry_box, exclusions):
  90. name = self._get_valid_qube_name(combo, entry_box, exclusions)
  91. if name:
  92. activation_trigger(entry_box)
  93. def apply_model(self, destination_object, vm_filter_list=None,
  94. selection_trigger=None, activation_trigger=None):
  95. if isinstance(destination_object, Gtk.ComboBox):
  96. list_store = Gtk.ListStore(int, str, GdkPixbuf.Pixbuf)
  97. exclusions = []
  98. for vm_name in sorted(self._entries.keys()):
  99. entry = self._entries[vm_name]
  100. matches = True
  101. if vm_filter_list:
  102. for vm_filter in vm_filter_list:
  103. if not vm_filter.matches(entry['vm']):
  104. matches = False
  105. break
  106. if matches:
  107. list_store.append([entry['qid'], vm_name, entry['icon']])
  108. else:
  109. exclusions += [vm_name]
  110. destination_object.set_model(list_store)
  111. destination_object.set_id_column(1)
  112. icon_column = Gtk.CellRendererPixbuf()
  113. destination_object.pack_start(icon_column, False)
  114. destination_object.add_attribute(icon_column, "pixbuf", 2)
  115. destination_object.set_entry_text_column(1)
  116. if destination_object.get_has_entry():
  117. entry_box = destination_object.get_child()
  118. area = Gtk.CellAreaBox()
  119. area.pack_start(icon_column, False, False, False)
  120. area.add_attribute(icon_column, "pixbuf", 2)
  121. completion = Gtk.EntryCompletion.new_with_area(area)
  122. completion.set_inline_selection(True)
  123. completion.set_inline_completion(True)
  124. completion.set_popup_completion(True)
  125. completion.set_popup_single_match(False)
  126. completion.set_model(list_store)
  127. completion.set_text_column(1)
  128. entry_box.set_completion(completion)
  129. if activation_trigger:
  130. entry_box.connect("activate",
  131. lambda entry: self._entry_activate(
  132. activation_trigger,
  133. destination_object,
  134. entry,
  135. exclusions))
  136. # A Combo with an entry has a text column already
  137. text_column = destination_object.get_cells()[0]
  138. destination_object.reorder(text_column, 1)
  139. else:
  140. entry_box = None
  141. text_column = Gtk.CellRendererText()
  142. destination_object.pack_start(text_column, False)
  143. destination_object.add_attribute(text_column, "text", 1)
  144. changed_function = lambda combo: self._combo_change(
  145. selection_trigger,
  146. combo,
  147. entry_box,
  148. exclusions)
  149. destination_object.connect("changed", changed_function)
  150. changed_function(destination_object)
  151. else:
  152. raise TypeError(
  153. "Only expecting Gtk.ComboBox objects to want our model.")
  154. def apply_icon(self, entry, qube_name):
  155. if isinstance(entry, Gtk.Entry):
  156. if qube_name in self._entries:
  157. entry.set_icon_from_pixbuf(
  158. Gtk.EntryIconPosition.PRIMARY,
  159. self._entries[qube_name]['icon'])
  160. else:
  161. raise ValueError("The specified source qube does not exist!")
  162. else:
  163. raise TypeError(
  164. "Only expecting Gtk.Entry objects to want our icon.")
  165. class NameBlacklistFilter:
  166. def __init__(self, avoid_names_list):
  167. self._avoid_names_list = avoid_names_list
  168. def matches(self, vm):
  169. return vm.name not in self._avoid_names_list
  170. class NameWhitelistFilter:
  171. def __init__(self, allowed_names_list):
  172. self._allowed_names_list = allowed_names_list
  173. def matches(self, vm):
  174. return vm.name in self._allowed_names_list
  175. class GtkOneTimerHelper:
  176. def __init__(self, wait_seconds):
  177. self._wait_seconds = wait_seconds
  178. self._current_timer_id = 0
  179. self._timer_completed = False
  180. def _invalidate_timer_completed(self):
  181. self._timer_completed = False
  182. def _invalidate_current_timer(self):
  183. self._current_timer_id += 1
  184. def _timer_check_run(self, timer_id):
  185. if self._current_timer_id == timer_id:
  186. self._timer_run(timer_id)
  187. self._timer_completed = True
  188. else:
  189. pass
  190. def _timer_run(self, timer_id):
  191. raise NotImplementedError("Not yet implemented")
  192. def _timer_schedule(self):
  193. self._invalidate_current_timer()
  194. GObject.timeout_add(int(round(self._wait_seconds * 1000)),
  195. self._timer_check_run,
  196. self._current_timer_id)
  197. def _timer_has_completed(self):
  198. return self._timer_completed
  199. class FocusStealingHelper(GtkOneTimerHelper):
  200. def __init__(self, window, target_button, wait_seconds=1):
  201. GtkOneTimerHelper.__init__(self, wait_seconds)
  202. self._window = window
  203. self._target_button = target_button
  204. self._window.connect("window-state-event", self._window_state_event)
  205. self._target_sensitivity = False
  206. self._target_button.set_sensitive(self._target_sensitivity)
  207. def _window_changed_focus(self, window_is_focused):
  208. self._target_button.set_sensitive(False)
  209. self._invalidate_timer_completed()
  210. if window_is_focused:
  211. self._timer_schedule()
  212. else:
  213. self._invalidate_current_timer()
  214. def _window_state_event(self, window, event):
  215. assert window == self._window, \
  216. 'Window state callback called with wrong window'
  217. changed_focus = event.changed_mask & Gdk.WindowState.FOCUSED
  218. window_focus = event.new_window_state & Gdk.WindowState.FOCUSED
  219. if changed_focus:
  220. self._window_changed_focus(window_focus != 0)
  221. # Propagate event further
  222. return False
  223. def _timer_run(self, timer_id):
  224. self._target_button.set_sensitive(self._target_sensitivity)
  225. def request_sensitivity(self, sensitivity):
  226. if self._timer_has_completed() or not sensitivity:
  227. self._target_button.set_sensitive(sensitivity)
  228. self._target_sensitivity = sensitivity
  229. def can_perform_action(self):
  230. return self._timer_has_completed()