1# Copyright (C) 2006-2008, 2015 Red Hat, Inc.
2# Copyright (C) 2006 Daniel P. Berrange <berrange@redhat.com>
3# Copyright (C) 2010 Marc-Andre Lureau <marcandre.lureau@redhat.com>
4#
5# This work is licensed under the GNU GPLv2 or later.
6# See the COPYING file in the top-level directory.
7
8from gi.repository import Gtk
9from gi.repository import Gdk
10
11from virtinst import log
12
13from .serialcon import vmmSerialConsole
14from .sshtunnels import ConnectionInfo
15from .viewers import SpiceViewer, VNCViewer, have_spice_gtk
16from ..baseclass import vmmGObject, vmmGObjectUI
17from ..lib.keyring import vmmKeyring
18
19
20# console-pages IDs
21(_CONSOLE_PAGE_UNAVAILABLE,
22 _CONSOLE_PAGE_SERIAL,
23 _CONSOLE_PAGE_GRAPHICS) = range(3)
24
25# console-gfx-pages IDs
26(_GFX_PAGE_VIEWER,
27 _GFX_PAGE_AUTH,
28 _GFX_PAGE_UNAVAILABLE,
29 _GFX_PAGE_CONNECT) = range(4)
30
31
32class _TimedRevealer(vmmGObject):
33    """
34    Revealer for the fullscreen toolbar, with a bit of extra logic to
35    hide/show based on mouse over
36    """
37    def __init__(self, toolbar):
38        vmmGObject.__init__(self)
39
40        self._in_fullscreen = False
41        self._timeout_id = None
42
43        self._revealer = Gtk.Revealer()
44        self._revealer.add(toolbar)
45
46        # Adding the revealer to the eventbox seems to ensure the
47        # eventbox always has 1 invisible pixel showing at the top of the
48        # screen, which we can use to grab the pointer event to show
49        # the hidden toolbar.
50
51        self._ebox = Gtk.EventBox()
52        self._ebox.add(self._revealer)
53        self._ebox.set_halign(Gtk.Align.CENTER)
54        self._ebox.set_valign(Gtk.Align.START)
55        self._ebox.show_all()
56
57        self._ebox.connect("enter-notify-event", self._enter_notify)
58        self._ebox.connect("leave-notify-event", self._enter_notify)
59
60    def _cleanup(self):
61        self._ebox.destroy()
62        self._ebox = None
63        self._revealer.destroy()
64        self._revealer = None
65        self._timeout_id = None
66
67    def _enter_notify(self, ignore1, ignore2):
68        x, y = self._ebox.get_pointer()
69        alloc = self._ebox.get_allocation()
70        entered = bool(x >= 0 and y >= 0 and
71                       x < alloc.width and y < alloc.height)
72
73        if not self._in_fullscreen:
74            return
75
76        # Pointer exited the toolbar, and toolbar is revealed. Schedule
77        # a timeout to close it, if one isn't already scheduled
78        if not entered and self._revealer.get_reveal_child():
79            self._schedule_unreveal_timeout(1000)
80            return
81
82        self._unregister_timeout()
83        if entered and not self._revealer.get_reveal_child():
84            self._revealer.set_reveal_child(True)
85
86    def _schedule_unreveal_timeout(self, timeout):
87        if self._timeout_id:
88            return  # pragma: no cover
89
90        def cb():
91            self._revealer.set_reveal_child(False)
92            self._timeout_id = None
93        self._timeout_id = self.timeout_add(timeout, cb)
94
95    def _unregister_timeout(self):
96        if self._timeout_id:  # pragma: no cover
97            self.remove_gobject_timeout(self._timeout_id)
98            self._timeout_id = None
99
100    def force_reveal(self, val):
101        self._unregister_timeout()
102        self._in_fullscreen = val
103        self._revealer.set_reveal_child(val)
104        self._schedule_unreveal_timeout(2000)
105
106    def get_overlay_widget(self):
107        return self._ebox
108
109
110def build_keycombo_menu(on_send_key_fn):
111    menu = Gtk.Menu()
112
113    def make_item(accel, combo):
114        name = Gtk.accelerator_get_label(*Gtk.accelerator_parse(accel))
115        item = Gtk.MenuItem(name)
116        item.connect("activate", on_send_key_fn, combo)
117
118        menu.add(item)
119
120    make_item("<Control><Alt>BackSpace", ["Control_L", "Alt_L", "BackSpace"])
121    make_item("<Control><Alt>Delete", ["Control_L", "Alt_L", "Delete"])
122    menu.add(Gtk.SeparatorMenuItem())
123
124    for i in range(1, 13):
125        make_item("<Control><Alt>F%d" % i, ["Control_L", "Alt_L", "F%d" % i])
126    menu.add(Gtk.SeparatorMenuItem())
127
128    make_item("Print", ["Print"])
129
130    menu.show_all()
131    return menu
132
133
134class vmmOverlayToolbar:
135    def __init__(self, on_leave_fn, on_send_key_fn):
136        self._send_key_button = None
137        self._keycombo_menu = None
138        self._toolbar = None
139
140        self.timed_revealer = None
141        self._init_ui(on_leave_fn, on_send_key_fn)
142
143    def _init_ui(self, on_leave_fn, on_send_key_fn):
144        self._keycombo_menu = build_keycombo_menu(on_send_key_fn)
145
146        self._toolbar = Gtk.Toolbar()
147        self._toolbar.set_show_arrow(False)
148        self._toolbar.set_style(Gtk.ToolbarStyle.BOTH_HORIZ)
149        self._toolbar.get_accessible().set_name("Fullscreen Toolbar")
150
151        # Exit button
152        button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_LEAVE_FULLSCREEN)
153        button.set_tooltip_text(_("Leave fullscreen"))
154        button.show()
155        button.get_accessible().set_name("Fullscreen Exit")
156        self._toolbar.add(button)
157        button.connect("clicked", on_leave_fn)
158
159        self._send_key_button = Gtk.ToolButton()
160        self._send_key_button.set_icon_name(
161                                "preferences-desktop-keyboard-shortcuts")
162        self._send_key_button.set_tooltip_text(_("Send key combination"))
163        self._send_key_button.show_all()
164        self._send_key_button.connect("clicked",
165                self._on_send_key_button_clicked_cb)
166        self._send_key_button.get_accessible().set_name("Fullscreen Send Key")
167        self._toolbar.add(self._send_key_button)
168
169        self.timed_revealer = _TimedRevealer(self._toolbar)
170
171    def _on_send_key_button_clicked_cb(self, src):
172        event = Gtk.get_current_event()
173        win = self._toolbar.get_window()
174        rect = Gdk.Rectangle()
175
176        rect.y = win.get_height()
177        self._keycombo_menu.popup_at_rect(win, rect,
178                Gdk.Gravity.NORTH_WEST, Gdk.Gravity.NORTH_WEST, event)
179
180    def cleanup(self):
181        self._keycombo_menu.destroy()
182        self._keycombo_menu = None
183        self._toolbar.destroy()
184        self._toolbar = None
185        self.timed_revealer.cleanup()
186        self.timed_revealer = None
187
188
189class _ConsoleMenu:
190    """
191    Helper class for building the text/graphical console menu list
192    """
193
194    ################
195    # Internal API #
196    ################
197
198    def _build_serial_menu_items(self, vm):
199        devs = vmmSerialConsole.get_serialcon_devices(vm)
200        if len(devs) == 0:
201            return [[_("No text console available"), None, None]]
202
203        ret = []
204        for dev in devs:
205            if dev.DEVICE_TYPE == "console":
206                label = _("Text Console %d") % (dev.get_xml_idx() + 1)
207            else:
208                label = _("Serial %d") % (dev.get_xml_idx() + 1)
209
210            tooltip = vmmSerialConsole.can_connect(vm, dev)
211            ret.append([label, dev, tooltip])
212        return ret
213
214    def _build_graphical_menu_items(self, vm):
215        devs = vm.xmlobj.devices.graphics
216        if len(devs) == 0:
217            return [[_("No graphical console available"), None, None]]
218
219        from ..device.gfxdetails import vmmGraphicsDetails
220
221        ret = []
222        for idx, dev in enumerate(devs):
223            label = (_("Graphical Console") + " " +
224                     vmmGraphicsDetails.graphics_pretty_type_simple(dev.type))
225
226            tooltip = None
227            if idx > 0:
228                label += " %s" % (idx + 1)
229                tooltip = _("virt-manager does not support more "
230                            "than one graphical console")
231
232            ret.append([label, dev, tooltip])
233        return ret
234
235
236    ##############
237    # Public API #
238    ##############
239
240    def rebuild_menu(self, vm, submenu, toggled_cb):
241        oldlabel = None
242        for child in submenu.get_children():
243            if hasattr(child, 'get_active') and child.get_active():
244                oldlabel = child.get_label()
245            submenu.remove(child)
246
247        graphics = self._build_graphical_menu_items(vm)
248        serials = self._build_serial_menu_items(vm)
249
250        # Use label == None to tell the loop to add a separator
251        items = graphics + [[None, None, None]] + serials
252
253        last_item = None
254        for (label, dev, tooltip) in items:
255            if label is None:
256                submenu.add(Gtk.SeparatorMenuItem())
257                continue
258
259            cb = toggled_cb
260            cbdata = dev
261            sensitive = dev and not tooltip
262
263            active = False
264            if oldlabel is None and sensitive:
265                # Select the first selectable option
266                oldlabel = label
267            if label == oldlabel:
268                active = True
269
270            item = Gtk.RadioMenuItem()
271            if last_item is None:
272                last_item = item
273            else:
274                item.join_group(last_item)
275
276            item.set_label(label)
277            item.set_active(active and sensitive)
278            if cbdata and sensitive:
279                item.connect("toggled", cb, cbdata)
280
281            item.set_sensitive(sensitive)
282            item.set_tooltip_text(tooltip or None)
283            submenu.add(item)
284
285        submenu.show_all()
286
287    def activate_default(self, menu):
288        for child in menu.get_children():
289            if child.get_sensitive() and hasattr(child, "toggled"):
290                child.toggled()
291                return True
292        return False
293
294
295class vmmConsolePages(vmmGObjectUI):
296    """
297    Handles all the complex UI handling dictated by the spice/vnc widgets
298    """
299    __gsignals__ = {
300        "page-changed": (vmmGObjectUI.RUN_FIRST, None, []),
301        "leave-fullscreen": (vmmGObjectUI.RUN_FIRST, None, []),
302        "change-title": (vmmGObjectUI.RUN_FIRST, None, []),
303    }
304
305    def __init__(self, vm, builder, topwin):
306        vmmGObjectUI.__init__(self, "console.ui",
307                              None, builder=builder, topwin=topwin)
308
309        self.vm = vm
310        self.top_box = self.widget("console-pages")
311        self._pointer_is_grabbed = False
312
313        # State for disabling modifiers when keyboard is grabbed
314        self._accel_groups = Gtk.accel_groups_from_object(self.topwin)
315        self._gtk_settings_accel = None
316        self._gtk_settings_mnemonic = None
317
318        # Initialize display widget
319        self._viewer = None
320        self._viewer_connect_clicked = False
321        self._in_fullscreen = False
322
323        # Fullscreen toolbar
324        self._keycombo_menu = build_keycombo_menu(self._do_send_key)
325        self._console_list_menu = Gtk.Menu()
326        self._console_list_menu.connect("show",
327                self._populate_console_list_menu)
328
329        self._overlay_toolbar_fullscreen = vmmOverlayToolbar(
330            on_leave_fn=self._leave_fullscreen,
331            on_send_key_fn=self._do_send_key)
332        self.widget("console-overlay").add_overlay(
333                self._overlay_toolbar_fullscreen.timed_revealer.get_overlay_widget())
334
335        # Make viewer widget background always be black
336        black = Gdk.Color(0, 0, 0)
337        self.widget("console-gfx-viewport").modify_bg(Gtk.StateType.NORMAL,
338                                                      black)
339
340        self.widget("console-pages").set_show_tabs(False)
341        self.widget("serial-pages").set_show_tabs(False)
342        self.widget("console-gfx-pages").set_show_tabs(False)
343
344        self._consolemenu = _ConsoleMenu()
345        self._serial_consoles = []
346
347        # Signals are added by vmmVMWindow. Don't use connect_signals here
348        # or it changes will be overwritten
349
350        self.builder.connect_signals({
351            "on_console_pages_switch_page": self._page_changed_cb,
352            "on_console_auth_password_activate": self._auth_login_cb,
353            "on_console_auth_login_clicked": self._auth_login_cb,
354            "on_console_connect_button_clicked": self._connect_button_clicked_cb,
355        })
356
357        self.widget("console-gfx-scroll").connect("size-allocate",
358            self._scroll_size_allocate)
359        self.widget("console-gfx-pages").connect("switch-page",
360                self._page_changed_cb)
361
362
363    def _cleanup(self):
364        self.vm = None
365
366        if self._viewer:
367            self._viewer.cleanup()  # pragma: no cover
368        self._viewer = None
369
370        self._overlay_toolbar_fullscreen.cleanup()
371
372        for serial in self._serial_consoles:
373            serial.cleanup()
374        self._serial_consoles = []
375
376
377    #################
378    # Internal APIs #
379    #################
380
381    def _disable_modifiers(self):
382        if self._gtk_settings_accel is not None:
383            return  # pragma: no cover
384
385        for g in self._accel_groups:
386            self.topwin.remove_accel_group(g)
387
388        settings = Gtk.Settings.get_default()
389        self._gtk_settings_accel = settings.get_property('gtk-menu-bar-accel')
390        settings.set_property('gtk-menu-bar-accel', None)
391
392        self._gtk_settings_mnemonic = settings.get_property(
393            "gtk-enable-mnemonics")
394        settings.set_property("gtk-enable-mnemonics", False)
395
396    def _enable_modifiers(self):
397        if self._gtk_settings_accel is None:
398            return
399
400        settings = Gtk.Settings.get_default()
401        settings.set_property('gtk-menu-bar-accel', self._gtk_settings_accel)
402        self._gtk_settings_accel = None
403
404        if self._gtk_settings_mnemonic is not None:
405            settings.set_property("gtk-enable-mnemonics",
406                                  self._gtk_settings_mnemonic)
407
408        for g in self._accel_groups:
409            self.topwin.add_accel_group(g)
410
411    def _do_send_key(self, src, keys):
412        ignore = src
413
414        if keys is not None:
415            self._viewer.console_send_keys(keys)
416
417
418    ###########################
419    # Resize and scaling APIs #
420    ###########################
421
422    def _scroll_size_allocate(self, src_ignore, req):
423        if not self._viewer:
424            return
425
426        res = self._viewer.console_get_desktop_resolution()
427        if res is None:
428            if not self.config.CLITestOptions.fake_console_resolution:
429                return
430            res = (800, 600)
431
432        scroll = self.widget("console-gfx-scroll")
433        is_scale = self._viewer.console_get_scaling()
434        is_resizeguest = self._viewer.console_get_resizeguest()
435
436        dx = 0
437        dy = 0
438        align_ratio = float(req.width) / float(req.height)
439
440        # pylint: disable=unpacking-non-sequence
441        desktop_w, desktop_h = res
442        desktop_ratio = float(desktop_w) / float(desktop_h)
443
444        if is_scale:
445            # Make sure we never show scrollbars when scaling
446            scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)
447        else:
448            scroll.set_policy(Gtk.PolicyType.AUTOMATIC,
449                              Gtk.PolicyType.AUTOMATIC)
450
451        if is_resizeguest:
452            # With resize guest, we don't want to maintain aspect ratio,
453            # since the guest can resize to arbitrary resolutions.
454            viewer_alloc = Gdk.Rectangle()
455            viewer_alloc.width = req.width
456            viewer_alloc.height = req.height
457            self._viewer.console_size_allocate(viewer_alloc)
458            return
459
460        if not is_scale:
461            # Scaling disabled is easy, just force the VNC widget size. Since
462            # we are inside a scrollwindow, it shouldn't cause issues.
463            self._viewer.console_set_size_request(desktop_w, desktop_h)
464            return
465
466        # Make sure there is no hard size requirement so we can scale down
467        self._viewer.console_set_size_request(-1, -1)
468
469        # Make sure desktop aspect ratio is maintained
470        if align_ratio > desktop_ratio:
471            desktop_w = int(req.height * desktop_ratio)
472            desktop_h = req.height
473            dx = (req.width - desktop_w) // 2
474
475        else:
476            desktop_w = req.width
477            desktop_h = int(req.width // desktop_ratio)
478            dy = (req.height - desktop_h) // 2
479
480        viewer_alloc = Gdk.Rectangle()
481        viewer_alloc.x = dx
482        viewer_alloc.y = dy
483        viewer_alloc.width = desktop_w
484        viewer_alloc.height = desktop_h
485        self._viewer.console_size_allocate(viewer_alloc)
486
487    def _viewer_get_resizeguest_tooltip(self):
488        tooltip = ""
489        if self._viewer:
490            if self._viewer.viewer_type != "spice":
491                tooltip = (
492                    _("Graphics type '%s' does not support auto resize.") %
493                    self._viewer.viewer_type)
494            elif not self._viewer.console_has_agent():
495                tooltip = _("Guest agent is not available.")
496        return tooltip
497
498    def _sync_resizeguest_with_display(self):
499        if not self._viewer:
500            return
501
502        val = bool(self.vm.get_console_resizeguest())
503        self._viewer.console_set_resizeguest(val)
504        self.widget("console-gfx-scroll").queue_resize()
505
506    def _set_size_to_vm(self):
507        # Resize the console to best fit the VM resolution
508        if not self._viewer:
509            return  # pragma: no cover
510        if not self._viewer.console_get_desktop_resolution():
511            return  # pragma: no cover
512
513        top_w, top_h = self.topwin.get_size()
514        viewer_alloc = self.widget("console-gfx-scroll").get_allocation()
515        desktop_w, desktop_h = self._viewer.console_get_desktop_resolution()
516
517        self.topwin.unmaximize()
518        self.topwin.resize(
519            desktop_w + (top_w - viewer_alloc.width),
520            desktop_h + (top_h - viewer_alloc.height))
521
522
523    ################
524    # Scaling APIs #
525    ################
526
527    def _sync_scaling_with_display(self):
528        if not self._viewer:
529            return
530
531        fs = self._in_fullscreen
532        curscale = self._viewer.console_get_scaling()
533        scale_type = self.vm.get_console_scaling()
534
535        if (scale_type == self.config.CONSOLE_SCALE_NEVER and
536            curscale is True):
537            self._viewer.console_set_scaling(False)
538        elif (scale_type == self.config.CONSOLE_SCALE_ALWAYS and
539              curscale is False):
540            self._viewer.console_set_scaling(True)
541        elif (scale_type == self.config.CONSOLE_SCALE_FULLSCREEN and
542              curscale != fs):
543            self._viewer.console_set_scaling(fs)
544
545        # Refresh viewer size
546        self.widget("console-gfx-scroll").queue_resize()
547
548
549    ###################
550    # Fullscreen APIs #
551    ###################
552
553    def _leave_fullscreen(self, ignore=None):
554        self.emit("leave-fullscreen")
555
556    def _change_fullscreen(self, do_fullscreen):
557        if do_fullscreen:
558            self._in_fullscreen = True
559            self.topwin.fullscreen()
560            self._overlay_toolbar_fullscreen.timed_revealer.force_reveal(True)
561        else:
562            self._in_fullscreen = False
563            self._overlay_toolbar_fullscreen.timed_revealer.force_reveal(False)
564            self.topwin.unfullscreen()
565
566        self._sync_scaling_with_display()
567
568
569    ##########################
570    # State tracking methods #
571    ##########################
572
573    def _show_vm_status_unavailable(self):
574        if self.vm.is_crashed():  # pragma: no cover
575            self._activate_vm_unavailable_page(_("Guest has crashed."))
576        else:
577            self._activate_vm_unavailable_page(_("Guest is not running."))
578
579    def _close_viewer(self):
580        self._leave_fullscreen()
581        self._viewer_connect_clicked = False
582
583        for serial in self._serial_consoles:
584            serial.close()
585
586        if self._viewer is None:
587            return
588        self._viewer.console_remove_display_from_widget(
589            self.widget("console-gfx-viewport"))
590        self._viewer.cleanup()
591        self._viewer = None
592        log.debug("Viewer disconnected")
593
594    def _refresh_vm_state(self):
595        self._activate_default_console_page()
596
597
598    ###########################
599    # console page navigation #
600    ###########################
601
602    def _activate_gfx_unavailable_page(self, msg):
603        self._close_viewer()
604        self.widget("console-gfx-pages").set_current_page(
605                _GFX_PAGE_UNAVAILABLE)
606        if msg:
607            self.widget("console-gfx-unavailable").set_label(
608                    "<b>" + msg + "</b>")
609
610    def _activate_vm_unavailable_page(self, msg):
611        """
612        This is the top level error page. We should only set it for very
613        specific error cases, because when it is set and the VM is running
614        we take that to mean we should attempt to connect to the default
615        console.
616        """
617        self._close_viewer()
618        self.widget("console-pages").set_current_page(
619                _CONSOLE_PAGE_UNAVAILABLE)
620        if msg:
621            self.widget("console-unavailable").set_label(
622                    "<b>" + msg + "</b>")
623        self._activate_gfx_unavailable_page(msg)
624
625    def _activate_auth_page(self, withPassword, withUsername):
626        (pw, username) = vmmKeyring.get_instance().get_console_password(self.vm)
627
628        self.widget("console-auth-password").set_visible(withPassword)
629        self.widget("label-auth-password").set_visible(withPassword)
630
631        self.widget("console-auth-username").set_visible(withUsername)
632        self.widget("label-auth-username").set_visible(withUsername)
633
634        self.widget("console-auth-username").set_text(username)
635        self.widget("console-auth-password").set_text(pw)
636
637        has_keyring = vmmKeyring.get_instance().is_available()
638        remember = bool(withPassword and pw) or (withUsername and username)
639        remember = has_keyring and remember
640        self.widget("console-auth-remember").set_sensitive(has_keyring)
641        self.widget("console-auth-remember").set_active(remember)
642
643        self.widget("console-gfx-pages").set_current_page(_GFX_PAGE_AUTH)
644
645        if withUsername:
646            self.widget("console-auth-username").grab_focus()
647        else:
648            self.widget("console-auth-password").grab_focus()
649
650    def _activate_gfx_viewer_page(self):
651        self.widget("console-pages").set_current_page(_CONSOLE_PAGE_GRAPHICS)
652        self.widget("console-gfx-pages").set_current_page(_GFX_PAGE_VIEWER)
653        if self._viewer:
654            self._viewer.console_grab_focus()
655
656    def _activate_gfx_connect_page(self):
657        self.widget("console-gfx-pages").set_current_page(_GFX_PAGE_CONNECT)
658
659    def _viewer_is_visible(self):
660        is_visible = self.widget("console-pages").is_visible()
661        cpage = self.widget("console-pages").get_current_page()
662        gpage = self.widget("console-gfx-pages").get_current_page()
663
664        return bool(
665            is_visible and
666            cpage == _CONSOLE_PAGE_GRAPHICS and
667            gpage == _GFX_PAGE_VIEWER and
668            self._viewer and self._viewer.console_is_open())
669
670    def _viewer_can_usb_redirect(self):
671        return (self._viewer_is_visible() and
672                self._viewer.console_has_usb_redirection() and
673                self.vm.has_spicevmc_type_redirdev())
674
675
676    #########################
677    # Viewer login attempts #
678    #########################
679
680    def _init_viewer(self):
681        if self._viewer or not self.is_visible():
682            # Don't try and login for these cases
683            return
684
685        ginfo = None
686        try:
687            gdevs = self.vm.xmlobj.devices.graphics
688            gdev = gdevs and gdevs[0] or None
689            if gdev:
690                ginfo = ConnectionInfo(self.vm.conn, gdev)
691        except Exception as e:  # pragma: no cover
692            # We can fail here if VM is destroyed: xen is a bit racy
693            # and can't handle domain lookups that soon after
694            log.exception("Getting graphics console failed: %s", str(e))
695            return
696
697        if ginfo is None:
698            log.debug("No graphics configured for guest")
699            self._activate_gfx_unavailable_page(
700                _("Graphical console not configured for guest"))
701            return
702
703        if ginfo.gtype not in self.config.embeddable_graphics():
704            log.debug("Don't know how to show graphics type '%s' "
705                          "disabling console page", ginfo.gtype)
706
707            msg = (_("Cannot display graphical console type '%s'")
708                     % ginfo.gtype)
709
710            self._activate_gfx_unavailable_page(msg)
711            return
712
713        if (not self.vm.get_console_autoconnect() and
714            not self._viewer_connect_clicked):
715            self._activate_gfx_connect_page()
716            return
717
718        self._activate_gfx_unavailable_page(
719            _("Connecting to graphical console for guest"))
720
721        log.debug("Starting connect process for %s", ginfo.logstring())
722        try:
723            if ginfo.gtype == "vnc":
724                viewer_class = VNCViewer
725            elif ginfo.gtype == "spice":
726                if not have_spice_gtk:  # pragma: no cover
727                    raise RuntimeError("Error opening Spice console, "
728                                       "SpiceClientGtk missing")
729                viewer_class = SpiceViewer
730
731            self._viewer = viewer_class(self.vm, ginfo)
732            self._connect_viewer_signals()
733
734            self._viewer.console_open()
735        except Exception as e:
736            log.exception("Error connecting to graphical console")
737            self._activate_gfx_unavailable_page(
738                    _("Error connecting to graphical console:\n%s") % e)
739
740    def _set_credentials(self, src_ignore=None):
741        passwd = self.widget("console-auth-password")
742        username = self.widget("console-auth-username")
743
744        if passwd.get_visible():
745            self._viewer.console_set_password(passwd.get_text())
746        if username.get_visible():
747            self._viewer.console_set_username(username.get_text())
748
749        if self.widget("console-auth-remember").get_active():
750            vmmKeyring.get_instance().set_console_password(
751                    self.vm, passwd.get_text(), username.get_text())
752        else:
753            vmmKeyring.get_instance().del_console_password(self.vm)
754
755
756    ##########################
757    # Viewer signal handling #
758    ##########################
759
760    def _viewer_add_display(self, ignore, display):
761        self.widget("console-gfx-viewport").add(display)
762
763        # Sync initial settings
764        self._sync_scaling_with_display()
765        self._sync_resizeguest_with_display()
766
767    def _pointer_grabbed(self, ignore):
768        self._pointer_is_grabbed = True
769        self.emit("change-title")
770
771    def _pointer_ungrabbed(self, ignore):
772        self._pointer_is_grabbed = False
773        self.emit("change-title")
774
775    def _viewer_allocate_cb(self, src, ignore):
776        self.widget("console-gfx-scroll").queue_resize()
777
778    def _viewer_keyboard_grab_cb(self, src):
779        self._viewer_sync_modifiers()
780
781    def _serial_focus_changed_cb(self, src, event):
782        self._viewer_sync_modifiers()
783
784    def _viewer_sync_modifiers(self):
785        serial_has_focus = any([s.has_focus() for s in self._serial_consoles])
786        viewer_keyboard_grab = (self._viewer and
787                self._viewer.console_has_keyboard_grab())
788
789        if serial_has_focus or viewer_keyboard_grab:
790            self._disable_modifiers()
791        else:
792            self._enable_modifiers()
793
794    def _viewer_auth_error(self, ignore, errmsg, viewer_will_disconnect):
795        errmsg = _("Viewer authentication error: %s") % errmsg
796        self.err.val_err(errmsg)
797
798        if viewer_will_disconnect:
799            # GtkVNC will disconnect after an auth error, so lets do it for
800            # them and re-init the viewer (which will be triggered by
801            # _refresh_vm_state if needed)
802            self._activate_vm_unavailable_page(errmsg)
803
804        self._refresh_vm_state()
805
806    def _viewer_need_auth(self, ignore, withPassword, withUsername):
807        self._activate_auth_page(withPassword, withUsername)
808
809    def _viewer_agent_connected(self, ignore):
810        # Tell the vmwindow to trigger a state refresh, since
811        # resizeguest setting depends on the agent value
812        if self.widget("console-pages").is_visible():  # pragma: no cover
813            self.emit("page-changed")
814
815    def _viewer_usb_redirect_error(self, ignore, errstr):
816        self.err.show_err(
817                _("USB redirection error"),
818                text2=str(errstr), modal=True)  # pragma: no cover
819
820    def _viewer_disconnected_set_page(self, errdetails, ssherr):
821        if self.vm.is_runable():  # pragma: no cover
822            # Exit was probably for legitimate reasons
823            self._show_vm_status_unavailable()
824            return
825
826        msg = _("Viewer was disconnected.")
827        if errdetails:
828            msg += "\n" + errdetails
829        if ssherr:
830            log.debug("SSH tunnel error output: %s", ssherr)
831            msg += "\n\n"
832            msg += _("SSH tunnel error output: %s") % ssherr
833
834        self._activate_gfx_unavailable_page(msg)
835
836    def _viewer_disconnected(self, ignore, errdetails, ssherr):
837        self._activate_gfx_unavailable_page(_("Viewer disconnected."))
838        log.debug("Viewer disconnected")
839
840        # Make sure modifiers are set correctly
841        self._viewer_sync_modifiers()
842
843        self._viewer_disconnected_set_page(errdetails, ssherr)
844
845    def _viewer_connected(self, ignore):
846        log.debug("Viewer connected")
847        self._activate_gfx_viewer_page()
848
849        # Make sure modifiers are set correctly
850        self._viewer_sync_modifiers()
851
852    def _connect_viewer_signals(self):
853        self._viewer.connect("add-display-widget", self._viewer_add_display)
854        self._viewer.connect("pointer-grab", self._pointer_grabbed)
855        self._viewer.connect("pointer-ungrab", self._pointer_ungrabbed)
856        self._viewer.connect("size-allocate", self._viewer_allocate_cb)
857        self._viewer.connect("keyboard-grab", self._viewer_keyboard_grab_cb)
858        self._viewer.connect("keyboard-ungrab", self._viewer_keyboard_grab_cb)
859        self._viewer.connect("connected", self._viewer_connected)
860        self._viewer.connect("disconnected", self._viewer_disconnected)
861        self._viewer.connect("auth-error", self._viewer_auth_error)
862        self._viewer.connect("need-auth", self._viewer_need_auth)
863        self._viewer.connect("agent-connected", self._viewer_agent_connected)
864        self._viewer.connect("usb-redirect-error",
865            self._viewer_usb_redirect_error)
866
867
868    ##############################
869    # Console list menu handling #
870    ##############################
871
872    def _console_list_menu_toggled(self, src, dev):
873        if not dev or dev.DEVICE_TYPE == "graphics":
874            self.widget("console-pages").set_current_page(
875                    _CONSOLE_PAGE_GRAPHICS)
876            self.idle_add(self._init_viewer)
877            return
878
879        target_port = dev.get_xml_idx()
880        serial = None
881        name = src.get_label()
882        for s in self._serial_consoles:
883            if s.name == name:
884                serial = s
885                break
886
887        if not serial:
888            serial = vmmSerialConsole(self.vm, target_port, name)
889            serial.set_focus_callbacks(self._serial_focus_changed_cb,
890                                       self._serial_focus_changed_cb)
891
892            title = Gtk.Label(label=name)
893            self.widget("serial-pages").append_page(serial.get_box(), title)
894            self._serial_consoles.append(serial)
895
896        serial.open_console()
897        page_idx = self._serial_consoles.index(serial)
898        self.widget("console-pages").set_current_page(_CONSOLE_PAGE_SERIAL)
899        self.widget("serial-pages").set_current_page(page_idx)
900
901    def _populate_console_list_menu(self, ignore=None):
902        self._consolemenu.rebuild_menu(
903                self.vm, self._console_list_menu,
904                self._console_list_menu_toggled)
905
906    def _toggle_first_console_menu_item(self):
907        # We iterate through the 'console' menu and activate the first
908        # valid entry... hacky but it works
909        self._populate_console_list_menu()
910        found = self._consolemenu.activate_default(self._console_list_menu)
911        if not found:
912            # Calling this with dev=None will trigger _init_viewer
913            # which shows some meaningful errors
914            self._console_list_menu_toggled(None, None)
915
916    def _activate_default_console_page(self):
917        if self.vm.is_runable():
918            self._show_vm_status_unavailable()
919            return
920
921        viewer_initialized = (self._viewer and self._viewer.console_is_open())
922        if viewer_initialized:
923            return
924
925        cpage = self.widget("console-pages").get_current_page()
926        if cpage != _CONSOLE_PAGE_UNAVAILABLE:
927            return
928
929        # If we are in this condition it should mean the VM was
930        # just started, so connect to the default page
931        self._toggle_first_console_menu_item()
932
933
934    ################
935    # UI listeners #
936    ################
937
938    def _auth_login_cb(self, src):
939        self._set_credentials()
940
941    def _connect_button_clicked_cb(self, src):
942        self._viewer_connect_clicked = True
943        self._init_viewer()
944
945    def _page_changed_cb(self, src, origpage, newpage):
946        # Hide the contents of all other pages, so they don't screw
947        # up window sizing
948        for i in range(src.get_n_pages()):
949            src.get_nth_page(i).set_visible(i == newpage)
950
951        # Dispatch the next bit in idle_add, so the UI size can change
952        self.idle_emit("page-changed")
953
954
955    ###########################
956    # API used by vmmVMWindow #
957    ###########################
958
959    def vmwindow_viewer_has_usb_redirection(self):
960        return bool(self._viewer and
961            self._viewer.console_has_usb_redirection())
962    def vmwindow_viewer_get_usb_widget(self):
963        return self._viewer.console_get_usb_widget()
964    def vmwindow_viewer_get_pixbuf(self):
965        return self._viewer.console_get_pixbuf()
966
967    def vmwindow_close(self):
968        return self._activate_vm_unavailable_page(
969                _("Viewer disconnected."))
970    def vmwindow_get_title_message(self):
971        if self._pointer_is_grabbed and self._viewer:
972            keystr = self._viewer.console_get_grab_keys()
973            return _("Press %s to release pointer.") % keystr
974
975    def vmwindow_activate_default_console_page(self):
976        return self._activate_default_console_page()
977    def vmwindow_refresh_vm_state(self):
978        return self._refresh_vm_state()
979
980    def vmwindow_set_size_to_vm(self):
981        return self._set_size_to_vm()
982    def vmwindow_set_fullscreen(self, do_fullscreen):
983        self._change_fullscreen(do_fullscreen)
984
985    def vmwindow_get_keycombo_menu(self):
986        return self._keycombo_menu
987    def vmwindow_get_console_list_menu(self):
988        return self._console_list_menu
989    def vmwindow_get_viewer_is_visible(self):
990        return self._viewer_is_visible()
991    def vmwindow_get_can_usb_redirect(self):
992        return self._viewer_can_usb_redirect()
993    def vmwindow_get_resizeguest_tooltip(self):
994        return self._viewer_get_resizeguest_tooltip()
995
996    def vmwindow_sync_scaling_with_display(self):
997        return self._sync_scaling_with_display()
998    def vmwindow_sync_resizeguest_with_display(self):
999        return self._sync_resizeguest_with_display()
1000