1# This work is licensed under the GNU GPLv2 or later. 2# See the COPYING file in the top-level directory. 3 4# pylint: disable=wrong-import-order,ungrouped-imports 5import gi 6 7from virtinst import log 8 9# We can use either gtksourceview3 or gtksourceview4 10try: 11 gi.require_version("GtkSource", "4") 12 log.debug("Using GtkSource 4") 13except ValueError: # pragma: no cover 14 gi.require_version("GtkSource", "3.0") 15 log.debug("Using GtkSource 3.0") 16from gi.repository import GtkSource 17 18from .lib import uiutil 19from .baseclass import vmmGObjectUI 20 21_PAGE_DETAILS = 0 22_PAGE_XML = 1 23 24 25class vmmXMLEditor(vmmGObjectUI): 26 __gsignals__ = { 27 "changed": (vmmGObjectUI.RUN_FIRST, None, []), 28 "xml-requested": (vmmGObjectUI.RUN_FIRST, None, []), 29 "xml-reset": (vmmGObjectUI.RUN_FIRST, None, []), 30 } 31 32 def __init__(self, builder, topwin, parent_container, details_widget): 33 super().__init__("xmleditor.ui", None, 34 builder=builder, topwin=topwin) 35 36 parent_container.remove(details_widget) 37 parent_container.add(self.widget("xml-notebook")) 38 self.widget("xml-details-box").add(details_widget) 39 40 self._curpage = _PAGE_DETAILS 41 self._srcxml = "" 42 self._srcview = None 43 self._srcbuff = None 44 self._init_ui() 45 46 self.details_changed = False 47 48 self.add_gsettings_handle( 49 self.config.on_xmleditor_enabled_changed( 50 self._xmleditor_enabled_changed_cb)) 51 52 53 def _cleanup(self): 54 self._srcview.destroy() 55 self._srcbuff = None 56 57 58 ########### 59 # UI init # 60 ########### 61 62 def _set_xmleditor_enabled_from_config(self): 63 enabled = self.config.get_xmleditor_enabled() 64 self._srcview.set_editable(enabled) 65 uiutil.set_grid_row_visible(self.widget("xml-warning-box"), 66 not enabled) 67 68 def _init_ui(self): 69 self._srcview = GtkSource.View() 70 self._srcbuff = self._srcview.get_buffer() 71 72 lang = GtkSource.LanguageManager.get_default().get_language("xml") 73 self._srcbuff.set_language(lang) 74 75 self._srcview.set_monospace(True) 76 self._srcview.set_auto_indent(True) 77 self._srcview.get_accessible().set_name("XML editor") 78 79 self._srcbuff.set_highlight_syntax(True) 80 self._srcbuff.connect("changed", self._buffer_changed_cb) 81 82 self.widget("xml-notebook").connect("switch-page", 83 self._before_page_changed_cb) 84 self.widget("xml-notebook").connect("notify::page", 85 self._after_page_changed_cb) 86 87 self._srcview.show_all() 88 self.widget("xml-scroll").add(self._srcview) 89 self._set_xmleditor_enabled_from_config() 90 91 92 #################### 93 # Internal helpers # 94 #################### 95 96 def _reselect_page(self, pagenum): 97 # Setting _curpage first will shortcircuit our page changed callback 98 self._curpage = pagenum 99 self.widget("xml-notebook").set_current_page(pagenum) 100 101 def _reset_xml(self): 102 self.set_xml("") 103 self.emit("xml-reset") 104 105 def _reset_cursor(self): 106 # Put cursor at the start of the second line. Starting on the 107 # first means XML open/close tags are highlighted which is weird 108 # starting visual 109 startiter = self._srcbuff.get_start_iter() 110 startiter.forward_line() 111 self._srcbuff.place_cursor(startiter) 112 113 def _detials_unapplied_changes(self): 114 if not self.details_changed: 115 return False 116 117 ret = self.err.yes_no( 118 _("There are unapplied changes."), 119 _("Your changes will be lost if you leave this tab. " 120 "Really leave this tab?")) 121 if ret: 122 self.details_changed = False 123 124 return not ret 125 126 def _xml_unapplied_changes(self): 127 if self._srcxml == self.get_xml(): 128 return False 129 130 ret = self.err.yes_no( 131 _("There are unapplied changes."), 132 _("Your XML changes will be lost if you leave this tab. " 133 "Really leave this tab?")) 134 135 return not ret 136 137 138 139 140 ############## 141 # Public API # 142 ############## 143 144 def reset_state(self): 145 """ 146 Clear XML and select the details page. Used when callers do 147 their own reset_state 148 """ 149 self._reset_xml() 150 return self.widget("xml-notebook").set_current_page(_PAGE_DETAILS) 151 152 def get_xml(self): 153 """ 154 Return the XML from the editor UI 155 """ 156 return self._srcbuff.get_property("text") 157 158 def set_xml(self, xml): 159 """ 160 Set the editor UI XML to the passed string 161 """ 162 try: 163 self._srcbuff.disconnect_by_func(self._buffer_changed_cb) 164 self._srcxml = xml or "" 165 self._srcbuff.set_text(self._srcxml) 166 self._reset_cursor() 167 finally: 168 self._srcbuff.connect("changed", self._buffer_changed_cb) 169 170 def set_xml_from_libvirtobject(self, libvirtobject): 171 """ 172 Set the editor UI XML to the inactive XML from the passed 173 vmmLibvirtObject. If the XML UI isn't visible, we don't set 174 anything, which lets callers use this on every page refresh 175 """ 176 if not self.is_xml_selected(): 177 return 178 xml = "" 179 if libvirtobject: 180 xml = libvirtobject.get_xml_to_define() 181 self.set_xml(xml) 182 183 def is_xml_selected(self): 184 """ 185 Return True if the XML page is selected 186 """ 187 return self._curpage == _PAGE_XML 188 189 190 ############# 191 # Listeners # 192 ############# 193 194 def _buffer_changed_cb(self, buf): 195 self.emit("changed") 196 197 def _before_page_changed_cb(self, notebook, widget, pagenum): 198 if self._curpage == pagenum: 199 return 200 prevpage = self._curpage 201 self._curpage = pagenum 202 203 if pagenum == _PAGE_XML: 204 if not self._detials_unapplied_changes(): 205 # If the XML page is clicked, emit xml-requested signal which 206 # expects the user to call set_xml/set_libvirtobject. This saves 207 # having to fetch inactive XML up front, and gives users like 208 # a hook to actually serialize the final XML to return 209 self.emit("xml-requested") 210 return 211 else: 212 if not self._xml_unapplied_changes(): 213 self._reset_xml() 214 return 215 216 # I can't find anyway to make the notebook stay on the current page 217 # So set an idle callback to switch back to the XML page. It causes 218 # a visual UI blip unfortunately 219 self.idle_add(self._reselect_page, prevpage) 220 221 def _after_page_changed_cb(self, notebook, gparam): 222 self._curpage = notebook.get_current_page() 223 224 def _xmleditor_enabled_changed_cb(self): 225 self._set_xmleditor_enabled_from_config() 226