From 77479267f3e1b30408e3f5abf2d51f89f0f86cc3 Mon Sep 17 00:00:00 2001 From: Cole Robinson Date: Wed, 18 May 2011 17:22:07 -0400 Subject: [PATCH] console: Use a hiding automenu for fullscreen mode Code is a python implementation of autodrawer. I originally found it in the vinagre sources: http://git.gnome.org/browse/vinagre/tree/vinagre/view For now the menu just provides a button to leave fullscreen --- src/virtManager/autodrawer.py | 582 ++++++++++++++++++++++++++++++++++ src/virtManager/console.py | 52 +++ 2 files changed, 634 insertions(+) create mode 100644 src/virtManager/autodrawer.py diff --git a/src/virtManager/autodrawer.py b/src/virtManager/autodrawer.py new file mode 100644 index 000000000..bd2f44567 --- /dev/null +++ b/src/virtManager/autodrawer.py @@ -0,0 +1,582 @@ +# +# Copyright (C) 2011 Red Hat, Inc. +# Copyright (C) 2011 Cole Robinson +# +# Python implementation of autodrawer, originally found in vinagre sources: +# http://git.gnome.org/browse/vinagre/tree/vinagre/view +# Copyright (c) 2005 VMware Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA. +# + +import gobject +import gtk + +parentclass = gtk.VBox + +class OverBox(parentclass): + """ + Implementation of an overlapping box + """ + def __init__(self): + parentclass.__init__(self) + + self.underWin = None + self.underWidget = None + self.overWin = None + self.overWidget = None + self.overWidth = -1 + self.overHeight = -1 + self.min = 0 + self._fraction = 0 + self.verticalOffset = 0 + + self.set_has_window(True) + + #################### + # Internal helpers # + #################### + + def is_realized(self): + return bool(self.flags() & gtk.REALIZED) + def set_realized(self): + flags = self.flags() | gtk.REALIZED + self.set_flags(flags) + + def _get_actual_min(self): + """ + Retrieve the actual 'min' value, i.e. a value that is guaranteed + not to exceed the height of the 'over' child. + """ + ret = min(self.min, self.overHeight) + return ret + + def _get_under_window_geometry(self): + geo = gtk.gdk.Rectangle() + actual_min = self._get_actual_min() + + geo.x = 0 + geo.y = actual_min + geo.width = self.allocation.width + geo.height = (self.allocation.height - actual_min) + + return geo + + def _get_over_window_geometry(self): + geo = gtk.gdk.Rectangle() + boxwidth = self.allocation.width + expand = True + fill = True + padding = 0 + actual_min = self._get_actual_min() + + if self.overWidget: + expand = self.child_get(self.overWidget, "expand")[0] + fill = self.child_get(self.overWidget, "fill")[0] + padding = self.child_get(self.overWidget, "padding")[0] + + if not expand: + width = min(self.overWidth, boxwidth - padding) + x = padding + elif not fill: + width = min(self.overWidth, boxwidth) + x = ((boxwidth - width) / 2) + else: + width = boxwidth + x = 0 + + y = (((self.overHeight - actual_min) * (self.fraction - 1)) + + self.verticalOffset) + height = self.overHeight + + geo.x = x + geo.y = y + geo.width = width + geo.height = height + return geo + + def _set_background(self): + style = self.get_style() + style.set_background(self.window, gtk.STATE_NORMAL) + style.set_background(self.underWin, gtk.STATE_NORMAL) + style.set_background(self.overWin, gtk.STATE_NORMAL) + + def _size_request(self): + underw, underh = self.underWidget.size_request() + overw, overh = self.overWidget.size_request() + + self.overWidth = overw + self.overHeight = overh + + expand = self.child_get(self.overWidget, "expand") + fill = self.child_get(self.overWidget, "fill") + padding = self.child_get(self.overWidget, "padding") + + if expand or fill: + wpad = 0 + else: + wpad = padding + + width = max(underw, overw + wpad) + height = max(underh + self._get_actual_min(), overh) + + return width, height + + ######################## + # Custom functionality # + ######################## + + def do_set_over(self, widget): + self.set_over(widget) + + def set_over(self, widget): + if self.overWidget: + self.remove(self.overWidget) + + widget.set_parent_window(self.overWin) + self.add(widget) + self.overWidget = widget + + def set_under(self, widget): + if self.underWidget: + self.remove(self.underWidget) + + widget.set_parent_window(self.underWin) + self.add(widget) + self.underWidget = widget + self.underWidget.show_all() + + def set_min(self, newmin): + self.min = newmin + self.queue_resize() + + def set_fraction(self, newfraction): + self._fraction = newfraction + + if self.is_realized(): + overgeo = self._get_over_window_geometry() + self.overWin.move(overgeo.x, overgeo.y) + def get_fraction(self): + return self._fraction + fraction = property(get_fraction, set_fraction) + + def set_vertical_offset(self, newoff): + self.verticalOffset = newoff + + if self.is_realized(): + overgeo = self._get_over_window_geometry() + self.overWin.move(overgeo.x, overgeo.y) + + #################### + # Standard methods # + #################### + + def do_map(self): + self.get_window().show() + parentclass.do_map(self) + + def do_unmap(self): + self.get_window().hide() + parentclass.do_unmap(self) + + def do_realize(self): + event_mask = self.get_events() | gtk.gdk.EXPOSURE_MASK + colormap = self.get_colormap() + visual = self.get_visual() + + self.set_realized() + + def make_window(parent, rect): + return gtk.gdk.Window(parent, + rect.width, + rect.height, + gtk.gdk.WINDOW_CHILD, + event_mask, + gtk.gdk.INPUT_OUTPUT, + x=rect.x, + y=rect.y, + colormap=colormap, + visual=visual) + + window = make_window(self.get_parent_window(), self.allocation) + self.window = window + self.window.set_user_data(self) + self.style.attach(window) + + self.underWin = make_window(window, + self._get_under_window_geometry()) + self.underWin.set_user_data(self) + if self.underWidget: + self.underWidget.set_parent_window(self.underWin) + self.underWin.show() + + self.overWin = make_window(window, + self._get_over_window_geometry()) + self.overWin.set_user_data(self) + if self.overWidget: + self.overWidget.set_parent_window(self.overWin) + self.overWin.show() + + self._set_background() + + def do_unrealize(self): + parentclass.do_unrealize(self) + + self.overWin.destroy() + self.overWin = None + + self.underWin.destroy() + self.underWin = None + + def do_size_request(self, req): + height, width = self._size_request() + + req.height = height + req.width = width + + def do_size_allocate(self, newalloc): + self.allocation = newalloc + + over = self._get_over_window_geometry() + under = self._get_under_window_geometry() + + if self.is_realized(): + self.window.move_resize(newalloc.x, newalloc.y, + newalloc.width, newalloc.height) + self.underWin.move_resize(under.x, under.y, + under.width, under.height) + self.overWin.move_resize(over.x, over.y, + over.width, over.height) + + under.x = 0 + under.y = 0 + self.underWidget.size_allocate(under) + over.x = 0 + over.y = 0 + self.overWidget.size_allocate(over) + + def do_style_set(self, style): + if self.is_realized(): + self.set_background() + + parentclass.do_style_set(self, style) + + +class Drawer(OverBox): + """ + Implementation of a drawer, basically a floating toolbar + """ + def __init__(self): + OverBox.__init__(self) + + self.period = 10 + self.step = 0.2 + self.goal = 0 + + self.timer_pending = False + self.timer_id = None + + + # XXX: C version has a finalize impl + + #################### + # Internal helpers # + #################### + + def _on_timer(self): + fraction = self.fraction + + if self.goal == fraction: + self.timer_pending = False + return False + + if self.goal > fraction: + self.fraction = min(fraction + self.step, self.goal) + else: + self.fraction = max(fraction - self.step, self.goal) + + return True + + ############## + # Public API # + ############## + + def set_speed(self, period, step): + self.period = period + + if self.timer_pending: + gobject.source_remove(self.timer_id) + self.timer_id = gobject.timeout_add(self.period, self._on_timer) + + self.step = step + + def set_goal(self, goal): + self.goal = goal + + if not self.timer_pending: + self.timer_id = gobject.timeout_add(self.period, self._on_timer) + self.timer_pending = True + + def get_close_time(self): + return (self.period * (int(1 / self.step) + 1)) + + +class AutoDrawer(Drawer): + """ + Implemenation of an autodrawer, a drawer that hides and reappears on + mouse over, slowly sliding into view + """ + def __init__(self): + Drawer.__init__(self) + + self.active = True + self.pinned = False + self.forceClosing = False + self.inputUngrabbed = True + self.opened = False + + self.fill = True + self.offset = -1 + + self.closeConnection = 0 + self.delayConnection = 0 + self.delayValue = 250 + self.overlapPixels = 0 + self.noOverlapPixels = 1 + + self.over = None + self.eventBox = gtk.EventBox() + self.eventBox.show() + OverBox.set_over(self, self.eventBox) + + self.eventBox.connect("enter-notify-event", self._on_over_enter_leave) + self.eventBox.connect("leave-notify-event", self._on_over_enter_leave) + self.eventBox.connect("grab-notify", self._on_grab_notify) + + self.connect("hierarchy-changed", self._on_hierarchy_changed) + + self._update(True) + self._refresh_packing() + + # XXX: Has a finalize method + + #################### + # Internal Helpers # + #################### + + def set_over(self, newover): + oldChild = self.eventBox.get_child() + + if oldChild: + self.eventBox.remove(oldChild) + + if newover: + self.eventBox.add(newover) + + self.over = newover + + def _update(self, do_immediate): + + toplevel = self.get_toplevel() + if not toplevel or not toplevel.is_toplevel(): + # The autoDrawer cannot function properly without a toplevel. + return + + self.opened = False + + # Is the drawer pinned open? + if self.pinned: + do_immediate = True + self.opened = True + + # Is the mouse cursor inside the event box? */ + x, y = self.eventBox.get_pointer() + alloc = self.eventBox.get_allocation() + if x > -1 and y > -1 and x < alloc.width and y < alloc.height: + self.opened = True + + # If there is a focused widget, is it inside the event box? */ + focus = toplevel.get_focus() + if focus and focus.is_ancestor(self.eventBox): + do_immediate = True + self.opened = True + + # If input is grabbed, is it on behalf of a widget inside the + # event box? + if not self.inputUngrabbed: + grabbed = None + + if toplevel.get_group(): + # XXX: Not in pygtk? + #grabbed = toplevel.get_group().get_current_grab() + pass + if not grabbed: + grabbed = gtk.grab_get_current() + + if grabbed and isinstance(grabbed, gtk.Menu): + + while True: + menuAttach = grabbed.get_attach_widget() + if not menuAttach: + break + + grabbed = menuAttach + if not isinstance(grabbed, gtk.MenuItem): + break + + menuItemParent = grabbed.get_parent() + if not isinstance(menuItemParent, gtk.Menu): + break + + grabbed = menuItemParent + + if grabbed and grabbed.is_ancestor(self.eventBox): + do_immediate = True + self.opened = True + + if self.delayConnection: + gobject.source_remove(self.delayConnection) + + + if self.forceClosing: + self._enforce(True) + elif do_immediate: + self._enforce(False) + else: + self.delayConnection = gobject.timeout_add(self.delayValue, + self._on_enforce_delay) + + + def _refresh_packing(self): + expand = bool(self.fill or (self.offset < 0)) + + if expand or self.fill: + padding = 0 + else: + padding = self.offset + + self.set_child_packing(self.eventBox, expand, self.fill, padding, + gtk.PACK_START) + + def _enforce(self, do_animate): + if not self.active: + self.set_min(0) + self.set_fraction(0) + return + + self.set_min(self.noOverlapPixels) + + if self.opened and not self.forceClosing: + fraction = 1 + else: + alloc = self.over.get_allocation() + fraction = (float(self.overlapPixels) / alloc.height) + + if not do_animate: + self.set_fraction(fraction) + self.set_goal(fraction) + + + ############# + # Listeners # + ############# + + def _set_focus(self, ignore1, ignore2): + self._update(False) + + def _on_over_enter_leave(self, ignore1, ignore2): + self._update(False) + + def _on_grab_notify(self, eventbox, is_ungrabbed): + ignore = eventbox + self.inputUngrabbed = is_ungrabbed + self._update(False) + + def _on_hierarchy_changed(self, oldTopLevel, ignore): + newTopLevel = self.get_toplevel() + + if oldTopLevel and oldTopLevel.is_toplevel(): + oldTopLevel.disconnect_by_func(self._set_focus) + + if newTopLevel and newTopLevel.is_toplevel(): + newTopLevel.connect_after("set_focus", self._set_focus) + + self._update(True) + + def _on_enforce_delay(self): + self.delayConnection = 0 + self._enforce(True) + + return False + + def _on_close_delay(self): + self.closeConnection = 0 + self.forceClosing = False + + return False + + + ############## + # Public API # + ############## + + def set_slide_delay(self, delay): + self.delayValue = delay + + def set_overlap_pixels(self, overlap_pixels): + self.overlapPixels = overlap_pixels + self._update(True) + + def set_nooverlap_pixels(self, nooverlap_pixels): + self.noOverlapPixels = nooverlap_pixels + self._update(True) + + def set_active(self, active): + self.active = active + self._update(True) + + def set_pinned(self, pinned): + self.pinned = pinned + self._update(False) + + def set_fill(self, fill): + self.fill = fill + self._refresh_packing() + + def set_offset(self, offset): + self.offset = offset + self._refresh_packing() + + def drawer_close(self): + toplevel = self.get_toplevel() + if not toplevel or not toplevel.is_toplevel(): + # The autoDrawer cannot function properly without a toplevel. + return + + focus = toplevel.get_focus() + if focus and focus.is_ancestor(self.eventBox): + toplevel.set_focus(None) + + self.forceClosing = True + self.closeConnection = gobject.timeout_add( + self.get_close_time() + self.delayValue, + self._on_close_delay) + + self._update(True) + +gobject.type_register(OverBox) +gobject.type_register(Drawer) +gobject.type_register(AutoDrawer) diff --git a/src/virtManager/console.py b/src/virtManager/console.py index 6f7cd69d5..5e6ebfb30 100644 --- a/src/virtManager/console.py +++ b/src/virtManager/console.py @@ -37,6 +37,8 @@ import signal import socket import logging +import virtManager.util as util +from virtManager.autodrawer import AutoDrawer from virtManager.baseclass import vmmGObjectUI, vmmGObject from virtManager.error import vmmErrorDialog @@ -545,6 +547,11 @@ class vmmConsolePages(vmmGObjectUI): self.viewer_connecting = False self.scale_type = self.vm.get_console_scaling() + # Fullscreen toolbar + self.fs_toolbar = None + self.fs_drawer = None + self.init_fs_toolbar() + finish_img = gtk.image_new_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_BUTTON) self.window.get_widget("console-auth-login").set_image(finish_img) @@ -579,10 +586,44 @@ class vmmConsolePages(vmmGObjectUI): self.viewer.cleanup() self.viewer = None + self.fs_toolbar.destroy() + self.fs_toolbar = None + self.fs_drawer.destroy() + self.fs_drawer = None + ########################## # Initialization helpers # ########################## + def init_fs_toolbar(self): + scroll = self.window.get_widget("console-vnc-scroll") + pages = self.window.get_widget("console-pages") + pages.remove(scroll) + + self.fs_toolbar = gtk.Toolbar() + self.fs_toolbar.set_show_arrow(False) + self.fs_toolbar.set_no_show_all(True) + self.fs_toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ) + + # Exit fullscreen button + button = gtk.ToolButton(gtk.STOCK_LEAVE_FULLSCREEN) + util.tooltip_wrapper(button, _("Leave fullscreen")) + button.show() + self.fs_toolbar.add(button) + button.connect("clicked", self.leave_fullscreen) + + self.fs_drawer = AutoDrawer() + self.fs_drawer.set_active(False) + self.fs_drawer.set_over(self.fs_toolbar) + self.fs_drawer.set_under(scroll) + self.fs_drawer.set_offset(-1) + self.fs_drawer.set_fill(False) + self.fs_drawer.set_overlap_pixels(1) + self.fs_drawer.set_nooverlap_pixels(0) + self.fs_drawer.show_all() + + pages.add(self.fs_drawer) + def change_title(self, ignore1=None): title = self.vm.get_name() + " " + _("Virtual Machine") @@ -703,17 +744,28 @@ class vmmConsolePages(vmmGObjectUI): def toggle_fullscreen(self, src): do_fullscreen = src.get_active() + self._change_fullscreen(do_fullscreen) + def leave_fullscreen(self, ignore): + self._change_fullscreen(False) + + def _change_fullscreen(self, do_fullscreen): self.window.get_widget("control-fullscreen").set_active(do_fullscreen) if do_fullscreen: self.topwin.fullscreen() + self.fs_toolbar.show() + self.fs_drawer.set_active(True) self.window.get_widget("toolbar-box").hide() + self.window.get_widget("details-menubar").hide() else: + self.fs_toolbar.hide() + self.fs_drawer.set_active(False) self.topwin.unfullscreen() if self.window.get_widget("details-menu-view-toolbar").get_active(): self.window.get_widget("toolbar-box").show() + self.window.get_widget("details-menubar").show() self.update_scaling()