Files
clr-installer/gui/window.go
T
2019-05-28 13:47:21 -07:00

798 lines
23 KiB
Go

// Copyright © 2019 Intel Corporation
//
// SPDX-License-Identifier: GPL-3.0-only
package gui
import (
"strings"
"github.com/gotk3/gotk3/gtk"
"github.com/clearlinux/clr-installer/args"
"github.com/clearlinux/clr-installer/gui/common"
"github.com/clearlinux/clr-installer/gui/pages"
"github.com/clearlinux/clr-installer/log"
"github.com/clearlinux/clr-installer/model"
"github.com/clearlinux/clr-installer/storage"
"github.com/clearlinux/clr-installer/syscheck"
"github.com/clearlinux/clr-installer/utils"
)
const (
// WindowWidth specifies the default width of the window
WindowWidth int = 950
// WindowHeight specifies the default height of the window
WindowHeight int = 600
)
// PageConstructor is a typedef of the constructors for our pages
type PageConstructor func(controller pages.Controller, model *model.SystemInstall) (pages.Page, error)
// Window provides management of the underlying GtkWindow and
// associated windows to provide a level of OOP abstraction.
type Window struct {
handle *gtk.Window // Abstract the underlying GtkWindow
mainLayout *gtk.Box // Content layout (horizontal)
banner *Banner // Banner
contentLayout *gtk.Box // Content Layout
rootStack *gtk.Stack // Root-level stack
model *model.SystemInstall // model
options args.Args // installer args
rootDir string // root directory
// Menu
menu struct {
switcher *Switcher // Allow switching between main menu
stack *gtk.Stack // Menu switching
screens map[bool]*ContentView // Mapping to content views
welcomePage pages.Page // Pointer to the welcome page
currentPage pages.Page // Pointer to the currently open page
installPage pages.Page // Pointer to the installer page
}
// Buttons
buttons struct {
stack *gtk.Stack // Storage for buttons
boxWelcome *gtk.Box // Storage for welcome buttons
boxPrimary *gtk.Box // Storage for primary buttons
boxSecondary *gtk.Box // Storage for secondary buttons
// Welcome buttons
next *gtk.Button // Launch primary view
exit *gtk.Button // Exit installer
// Primary buttons
install *gtk.Button // Install Clear Linux
quit *gtk.Button // Exit installer
back *gtk.Button // Back to welcome page
// Secondary buttons
confirm *gtk.Button // Confirm changes
cancel *gtk.Button // Cancel changes
}
didInit bool // Whether initialized the view animation
pages map[int]gtk.IWidget // Mapping to each root page
scanInfo pages.ScanInfo // Information related to scanning the media
}
// CreateHeaderBar creates invisible header bar
func (window *Window) CreateHeaderBar() error {
box, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
if err != nil {
return err
}
st, err := box.GetStyleContext()
if err != nil {
return err
}
window.handle.SetTitlebar(box)
st.RemoveClass("titlebar")
st.RemoveClass("headerbar")
st.RemoveClass("header")
st.AddClass("invisible-titlebar")
return nil
}
// NewWindow creates a new instance of the welcome page
func NewWindow(model *model.SystemInstall, rootDir string, options args.Args) (*Window, error) {
var err error
// Create basic window
window := &Window{
didInit: false,
pages: make(map[int]gtk.IWidget),
model: model,
rootDir: rootDir,
options: options,
}
// Default Icon the application
gtk.WindowSetDefaultIconName("system-software-install")
// Construct main window
window.handle, err = gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
if err != nil {
return nil, err
}
window.handle.SetPosition(gtk.WIN_POS_CENTER)
window.handle.SetDefaultSize(WindowWidth, WindowHeight)
window.handle.SetResizable(false)
// Create invisible header bar
if err = window.CreateHeaderBar(); err != nil {
return nil, err
}
// Set up the content layout within main layout
window.mainLayout, err = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
if err != nil {
return nil, err
}
window.handle.Add(window.mainLayout)
// Set locale
utils.SetLocale(model.Language.Code)
// Create welcome page
window, err = window.createWelcomePage()
if err != nil {
return nil, err
}
window.scanInfo.Channel = make(chan bool)
go func() {
log.Debug("Scanning media")
window.scanInfo.Media, err = storage.RescanBlockDevices(window.model.TargetMedias)
if err != nil {
log.Warning("Error scanning media %s", err.Error())
}
window.scanInfo.Channel <- true
}()
return window, nil
}
// createWelcomePage creates the welcome page
func (window *Window) createWelcomePage() (*Window, error) {
var err error
// Create banner and add to main layout
if window.banner, err = NewBanner(); err != nil {
return nil, err
}
window.mainLayout.PackStart(window.banner.GetRootWidget(), false, false, 0)
// Set up content layout and add to main layout
window.contentLayout, err = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
if err != nil {
return nil, err
}
window.mainLayout.PackStart(window.contentLayout, true, true, 0)
// Set up the root stack and add to content layout
window.rootStack, err = gtk.StackNew()
window.rootStack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
if err != nil {
return nil, err
}
window.contentLayout.PackStart(window.rootStack, true, true, 0)
// Set up the menu stack and add to root stack
window.menu.stack, err = gtk.StackNew()
if err != nil {
return nil, err
}
window.menu.stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT)
window.rootStack.AddTitled(window.menu.stack, "menu", "Menu")
// Create the welcome page
pageCreators := []PageConstructor{
pages.NewLanguagePage,
}
for _, f := range pageCreators {
page, err := f(window, window.model)
if err != nil {
return nil, err
}
if err = window.AddPage(page); err != nil {
return nil, err
}
}
// TODO: Remove this temporary code after development phase
_, err = window.handle.Connect("destroy", func() {
gtk.MainQuit()
})
if err != nil {
return nil, err
}
// Create footer area now
if err = window.CreateFooter(window.contentLayout); err != nil {
return nil, err
}
window.handle.ShowAll()
window.ActivatePage(window.menu.welcomePage)
return window, nil
}
// createMenuPages creates the menu pages
func (window *Window) createMenuPages() (*Window, error) {
var err error
window.banner.labelText.SetMarkup(GetWelcomeMessage())
window.menu.screens = make(map[bool]*ContentView)
// Set up the stack switcher
window.menu.switcher, err = NewSwitcher(nil)
if err != nil {
return nil, err
}
window.menu.switcher.SetStack(window.menu.stack)
if err = window.InitScreens(); err != nil {
return nil, err
}
window.contentLayout.Remove(window.rootStack)
window.contentLayout.PackStart(window.menu.switcher.GetRootWidget(), false, false, 0)
window.contentLayout.PackStart(window.rootStack, true, true, 0)
// Create footer area
if err = window.UpdateFooter(window.contentLayout); err != nil {
return nil, err
}
// Our pages
pageCreators := []PageConstructor{
// required
pages.NewTimezonePage,
pages.NewKeyboardPage,
pages.NewDiskConfigPage,
pages.NewUserAddPage,
pages.NewTelemetryPage,
// advanced
pages.NewBundlePage,
pages.NewHostnamePage,
// always last
pages.NewInstallPage,
}
// Create all pages
for _, f := range pageCreators {
page, err := f(window, window.model)
if err != nil {
return nil, err
}
if err = window.AddPage(page); err != nil {
return nil, err
}
}
// Show the whole window now
window.handle.ShowAll()
// Show the menu view
window.ShowMenuView()
return window, nil
}
// ShowMenuView displays the menu view
func (window *Window) ShowMenuView() {
done := window.menu.screens[ContentViewRequired].IsDone()
window.buttons.install.SetSensitive(done)
window.banner.Show()
window.menu.switcher.Show()
window.rootStack.SetVisibleChildName("menu")
window.menu.stack.SetVisibleChildName("required")
window.buttons.stack.SetVisibleChildName("primary")
}
// InitScreens initializes the switcher screens
func (window *Window) InitScreens() error {
var err error
// Set up required screen
if window.menu.screens[true], err = NewContentView(window); err != nil {
return err
}
window.menu.stack.AddNamed(window.menu.screens[ContentViewRequired].GetRootWidget(), "required")
// Set up non required screen
if window.menu.screens[false], err = NewContentView(window); err != nil {
return err
}
window.menu.stack.AddNamed(window.menu.screens[ContentViewAdvanced].GetRootWidget(), "advanced")
return nil
}
// AddPage adds the page to the relevant switcher screen
func (window *Window) AddPage(page pages.Page) error {
var (
err error
id int
)
id = page.GetID()
if id == pages.PageIDWelcome {
window.menu.welcomePage = page
} else if id == pages.PageIDInstall {
window.menu.installPage = page
} else { // Add to the required or advanced (optional) screen
err := window.menu.screens[page.IsRequired()].AddPage(page)
if err != nil {
return err
}
}
// Create a header page wrap for the page
header, err := PageHeaderNew(page)
if err != nil {
return err
}
root := header.GetRootWidget()
// Make available via root stack
window.pages[id] = root
window.rootStack.AddNamed(root, "page:"+string(id))
return nil
}
// createNavButton creates specialised navigation button
func createNavButton(label, style string) (*gtk.Button, error) {
button, err := gtk.ButtonNewWithLabel(label)
if err != nil {
return nil, err
}
sc, err := button.GetStyleContext()
if err != nil {
return nil, err
}
// Not able to set size using CSS
button.SetSizeRequest(100, 36)
sc.AddClass(style)
return button, nil
}
// CreateFooter creates the navigation footer area
func (window *Window) CreateFooter(store *gtk.Box) error {
var err error
// Create stack for buttons
if window.buttons.stack, err = gtk.StackNew(); err != nil {
return err
}
// Set alignment up
window.buttons.stack.SetMarginTop(4)
window.buttons.stack.SetMarginBottom(6)
window.buttons.stack.SetMarginEnd(18)
window.buttons.stack.SetHAlign(gtk.ALIGN_FILL)
window.buttons.stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
store.PackEnd(window.buttons.stack, false, false, 0)
// Create box for welcome page buttons
if window.buttons.boxWelcome, err = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0); err != nil {
return err
}
// Install button
if window.buttons.next, err = createNavButton(utils.Locale.Get("NEXT"), "button-confirm"); err != nil {
return err
}
if _, err = window.buttons.next.Connect("clicked", func() { window.launchMenuView() }); err != nil {
return err
}
// Exit button
if window.buttons.exit, err = createNavButton(utils.Locale.Get("EXIT"), "button-cancel"); err != nil {
return err
}
if _, err = window.buttons.exit.Connect("clicked", func() { gtk.MainQuit() }); err != nil {
return err
}
// Pack the buttons
window.buttons.boxWelcome.PackEnd(window.buttons.next, false, false, 4)
window.buttons.boxWelcome.PackEnd(window.buttons.exit, false, false, 4)
// Add the boxes
window.buttons.stack.AddNamed(window.buttons.boxWelcome, "welcome")
return nil
}
// UpdateFooter updates the navigation footer area
func (window *Window) UpdateFooter(store *gtk.Box) error {
var err error
// Install button
if window.buttons.install, err = createNavButton(utils.Locale.Get("INSTALL"), "button-confirm"); err != nil {
return err
}
if _, err = window.buttons.install.Connect("clicked", func() { window.confirmInstall() }); err != nil {
return err
}
// Exit button
if window.buttons.quit, err = createNavButton(utils.Locale.Get("EXIT"), "button-cancel"); err != nil {
return err
}
if _, err = window.buttons.quit.Connect("clicked", func() { gtk.MainQuit() }); err != nil {
return err
}
// Back button
if window.buttons.back, err = createNavButton(utils.Locale.Get("CHANGE LANGUAGE"), "button-cancel"); err != nil {
return err
}
if _, err = window.buttons.back.Connect("clicked", func() { window.launchWelcomeView() }); err != nil {
return err
}
width, _ := window.handle.GetSize() // get current size
marginEnd := width * 35 / 100
window.buttons.back.SetMarginEnd(marginEnd) // TODO: MarginStart would be ideal but does not work
// Confirm button
if window.buttons.confirm, err = createNavButton(utils.Locale.Get("CONFIRM"), "button-confirm"); err != nil {
return err
}
if _, err = window.buttons.confirm.Connect("clicked", func() { window.pageClosed(true) }); err != nil {
return err
}
// Cancel button
if window.buttons.cancel, err = createNavButton(utils.Locale.Get("CANCEL"), "button-cancel"); err != nil {
return err
}
if _, err = window.buttons.cancel.Connect("clicked", func() { window.pageClosed(false) }); err != nil {
return err
}
// Create box for primary buttons
if window.buttons.boxPrimary, err = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0); err != nil {
return err
}
window.buttons.boxPrimary.PackEnd(window.buttons.install, false, false, 4)
window.buttons.boxPrimary.PackEnd(window.buttons.quit, false, false, 4)
window.buttons.boxPrimary.PackEnd(window.buttons.back, false, false, 4)
// Create box for secondary buttons
if window.buttons.boxSecondary, err = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0); err != nil {
return err
}
window.buttons.boxSecondary.PackEnd(window.buttons.confirm, false, false, 4)
window.buttons.boxSecondary.PackEnd(window.buttons.cancel, false, false, 4)
// Add the boxes
window.buttons.stack.AddNamed(window.buttons.boxPrimary, "primary")
window.buttons.stack.AddNamed(window.buttons.boxSecondary, "secondary")
return nil
}
// pageClosed handles closure of a page.
func (window *Window) pageClosed(applied bool) {
// If applied, tell page to stash in model
// otherwise, reset from existing model
if applied {
window.menu.currentPage.StoreChanges()
} else {
window.menu.currentPage.ResetChanges()
}
// Let installation continue if possible
done := window.menu.screens[ContentViewRequired].IsDone()
window.buttons.install.SetSensitive(done)
// Reset the SummaryWidget for responsible controller
window.menu.screens[window.menu.currentPage.IsRequired()].UpdateView(window.menu.currentPage)
// Reset currentPage
window.menu.currentPage = nil
// Switch UI back to primary view
window.rootStack.SetVisibleChildName("menu")
window.banner.Show()
window.menu.switcher.Show()
window.buttons.stack.SetVisibleChildName("primary")
}
// ActivatePage displays the page
func (window *Window) ActivatePage(page pages.Page) {
window.menu.currentPage = page
id := page.GetID()
if id == pages.PageIDWelcome { // Welcome Page
window.banner.Show()
window.buttons.stack.SetVisibleChildName("welcome")
} else if id == pages.PageIDInstall { // Install Page
window.menu.switcher.Hide()
window.banner.Show()
window.banner.labelText.SetMarkup(GetThankYouMessage())
window.buttons.stack.SetVisibleChildName("primary")
window.buttons.install.Hide()
window.buttons.back.Hide()
sc, err := window.buttons.quit.GetStyleContext()
sc.RemoveClass("button-cancel")
if err != nil {
log.Warning("Error getting style context: ", err) // Just log trivial error
} else {
sc.AddClass("button-confirm")
}
window.SetButtonState(pages.ButtonQuit, false)
} else {
window.menu.switcher.Hide()
window.banner.Hide()
window.buttons.stack.SetVisibleChildName("secondary")
window.SetButtonState(pages.ButtonConfirm, false)
}
// Allow page to take control now
page.ResetChanges()
// Set the root stack to show the new page
window.rootStack.SetVisibleChild(window.pages[id])
}
// SetButtonState is called by the pages to enable/disable certain buttons.
func (window *Window) SetButtonState(flags pages.Button, enabled bool) {
if window.menu.currentPage.GetID() != pages.PageIDWelcome {
if flags&pages.ButtonCancel == pages.ButtonCancel {
window.buttons.cancel.SetSensitive(enabled)
}
if flags&pages.ButtonConfirm == pages.ButtonConfirm {
window.buttons.confirm.SetSensitive(enabled)
}
if flags&pages.ButtonQuit == pages.ButtonQuit {
window.buttons.quit.SetSensitive(enabled)
}
if flags&pages.ButtonBack == pages.ButtonBack {
window.buttons.back.SetSensitive(enabled)
}
} else {
if flags&pages.ButtonNext == pages.ButtonNext {
window.buttons.next.SetSensitive(enabled)
}
if flags&pages.ButtonExit == pages.ButtonExit {
window.buttons.exit.SetSensitive(enabled)
}
}
}
// SetButtonVisible is called by the pages to view/hide certain buttons.
func (window *Window) SetButtonVisible(flags pages.Button, visible bool) {
if window.menu.currentPage.GetID() != pages.PageIDWelcome {
if flags&pages.ButtonCancel == pages.ButtonCancel {
window.buttons.cancel.SetVisible(visible)
}
if flags&pages.ButtonConfirm == pages.ButtonConfirm {
window.buttons.confirm.SetVisible(visible)
}
if flags&pages.ButtonQuit == pages.ButtonQuit {
window.buttons.quit.SetVisible(visible)
}
if flags&pages.ButtonBack == pages.ButtonBack {
window.buttons.back.SetVisible(visible)
}
} else {
if flags&pages.ButtonNext == pages.ButtonNext {
window.buttons.next.SetVisible(visible)
}
if flags&pages.ButtonExit == pages.ButtonExit {
window.buttons.exit.SetVisible(visible)
}
}
}
// launchWelcomeView launches the welcome view
func (window *Window) launchWelcomeView() {
window.mainLayout.Remove(window.contentLayout)
window.mainLayout.Remove(window.banner.GetRootWidget())
if _, err := window.createWelcomePage(); err != nil {
log.ErrorError(err) // TODO: Handle error
}
}
// launchMenuView launches the menu view
func (window *Window) launchMenuView() {
window.menu.currentPage.StoreChanges()
// If syscheck fails, launch a dialog box and force the user to exit
if retErr := syscheck.RunSystemCheck(true); retErr != nil {
contentBox, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 10)
contentBox.SetHAlign(gtk.ALIGN_FILL)
contentBox.SetMarginBottom(common.TopBottomMargin)
if err != nil {
log.Warning("Error creating box")
return
}
st, err := contentBox.GetStyleContext()
if err != nil {
log.Warning("Error getting style context")
return
}
// Style the dialog
st.AddClass("dialog-warning")
icon, err := gtk.ImageNewFromIconName("dialog-error-symbolic", gtk.ICON_SIZE_DIALOG)
if err != nil {
log.Warning("gtk.ImageNewFromIconName failed for icon dialog-error-symbolic")
return
}
icon.SetMarginEnd(12)
icon.SetHAlign(gtk.ALIGN_START)
icon.SetVAlign(gtk.ALIGN_START)
contentBox.PackStart(icon, false, true, 0)
label, err := gtk.LabelNew(utils.Locale.Get("System failed to pass pre-install checks.") + "\n\n" + retErr.Error())
if err != nil {
log.Warning("Error creating label")
return
}
label.SetUseMarkup(true)
label.SetHAlign(gtk.ALIGN_END)
contentBox.PackStart(label, false, true, 0)
dialog, err := common.CreateDialogOneButton(contentBox, utils.Locale.Get("System Check Failed"), utils.Locale.Get("EXIT"), "button-cancel")
if err != nil {
log.Warning("Error creating dialog")
return
}
_, err = dialog.Connect("response", func() { gtk.MainQuit() })
if err != nil {
log.Warning("Error connecting to dialog")
}
dialog.ShowAll()
dialog.Run()
} else {
if _, err := window.createMenuPages(); err != nil {
log.ErrorError(err) // TODO: Handle error
}
}
}
// confirmInstall prompts the user for confirmation before installing
func (window *Window) confirmInstall() {
var text, primaryText, secondaryText string
var err error
if window.model.InstallSelected.EraseDisk {
primaryText = utils.Locale.Get(storage.DestructiveWarning)
} else if window.model.InstallSelected.DataLoss {
primaryText = utils.Locale.Get(storage.DataLossWarning)
} else if window.model.InstallSelected.WholeDisk {
primaryText = utils.Locale.Get(storage.SafeWholeWarning)
} else {
primaryText = utils.Locale.Get(storage.SafePartialWarning)
}
// Build the string with the media being modified
targets := []string{}
if len(window.model.TargetMedias) == 0 {
targets = append(targets, utils.Locale.Get("None"))
} else {
for _, media := range window.model.TargetMedias {
targets = append(targets, media.GetDeviceFile())
}
}
secondaryText = utils.Locale.Get("Target Media") + ": " + strings.Join(targets, ", ")
title := utils.Locale.Get(storage.ConfirmInstallation)
text = primaryText + "\n" + "<small>" + secondaryText + "</small>"
contentBox, err := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
contentBox.SetHAlign(gtk.ALIGN_FILL)
contentBox.SetMarginBottom(common.TopBottomMargin)
if err != nil {
log.Warning("Error creating box")
return
}
label, err := gtk.LabelNew(text)
if err != nil {
log.Warning("Error creating label")
return
}
label.SetUseMarkup(true)
label.SetHAlign(gtk.ALIGN_START)
contentBox.PackStart(label, false, true, 0)
dialog, err := common.CreateDialogOkCancel(contentBox, title, utils.Locale.Get("CONFIRM"), utils.Locale.Get("CANCEL"))
if err != nil {
log.Warning("Error creating dialog")
return
}
_, err = dialog.Connect("response", window.dialogResponse)
if err != nil {
log.Warning("Error connecting to dialog")
}
dialog.ShowAll()
dialog.Run()
}
// dialogResponse handles the response from the dialog message
func (window *Window) dialogResponse(msgDialog *gtk.Dialog, responseType gtk.ResponseType) {
if responseType == gtk.RESPONSE_OK {
window.ActivatePage(window.menu.installPage)
}
msgDialog.Destroy()
}
// GetOptions returns the options given to the window
func (window *Window) GetOptions() args.Args {
return window.options
}
// GetRootDir returns the root dir
func (window *Window) GetRootDir() string {
return window.rootDir
}
// GetScanChannel is the getter for ScanInfo Channel
func (window *Window) GetScanChannel() chan bool {
return window.scanInfo.Channel
}
// GetScanDone is the getter for ScanInfo Done
func (window *Window) GetScanDone() bool {
return window.scanInfo.Done
}
// SetScanDone is the setter for ScanInfo Done
func (window *Window) SetScanDone(done bool) {
window.scanInfo.Done = done
}
// GetScanMedia is the getter for ScanInfo Media
func (window *Window) GetScanMedia() []*storage.BlockDevice {
return window.scanInfo.Media
}
// SetScanMedia is the setter for ScanInfo Media
func (window *Window) SetScanMedia(scannedMedia []*storage.BlockDevice) {
window.scanInfo.Media = scannedMedia
}
// GetWelcomeMessage gets the welcome message
func GetWelcomeMessage() string {
text := "<span font-size='xx-large'>" + utils.Locale.Get("Welcome to Clear Linux* OS Desktop Installation") + "</span>"
if model.Version != model.DemoVersion {
text += "\n\n<small>" + utils.Locale.Get("VERSION %s", model.Version) + "</small>"
}
return text
}
// GetThankYouMessage gets the thank you message
func GetThankYouMessage() string {
text := "<span font-size='xx-large'>" + utils.Locale.Get("Thank you for choosing Clear Linux* OS") + "</span>"
if model.Version != model.DemoVersion {
text += "\n\n<small>" + utils.Locale.Get("VERSION %s", model.Version) + "</small>"
}
return text
}