You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
qubes-installer-qubes-os/firstboot/interface.py

476 lines
19 KiB

#
# Chris Lumens <clumens@redhat.com>
#
# Copyright 2007 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use, modify,
# copy, or redistribute it subject to the terms and conditions of the GNU
# General Public License v.2. This program is distributed in the hope that it
# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
# implied warranties 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. Any Red Hat
# trademarks that are incorporated in the source code or documentation are not
# subject to the GNU General Public License and may only be used or replicated
# with the express permission of Red Hat, Inc.
#
import gtk
import logging, os
from firstboot.config import *
from firstboot.constants import *
from firstboot.functions import *
from firstboot.moduleset import *
import gettext
_ = lambda x: gettext.ldgettext("firstboot", x)
class Control:
def __init__(self):
self.currentPage = 0
self.history = []
self.moduleList = []
class Interface:
def __init__(self, autoscreenshot=False, moduleList=[], testing=False):
"""Create a new Interface instance. Instance attributes:
autoscreenshot -- If true, a screenshot will be taken after every
module is run.
moduleList -- A list of references to all the loaded modules.
This is not typically used, but is required to
make the interface work.
testing -- Is firstboot running under testing mode, where no
changes will be made to the disk?
"""
self._screenshotDir = "/root/firstboot-screenshots"
self._screenshotIndex = 0
# This is needed for ModuleSet to work. We maintain a stack of control
# states, creating a new state for navigation when we enter into a
# ModuleSet, then popping it off when we leave.
self._controlStack = [Control()]
self._control = self._controlStack[0]
self.moduleList = moduleList
self._x_size = gtk.gdk.screen_width()
self._y_size = gtk.gdk.screen_height()
self.autoscreenshot = autoscreenshot
self.testing = testing
def _setModuleList(self, moduleList):
self._control.moduleList = moduleList
moduleList = property(lambda s: s._control.moduleList,
lambda s, v: s._setModuleList(v))
def _backClicked(self, *args):
# If there's nowhere to go back to, we're either at the first page in
# the module set or something went wrong (Back is enabled on the very
# first page). In the former case, revert back to the enclosing
# control state for what page to display next.
if len(self._control.history) == 0:
if len(self._controlStack) == 1:
logging.error(_("Attempted to go back, but history is empty."))
return
else:
self._controlStack.pop()
self._control = self._controlStack[-1]
# If we were previously on the last page, we need to set the Next
# button's label back to normal.
if self.nextButton.get_label() == _("_Finish"):
self.nextButton.set_label("gtk-go-forward")
self._control.currentPage = self._control.history.pop()
self.moveToPage(pageNum=self._control.currentPage)
def _keyRelease(self, window, event):
if event.keyval == gtk.keysyms.F12:
self.nextButton.clicked()
elif event.keyval == gtk.keysyms.F11:
self.backButton.clicked()
elif event.keyval == gtk.keysyms.Print and event.state & gtk.gdk.SHIFT_MASK:
self.takeScreenshot()
def _nextClicked(self, *args):
if self.autoscreenshot:
self.takeScreenshot()
self.advance()
def _setBackSensitivity(self):
self.backButton.set_sensitive(not(self._control.currentPage == 0 and len(self._controlStack) == 1))
def _setPointer(self, number):
# The sidebar pointer only works in terms of the top-level module list
# as we don't display anything on the side for a ModuleSet and making
# the pointer move around then would be confusing.
for i in range(len(self.moduleList)):
(alignment, label) = self.sidebar.get_children()[i].get_children()
pix = alignment.get_children()[0]
if i == number:
f = "%s/%s" % (config.themeDir, "pointer-white.png")
if os.access(f, os.F_OK):
pix.set_from_file(f)
else:
pix.set_from_file("%s/%s" % (config.defaultThemeDir, "pointer-white.png"))
else:
f = "%s/%s" % (config.themeDir, "pointer-blank.png")
if os.access(f, os.F_OK):
pix.set_from_file(f)
else:
pix.set_from_file("%s/%s" % (config.defaultThemeDir, "pointer-blank.png"))
def _sidebarExposed(self, eb, event):
pixbuf = self.sidebarBg.scale_simple(int(self._y_size * self.aspectRatio),
self._y_size, gtk.gdk.INTERP_BILINEAR)
cairo_context = eb.window.cairo_create()
cairo_context.set_source_pixbuf(pixbuf, 0, self._y_size-pixbuf.get_height())
cairo_context.paint()
return False
def advance(self):
"""Call the apply method on the currently displayed page, add it to
the history, and move to the next page. It is not safe to call this
method from within firstboot modules.
"""
module = self.moduleList[self._control.currentPage]
# This could fail, in which case the exception will propagate up to the
# interface which will know the proper way to handle it.
result = module.apply(self, self.testing)
# If something went wrong in the module, don't advance.
if result == RESULT_FAILURE:
return
# If the apply action from the current page jumped us to another page,
# don't try to jump again.
if result != RESULT_JUMP:
self.moveToPage(pageNum=self._control.currentPage+1)
# if we are on the last page overall (not just the last page of a
# ModuleSet), it's time to kill the interface.
if len(self._controlStack) == 1:
if self._control.currentPage == len(self.moduleList)-1:
self.nextButton.set_label(_("_Finish"))
elif self._control.currentPage == len(self.moduleList):
self.checkReboot()
self.destroy()
def checkReboot(self):
"""Check to see if any module requires a reboot for changes to take
effect, displaying a dialog if so. This method immediately reboots
the system.
"""
needReboot = False
for module in self.moduleList:
if module.needsReboot():
needReboot = True
break
if not needReboot or self.testing:
return
dlg = gtk.MessageDialog(None, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
_("The system must now reboot for some of your selections to take effect."))
dlg.set_position(gtk.WIN_POS_CENTER)
dlg.show_all()
dlg.run()
dlg.destroy()
os.system("/sbin/reboot")
def createMainWindow(self):
"""Create and initialize the main window. This includes switching to
fullscreen mode if necessary, adding buttons, displaying artwork,
and other functions. This method should also create the UI elements
that make up the sidebar. This method returns a gtk.Window.
"""
# Create the initial window and a vbox to fill it with.
self.win = gtk.Window()
self.win.set_position(gtk.WIN_POS_CENTER)
self.win.set_decorated(False)
# we don't set border width here so that the sidebar will meet
# the edge of the screen
# Create a box that will hold all other widgets.
self.mainHBox = gtk.HBox(False, 10)
# Create the sidebar box.
self.sidebar = gtk.VBox()
self.sidebar.set_border_width(24)
# Load this background now so we can figure out how big to make
# the left side.
try:
self.sidebarBg = loadPixbuf("%s/%s" % (config.themeDir, "firstboot-left.png"))
except:
self.sidebarBg = loadPixbuf("%s/%s" % (config.defaultThemeDir, "firstboot-left.png"))
self.aspectRatio = (1.0 * self.sidebarBg.get_width()) / (1.0 * self.sidebarBg.get_height())
# leftEventBox exists only so we have somewhere to paint an image.
self.leftEventBox = gtk.EventBox()
self.leftEventBox.add(self.sidebar)
self.sidebar.connect("expose-event", self._sidebarExposed)
# Create the box for the right side of the screen. This holds the
# display for the current module and the button box.
self.rightBox = gtk.VBox()
self.rightBox.set_border_width(24)
leftWidth = int(self._y_size * self.aspectRatio)
self.leftEventBox.set_size_request(leftWidth, self._y_size)
self.win.fullscreen()
# Create a button box to handle navigation.
self.buttonBox = gtk.HButtonBox()
self.buttonBox.set_layout(gtk.BUTTONBOX_END)
self.buttonBox.set_spacing(10)
self.buttonBox.set_border_width(10)
# Create the Back button, marking it insensitive by default since we
# start at the first page.
self.backButton = gtk.Button(use_underline=True, stock="gtk-go-back",
label=_("_Back"))
self._setBackSensitivity()
self.backButton.connect("clicked", self._backClicked)
self.buttonBox.pack_start(self.backButton)
# Create the Forward button.
self.nextButton = gtk.Button(use_underline=True, stock="gtk-go-forward",
label=_("_Forward"))
self.nextButton.connect("clicked", self._nextClicked)
self.buttonBox.pack_start(self.nextButton)
# Add the widgets into the right side.
self.rightBox.pack_end(self.buttonBox, expand=False)
# Add the widgets into the main hbox widget.
self.mainHBox.pack_start(self.leftEventBox, expand=False, fill=False)
self.mainHBox.pack_start(self.rightBox, expand=True, fill=True)
self.win.add(self.mainHBox)
self.win.connect("destroy", self.destroy)
self.win.connect("key-release-event", self._keyRelease)
return self.win
def createScreens(self):
"""Call the createScreen method on all loaded modules. This loads the
UI elements for each page and stuffs the rendered page into a UI
wrapper containing the module's title and icon.
"""
for module in self.moduleList:
try:
module.createScreen()
if isinstance(module, Module) and module.vbox is None:
logging.error(_("Module %s did not set up its UI, removing.") % module.title)
self.moduleList.remove(module)
module.renderModule(self)
except:
self.moduleList.remove(module)
continue
def createSidebar(self):
"""Add the sidebarTitle from every module to the sidebar."""
for module in self.moduleList:
hbox = gtk.HBox(False, 5)
label = gtk.Label("")
label.set_markup("<span foreground='#FFFFFF'><b>%s</b></span>" % _(module.sidebarTitle))
label.set_alignment(0.0, 0.5)
# Wrap the sidebar title if it's too long.
label.set_line_wrap(True)
(w, h) = self.leftEventBox.get_size_request()
label.set_size_request((int)(w*0.7), -1)
# Make sure the arrow is at the top of any wrapped line.
alignment = gtk.Alignment(yalign=0.2)
try:
alignment.add(loadToImage("%s/%s" % (config.themeDir, "pointer-blank.png")))
except:
alignment.add(loadToImage("%s/%s" % (config.defaultThemeDir, "pointer-blank.png")))
hbox.pack_start(alignment, False)
hbox.pack_end(label, True)
self.sidebar.pack_start(hbox, False, True, 3)
# Initialize sidebar pointer
self._setPointer(0)
def destroy(self, *args):
"""Destroy the UI, but do not take any other action to quit firstboot."""
try:
gtk.main_quit()
except RuntimeError:
os._exit(1)
def displayModule(self):
"""Display the current module on the main portion of the screen. This
method should take into account that a module might be displayed
already and should be removed first.
"""
# Remove any module that was already being displayed.
if len(self.rightBox.get_children()) == 2:
oldModule = self.rightBox.get_children()[0]
self.rightBox.remove(oldModule)
# Initialize the module's UI (sync up with the state of some file on
# disk, or whatever) and then pack it into the right side of the
# screen for display.
currentModule = self.moduleList[self._control.currentPage]
currentModule.initializeUI()
self.rightBox.pack_start(currentModule.vbox)
currentModule.focus()
self.win.show_all()
def moveToPage(self, moduleTitle=None, pageNum=None):
"""Move to and display the page given either by title or page number.
This method raises SystemError if neither is provided, or if no
page is found. It is safe to call this method from within the apply
method of modules, unlike advance().
"""
if moduleTitle is None and pageNum is None:
logging.error(_("moveToPage must be given a module title or page number."))
raise SystemError, _("moveToPage must be given a module title or page number.")
# If we were given a moduleTitle, look up the corresponding pageNum.
# Everything else in firstboot is indexed by number.
if moduleTitle is not None:
pageNum = self.titleToPageNum(moduleTitle, self.moduleList)
# If we're at the end of a ModuleSet's module list, pop off the control
# structure and set up to move to the next page after the set. If
# we're already at the top level, it's easy.
if pageNum == len(self.moduleList):
if len(self._controlStack) > 1:
oldFrame = self._controlStack.pop()
self._control = self._controlStack[-1]
# Put the ModuleSet's history into the object so we can keep
# the history should we go back into the set later on. This
# is kind of a hack.
oldPage = self.moduleList[self._control.currentPage]
if isinstance(oldPage, ModuleSet):
# Add the last page in the ModuleSet, since it will
# otherwise be forgotten. We don't append to the history
# until later in this method.
oldPage._history = oldFrame.history + [oldFrame.currentPage]
self.moveToPage(pageNum=self._control.currentPage+1)
return
else:
self._control.currentPage += 1
return
# Only add the current page to the history if we are moving forward.
# Adding it when we're going backwards traps us at the first page of
# a ModuleSet.
if pageNum > self._control.currentPage and not self._control.currentPage in self._control.history:
self._control.history.append(self._control.currentPage)
# Set this regardless so we know where we are on the way back out of
# a ModuleSet.
self._control.currentPage = pageNum
if isinstance(self.moduleList[pageNum], ModuleSet):
newControl = Control()
newControl.currentPage = 0
newControl.moduleList = self.moduleList[pageNum].moduleList
# If we are stepping back into a ModuleSet from somewhere after it, try
# to load any saved history. This preserves the UI flow that you'd
# expect.
if hasattr(self.moduleList[pageNum], "_history"):
newControl.history = self.moduleList[pageNum]._history
else:
newControl.history = []
self._controlStack.append(newControl)
self._control = newControl
if len(newControl.history) > 0:
self.moveToPage(pageNum=self._control.history.pop())
return
else:
self._setBackSensitivity()
self._setPointer(self._controlStack[0].currentPage)
self.displayModule()
def run(self):
"""Given an interface that has had all its UI components loaded and
initialized, run the interface. Because this method must call into
the UI toolkit to handle events and such, it is assumed that this
method does not exit until the UI is unloaded. From this point on,
all interaction must take place in callbacks.
"""
self.displayModule()
self.win.present()
self.nextButton.grab_focus()
gtk.main()
def takeScreenshot(self):
"""Take a screenshot."""
if not os.access(self._screenshotDir, os.R_OK):
try:
os.mkdir(self._screenshotDir)
except:
logging.error(_("Unable to create the screenshot dir; skipping."))
return
screenshot = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8,
self._x_size, self._y_size)
screenshot.get_from_drawable(gtk.gdk.get_default_root_window(),
gtk.gdk.colormap_get_system(),
0, 0, 0, 0, self._x_size, self._y_size)
if not screenshot:
return
while True:
sname = "screenshot-%04d.png" % self._screenshotIndex
if not os.access("%s/%s" % (self._screenshotDir, sname), os.R_OK):
break
self._screenshotIndex += 1
screenshot.save("%s/%s" % (self._screenshotDir, sname), "png")
self._screenshotIndex += 1
def titleToPageNum(self, moduleTitle, moduleList):
"""Lookup the given title in the given module list. Returns the page
number on success. This only works on the given moduleList, so
for a ModuleSet it would only find the page if it exists in the
set.
"""
if moduleTitle is None:
return None
pageNum = 0
while True:
try:
if moduleList[pageNum].title == moduleTitle:
break
pageNum += 1
except IndexError:
logging.error(_("No module exists with the title %s.") % moduleTitle)
raise SystemError, _("No module exists with the title %s.") % moduleTitle
return pageNum