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