Exemple #1
0
    def __init__(self, app, serverName='server', onCancel=None):
        super(ConnectingScreen, self).__init__(app)
        colours = app.theme.colours

        self.text = TextElement(self.app,
                                'Connecting to %s...' % serverName,
                                self.app.screenManager.fonts.bigMenuFont,
                                ScaledLocation(512, 384, 'center'),
                                colour=colours.connectingColour)

        button = TextButton(
            self.app,
            Location(ScaledScreenAttachedPoint(ScaledSize(0, 300), 'center'),
                     'center'),
            'cancel',
            self.app.screenManager.fonts.bigMenuFont,
            colours.mainMenuColour,
            colours.white,
            onClick=onCancel)
        self.onCancel = button.onClick

        self.elements = [button, self.text]
    def __init__(self, app, onClose=None, onRestart=None):
        super(ThemeTab, self).__init__(app, 'Themes')

        self.onClose = Event()
        if onClose is not None:
            self.onClose.addListener(onClose)
        self.onRestart = Event()
        if onRestart is not None:
            self.onRestart.addListener(onRestart)

        font = self.app.screenManager.fonts.menuFont
        colours = self.app.theme.colours

        self.inactiveTheme = False
        self.originalTheme = app.theme.name

        # Static text
        self.staticText = [
            TextElement(self.app, 'theme information:', font,
                        ScaledLocation(960, 250, 'topright'),
                        colours.headingColour),
            TextElement(self.app, 'theme contents:', font,
                        ScaledLocation(960, 390, 'topright'),
                        colours.headingColour)
        ]

        # Dynamic text
        self.feedbackText = TextElement(
            self.app, 'Your current theme: %s' % (app.theme.name, ), font,
            ScaledLocation(512, 200, 'midtop'), colours.startButton)
        self.listHeaderText = TextElement(self.app, 'available themes:', font,
                                          ScaledLocation(70, 250),
                                          colours.headingColour)
        self.themeNameText = TextElement(self.app, 'Default Theme', font,
                                         ScaledLocation(960, 290, 'topright'),
                                         colours.startButton)
        self.themeAuthorText = TextElement(
            self.app, 'created by: Trosnoth Team', font,
            ScaledLocation(960, 330, 'topright'), colours.startButton)

        self.contents = []

        numContents = 4
        for yPos in range(430, 430 + numContents * 40 + 1, 40):
            self.contents.append(
                TextElement(self.app, '', font,
                            ScaledLocation(960, yPos, 'topright'),
                            colours.startButton))

        self.dynamicText = [
            self.feedbackText, self.listHeaderText, self.themeNameText,
            self.themeAuthorText
        ] + self.contents

        # Theme list
        self.themeList = ListBox(self.app, ScaledArea(70, 290, 400, 290), [],
                                 font, colours.listboxButtons)
        self.themeList.onValueChanged.addListener(self.updateSidebar)

        # Text buttons
        self.useThemeButton = button(app, 'use selected theme',
                                     self.applyTheme, (0, -125), 'midbottom')
        self.refreshButton = button(app, 'refresh', self.populateList,
                                    (-100, -75), 'midbottom')
        self.cancelButton = button(app, 'cancel', self.backToMain, (100, -75),
                                   'midbottom')
        self.restartButton = button(app, 'restart Trosnoth', self.restart,
                                    (0, -125), 'midbottom')

        self.buttons = [
            self.useThemeButton, self.refreshButton, self.cancelButton,
            self.restartButton
        ]

        # Combine the elements
        self.elements = self.staticText + self.dynamicText + self.buttons + [
            self.themeList
        ]

        self.contentTypes = {
            "sprites": "sprite",
            "blocks": "map block",
            "fonts": "font",
            "startupMenu": "backdrop"
        }

        # Populate the list of replays
        self.populateList()
Exemple #3
0
    def __init__(self, app, onClose=None):
        super(SoundSettingsTab, self).__init__(app, 'Sounds')

        self.onClose = Event()
        if onClose is not None:
            self.onClose.addListener(onClose)

        font = self.app.screenManager.fonts.bigMenuFont
        colours = app.theme.colours

        text = [
            TextElement(self.app, 'Music Volume', font,
                        ScaledLocation(400, 285, 'topright'),
                        colours.headingColour),
            TextElement(self.app, 'Enable Music', font,
                        ScaledLocation(400, 355, 'topright'),
                        colours.headingColour),
            TextElement(self.app, 'Sound Volume', font,
                        ScaledLocation(400, 425, 'topright'),
                        colours.headingColour),
            TextElement(self.app, 'Enable Sound', font,
                        ScaledLocation(400, 495, 'topright'),
                        colours.headingColour),
        ]

        initVolume = app.soundSettings.musicVolume
        musicVolumeLabel = TextElement(self.app, '%d' % (initVolume, ), font,
                                       ScaledLocation(870, 280, 'topleft'),
                                       colours.headingColour)

        self.musicVolumeSlider = Slider(self.app,
                                        ScaledArea(450, 280, 400, 40))
        onSlide = lambda volume: musicVolumeLabel.setText('%d' % volume)
        self.musicVolumeSlider.onSlide.addListener(onSlide)
        self.musicVolumeSlider.onValueChanged.addListener(onSlide)
        self.musicVolumeSlider.setVal(initVolume)

        self.musicBox = CheckBox(self.app,
                                 ScaledLocation(450, 360),
                                 text='',
                                 font=font,
                                 colour=(192, 192, 192),
                                 initValue=app.soundSettings.musicEnabled)

        initSndVolume = app.soundSettings.soundVolume
        soundVolumeLabel = TextElement(self.app,
                                       '%d' % (initSndVolume, ), font,
                                       ScaledLocation(870, 420, 'topleft'),
                                       colours.headingColour)

        self.soundVolumeSlider = Slider(self.app,
                                        ScaledArea(450, 420, 400, 40))
        onSlide = lambda volume: soundVolumeLabel.setText('%d' % volume)
        self.soundVolumeSlider.onSlide.addListener(onSlide)
        self.soundVolumeSlider.onValueChanged.addListener(onSlide)
        self.soundVolumeSlider.setVal(initSndVolume)

        self.soundBox = CheckBox(self.app,
                                 ScaledLocation(450, 500),
                                 text='',
                                 font=font,
                                 colour=(192, 192, 192),
                                 initValue=app.soundSettings.soundEnabled)

        self.buttons = [
            button(app,
                   'save',
                   self.saveSettings, (-100, -75),
                   'midbottom',
                   secondColour=app.theme.colours.white),
            button(app,
                   'cancel',
                   self.onClose.execute, (100, -75),
                   'midbottom',
                   secondColour=app.theme.colours.white)
        ]

        self.elements = text + [
            musicVolumeLabel, self.musicVolumeSlider, self.musicBox,
            soundVolumeLabel, self.soundVolumeSlider, self.soundBox
        ] + self.buttons
Exemple #4
0
    def __init__(self, app, controller, world):
        super(JoinGameDialog, self).__init__(app, ScaledSize(512, 314),
                                             'Join Game')
        self.result = None
        self.controller = controller
        self.selectedTeam = None

        fonts = self.app.screenManager.fonts
        self.nickBox = prompt.InputBox(
            self.app,
            Area(DialogBoxAttachedPoint(self, ScaledSize(0, 40), 'midtop'),
                 ScaledSize(200, 60), 'midtop'),
            '',
            font=fonts.menuFont,
            maxLength=30,
        )
        self.nickBox.onClick.addListener(self.setFocus)
        self.nickBox.onTab.addListener(lambda sender: self.clearFocus())
        name = app.identitySettings.nick
        if name is not None:
            self.nickBox.setValue(name)

        colours = app.theme.colours
        self.cantJoinYet = elements.TextElement(
            self.app,
            '',
            fonts.ingameMenuFont,
            ScaledLocation(256, 115, 'center'),
            colours.cannotJoinColour,
        )

        teamA = world.teams[0]
        teamB = world.teams[1]

        self.elements = [
            elements.TextElement(
                self.app,
                'Please enter your nick:',
                fonts.smallMenuFont,
                Location(
                    DialogBoxAttachedPoint(self, ScaledSize(0, 10), 'midtop'),
                    'midtop'),
                colours.black,
            ), self.nickBox, self.cantJoinYet,
            elements.TextElement(
                self.app,
                'Select team:',
                fonts.smallMenuFont,
                Location(
                    DialogBoxAttachedPoint(self, ScaledSize(0, 130), 'midtop'),
                    'midtop'),
                colours.black,
            ),
            elements.TextButton(self.app,
                                Location(
                                    DialogBoxAttachedPoint(
                                        self, ScaledSize(-25, 160), 'midtop'),
                                    'topright'),
                                str(teamA),
                                fonts.menuFont,
                                colours.team1msg,
                                colours.white,
                                onClick=lambda obj: self.joinTeam(teamA)),
            elements.TextButton(self.app,
                                Location(
                                    DialogBoxAttachedPoint(
                                        self, ScaledSize(25, 160), 'midtop'),
                                    'topleft'),
                                str(teamB),
                                fonts.menuFont,
                                colours.team2msg,
                                colours.white,
                                onClick=lambda obj: self.joinTeam(teamB)),
            elements.TextButton(self.app,
                                Location(
                                    DialogBoxAttachedPoint(
                                        self, ScaledSize(-25, 210), 'midtop'),
                                    'topright'),
                                'Automatic',
                                fonts.menuFont,
                                colours.inGameButtonColour,
                                colours.white,
                                onClick=lambda obj: self.joinTeam()),
            elements.TextButton(self.app,
                                Location(
                                    DialogBoxAttachedPoint(
                                        self, ScaledSize(25, 210), 'midtop'),
                                    'topleft'),
                                'Spectator',
                                fonts.menuFont,
                                colours.inGameButtonColour,
                                colours.white,
                                onClick=lambda obj: self.spectate()),
            elements.TextButton(self.app,
                                Location(
                                    DialogBoxAttachedPoint(
                                        self, ScaledSize(0, -10), 'midbottom'),
                                    'midbottom'),
                                'Cancel',
                                fonts.menuFont,
                                colours.inGameButtonColour,
                                colours.white,
                                onClick=self.cancel)
        ]
        self.setColours(colours.joinGameBorderColour,
                        colours.joinGameTitleColour,
                        colours.joinGameBackgroundColour)
        self.setFocus(self.nickBox)
Exemple #5
0
    def __init__(self, app, tabContainer, onCancel=None, onReplay=None):
        super(SavedGameTab, self).__init__(app, 'Saved Games')
        self.app = app
        self.tabContainer = tabContainer
        self.onCancel = Event(listener=onCancel)
        self.onReplay = Event(listener=onReplay)

        font = self.app.screenManager.fonts.ampleMenuFont
        smallFont = self.app.screenManager.fonts.menuFont
        colours = app.theme.colours

        # Static text
        self.staticText = [
            TextElement(self.app, 'server details:', font,
                        ScaledLocation(960, 200, 'topright'),
                        colours.headingColour),
            TextElement(self.app, 'date and time:', font,
                        ScaledLocation(960, 370, 'topright'),
                        colours.headingColour),
            TextElement(self.app, 'replay:', font,
                        ScaledLocation(620, 550, 'topleft'),
                        colours.headingColour),
            TextElement(self.app, 'stats:', font,
                        ScaledLocation(620, 605, 'topleft'),
                        colours.headingColour)
        ]

        # Dynamic text
        self.listHeaderText = TextElement(self.app, 'available game files:',
                                          font, ScaledLocation(65, 200),
                                          colours.headingColour)
        self.noFiles1Text = TextElement(self.app, '', font,
                                        ScaledLocation(65, 260),
                                        colours.noGamesColour)
        self.noFiles2Text = TextElement(self.app, '', font,
                                        ScaledLocation(65, 310),
                                        colours.noGamesColour)
        self.serverNameText = TextElement(self.app, '', smallFont,
                                          ScaledLocation(960, 255, 'topright'),
                                          colours.startButton)
        self.serverDetailsText = TextElement(
            self.app, '', smallFont, ScaledLocation(960, 295, 'topright'),
            colours.startButton)
        self.dateText = TextElement(self.app, '', smallFont,
                                    ScaledLocation(960, 425, 'topright'),
                                    colours.startButton)
        self.lengthText = TextElement(self.app, '', smallFont,
                                      ScaledLocation(960, 465, 'topright'),
                                      colours.startButton)
        self.noReplayText = TextElement(self.app, '', smallFont,
                                        ScaledLocation(960, 550, 'topright'),
                                        colours.noGamesColour)
        self.noStatsText = TextElement(self.app, '', smallFont,
                                       ScaledLocation(960, 605, 'topright'),
                                       colours.noGamesColour)

        self.dynamicText = [
            self.listHeaderText, self.noFiles1Text, self.noFiles2Text,
            self.serverNameText, self.serverDetailsText, self.dateText,
            self.lengthText, self.noReplayText, self.noStatsText
        ]

        # Text buttons
        self.watchButton = TextButton(self.app,
                                      ScaledLocation(960, 550,
                                                     'topright'), '', font,
                                      colours.secondMenuColour, colours.white)
        self.watchButton.onClick.addListener(self.watchReplay)

        self.statsButton = TextButton(self.app,
                                      ScaledLocation(960, 605,
                                                     'topright'), '', font,
                                      colours.secondMenuColour, colours.white)
        self.statsButton.onClick.addListener(self.viewStats)

        self.refreshButton = TextButton(self.app,
                                        ScaledLocation(620, 665,
                                                       'topleft'), 'refresh',
                                        font, colours.secondMenuColour,
                                        colours.white)
        self.refreshButton.onClick.addListener(self.populateList)

        self.cancelButton = TextButton(self.app,
                                       ScaledLocation(960, 665, 'topright'),
                                       'cancel', font,
                                       colours.secondMenuColour, colours.white)
        self.cancelButton.onClick.addListener(self._cancel)

        self.loadFileButton = TextButton(
            self.app, ScaledLocation(960, 190, 'bottomright'), 'load file...',
            font, colours.mainMenuColour, colours.mainMenuHighlight)
        self.loadFileButton.onClick.addListener(self.showOpenDialog)

        self.buttons = [
            self.watchButton, self.statsButton, self.refreshButton,
            self.cancelButton, self.loadFileButton
        ]

        # Replay list
        self.gameList = ListBox(self.app, ScaledArea(65, 255, 500, 450), [],
                                smallFont, colours.listboxButtons)
        self.gameList.onValueChanged.addListener(self.updateSidebar)

        # Combine the elements
        self.elementsFiles = (self.staticText + self.dynamicText +
                              self.buttons + [self.gameList])
        self.elementsNoFiles = self.dynamicText + self.buttons

        # Populate the list of replays
        self.populateList()
Exemple #6
0
 def mkText(text, x, y, textFont=font, anchor='topright'):
     return TextElement(self.app, text, textFont,
                        ScaledLocation(x, y, anchor), colour)
Exemple #7
0
    def __init__(self, app, onClose=None):
        super(DisplaySettingsTab, self).__init__(app, 'Display')

        self.onClose = Event()
        if onClose is not None:
            self.onClose.addListener(onClose)

        font = self.app.screenManager.fonts.bigMenuFont
        smallNoteFont = self.app.screenManager.fonts.smallNoteFont

        colour = self.app.theme.colours.headingColour

        def mkText(text, x, y, textFont=font, anchor='topright'):
            return TextElement(self.app, text, textFont,
                               ScaledLocation(x, y, anchor), colour)

        self.text = [
            mkText('X', 640, 280),
            mkText('Screen resolution', 430, 280),
            mkText('Fullscreen mode', 430, 360),
            mkText('Graphics detail', 430, 440),
            mkText('low', 460, 475, textFont=smallNoteFont, anchor='midtop'),
            mkText('high', 845, 475, textFont=smallNoteFont, anchor='midtop'),
            mkText('Show timings', 430, 525),
        ]

        self.invalidInputText = TextElement(self.app, '', font,
                                            ScaledLocation(512, 230, 'midtop'),
                                            (192, 0, 0))

        self.widthInput = prompt.InputBox(self.app,
                                          ScaledArea(460, 265, 150, 60),
                                          initValue=str(
                                              self.app.screenManager.size[0]),
                                          font=font,
                                          maxLength=4,
                                          validator=prompt.intValidator)

        self.widthInput.onEnter.addListener(lambda sender: self.saveSettings())
        self.widthInput.onClick.addListener(self.setFocus)
        self.widthInput.onTab.addListener(self.tabNext)

        self.heightInput = prompt.InputBox(self.app,
                                           ScaledArea(652, 265, 150, 60),
                                           initValue=str(
                                               self.app.screenManager.size[1]),
                                           font=font,
                                           maxLength=4,
                                           validator=prompt.intValidator)

        self.heightInput.onEnter.addListener(
            lambda sender: self.saveSettings())
        self.heightInput.onClick.addListener(self.setFocus)
        self.heightInput.onTab.addListener(self.tabNext)

        self.tabOrder = [self.widthInput, self.heightInput]

        self.fullscreenBox = CheckBox(
            self.app,
            ScaledLocation(460, 365),
            text='',
            font=font,
            colour=(192, 192, 192),
            initValue=self.app.screenManager.isFullScreen(),
        )
        self.fullscreenBox.onValueChanged.addListener(self.fullscreenChanged)

        displaySettings = app.displaySettings

        self.detailSlider = Slider(
            self.app,
            ScaledArea(460, 430, 390, 40),
            bounds=(0, len(displaySettings.DETAIL_LEVELS) - 1),
            snap=True)
        self.detailSlider.setVal(
            displaySettings.DETAIL_LEVELS.index(displaySettings.detailLevel))
        self.detailSlider.onValueChanged.addListener(self.detailChanged)

        self.showTimingsBox = CheckBox(
            self.app,
            ScaledLocation(460, 530),
            text='',
            font=font,
            colour=(192, 192, 192),
            initValue=displaySettings.showTimings,
        )

        self.input = [
            self.widthInput, self.heightInput, self.widthInput,
            self.fullscreenBox, self.detailSlider, self.showTimingsBox
        ]

        self.elements = self.text + self.input + [
            self.invalidInputText,
            button(app,
                   'save',
                   self.saveSettings, (-100, -75),
                   'midbottom',
                   secondColour=app.theme.colours.white),
            button(app,
                   'cancel',
                   self.cancelMenu, (100, -75),
                   'midbottom',
                   secondColour=app.theme.colours.white),
        ]
        self.setFocus(self.widthInput)
Exemple #8
0
    def __init__(self, app, onClose=None):
        super(KeymapTab, self).__init__(app, 'Controls')
        self.font = app.screenManager.fonts.bigMenuFont

        self.onClose = Event()
        if onClose is not None:
            self.onClose.addListener(onClose)

        # Break things up into categories
        movement = ['jump', 'down', 'left', 'right']
        menus = ['menu', 'more actions']
        actions = [
            'respawn', 'select upgrade', 'activate upgrade', 'change nickname',
            'ready'
        ]
        misc = ['chat', 'follow']
        upgrades = [
            upgradeClass.action for upgradeClass in sorted(
                allUpgrades, key=lambda upgradeClass: upgradeClass.order)
        ]
        upgrades.append('no upgrade')

        display = ['leaderboard', 'toggle interface', 'toggle terminal']

        actionNames = {
            'select upgrade': 'Select upgrade',
            'activate upgrade': 'Activate upgrade',
            'change nickname': 'Change nickname',
            'chat': 'Chat',
            'down': 'Drop down',
            'follow': 'Auto pan (replay)',
            'jump': 'Jump',
            'leaderboard': 'Show leaderboard',
            'left': 'Move left',
            'menu': 'Main menu',
            'more actions': 'Advanced',
            'no upgrade': 'Deselect upgrade',
            'ready': 'Toggle ready',
            'respawn': 'Respawn',
            'right': 'Move right',
            'status bar': 'Status bar',
            'timer': 'Show timer',
            'toggle interface': 'Toggle HUD',
            'toggle terminal': 'Toggle terminal',
            'zone progress': 'Show zone bar',
        }
        actionNames.update((upgradeClass.action, upgradeClass.name)
                           for upgradeClass in allUpgrades)

        # Organise the categories by column
        self.layout = [
            [movement, menus],
            [actions, display],
            [upgrades, misc],
        ]

        self.errorInfo = TextElement(self.app, '', self.font,
                                     ScaledLocation(512, 580, 'center'))
        self.text = [self.errorInfo]
        self.inputLookup = {}
        xPos = 190

        # Lay everything out automatically.
        keymapFont = self.app.screenManager.fonts.keymapFont
        keymapInputFont = self.app.screenManager.fonts.keymapInputFont
        for column in self.layout:  # Each column
            yPos = 200
            for category in column:  # Each category
                for action in category:  # Each action
                    # Draw action name (eg. Respawn)
                    self.text.append(
                        TextElement(self.app, actionNames[action], keymapFont,
                                    ScaledLocation(xPos, yPos + 6, 'topright'),
                                    self.app.theme.colours.headingColour))

                    # Create input box
                    box = prompt.KeycodeBox(self.app,
                                            ScaledArea(xPos + 10, yPos, 100,
                                                       30),
                                            font=keymapInputFont)
                    box.onClick.addListener(self.setFocus)
                    box.onChange.addListener(self.inputChanged)
                    box.__action = action
                    self.inputLookup[action] = box

                    yPos += 35  # Between items
                yPos += 35  # Between categories
            xPos += 320  # Between columns

        self.elements = self.text + self.inputLookup.values() + [
            button(app,
                   'restore default controls',
                   self.restoreDefaults, (0, -125),
                   'midbottom',
                   secondColour=app.theme.colours.white),
            button(app,
                   'save',
                   self.saveSettings, (-100, -75),
                   'midbottom',
                   secondColour=app.theme.colours.white),
            button(app,
                   'cancel',
                   self.cancel, (100, -75),
                   'midbottom',
                   secondColour=app.theme.colours.white),
        ]

        self.populateInputs()
Exemple #9
0
 def heading(self, caption):
     return TextElement(self.app, caption, self.font,
                        ScaledLocation(1000, 60, 'topright'),
                        self.app.theme.colours.headingColour)
    def __init__(self, app, mainInterface):
        super(StartupInterface, self).__init__(app)
        self.interface = mainInterface

        # Create font.
        self.font = self.app.screenManager.fonts.bigMenuFont

        self.offsets = self.app.screenManager.offsets

        self.extra = []
        y0 = 0
        policies = getPolicySettings()
        if policies.get('privacy', 'sendusername', fallback=False):
            y0 = 75
            self.extra.extend([
                TextElement(self.app,
                            'Logged in as %s' % (getpass.getuser(), ),
                            self.font, ScaledLocation(65, 175, 'topleft'),
                            self.app.theme.colours.headingColour),
                TextElement(
                    self.app,
                    'Not you? Log out of this computer and log back in',
                    self.app.screenManager.fonts.smallMenuFont,
                    ScaledLocation(65, 225, 'topleft'),
                    self.app.theme.colours.headingColour),
                TextElement(self.app,
                            'as yourself so you can earn achievements',
                            self.app.screenManager.fonts.smallMenuFont,
                            ScaledLocation(65, 255, 'topleft'),
                            self.app.theme.colours.headingColour)
            ])

        # Create other elements.
        self.buttons = [
            self.button('play',
                        self.playClicked, (65, y0 + 225),
                        hugeFont=True),
            self.button('servers',
                        self.serverSelectionClicked, (85, y0 + 285),
                        smallFont=True),
            self.button('practise',
                        self.practiseClicked, (85, y0 + 325),
                        smallFont=True),
            self.button('archives', self.savedGamesClicked, (65, y0 + 420)),
            self.button('settings', self.settingsClicked, (65, y0 + 490)),
            self.button('credits', self.creditsClicked, (65, y0 + 560)),
            self.button('exit', self.exitClicked, (939, 700), 'topright')
        ]
        self.firstTimeNotification = FirstPlayNotificationBar(app)
        self.updateNotification = self._makeUpdateNotificationBar()
        self.elements = []

        if app.identitySettings.firstTime:
            self.firstTimeNotification.show()

        # Create sub-menus.
        self.settingsMenu = SettingsMenu(app,
                                         onClose=self.mainMenu,
                                         onRestart=app.restart)
        self.serverSelectionScreen = ServerSelectionScreen(
            app, onClose=self.mainMenu)
        self.savedGameMenu = None
        self.practiseScreen = self.practiseScreenFactory(
            app,
            onClose=self.mainMenu,
            onStart=self.interface.connectToGameObject)
        self.creditsScreen = self.creditsScreenFactory(
            self.app,
            self.app.theme.colours.mainMenuColour,
            self.mainMenu,
            highlight=self.app.theme.colours.mainMenuHighlight)

        self.mainMenu()
    def __init__(self, app, onClose=None):
        super(KeymapTab, self).__init__(app, 'Controls')
        self.font = app.screenManager.fonts.bigMenuFont

        self.onClose = Event()
        if onClose is not None:
            self.onClose.addListener(onClose)

        # Break things up into categories
        movement = [
            ACTION_JUMP, ACTION_DOWN, ACTION_LEFT, ACTION_RIGHT, ACTION_HOOK
        ]
        menus = [ACTION_MAIN_MENU, ACTION_MORE_MENU]
        actions = [
            ACTION_UPGRADE_MENU, ACTION_USE_UPGRADE, ACTION_EDIT_PLAYER_INFO,
            ACTION_READY, ACTION_SHOW_TRAJECTORY, ACTION_EMOTE
        ]
        misc = [ACTION_CHAT, ACTION_FOLLOW]
        upgrades = [
            upgradeClass.action for upgradeClass in sorted(
                allUpgrades, key=lambda upgradeClass: upgradeClass.order)
        ]
        upgrades.append(ACTION_CLEAR_UPGRADE)

        display = [
            ACTION_LEADERBOARD_TOGGLE, ACTION_HUD_TOGGLE,
            ACTION_TERMINAL_TOGGLE
        ]

        actionNames = {
            ACTION_EDIT_PLAYER_INFO: 'Change nick / hat',
            ACTION_CHAT: 'Chat',
            ACTION_CLEAR_UPGRADE: 'Deselect upgrade',
            ACTION_DOWN: 'Drop down',
            ACTION_FOLLOW: 'Auto pan (replay)',
            ACTION_HOOK: 'Grappling hook',
            ACTION_HUD_TOGGLE: 'Toggle HUD',
            ACTION_JUMP: 'Jump',
            ACTION_LEADERBOARD_TOGGLE: 'Show leaderboard',
            ACTION_LEFT: 'Move left',
            ACTION_MAIN_MENU: 'Main menu',
            ACTION_MORE_MENU: 'Advanced',
            ACTION_READY: 'Toggle ready',
            ACTION_RIGHT: 'Move right',
            ACTION_TERMINAL_TOGGLE: 'Toggle terminal',
            ACTION_UPGRADE_MENU: 'Select upgrade',
            ACTION_USE_UPGRADE: 'Activate upgrade',
            ACTION_SHOW_TRAJECTORY: 'Show trajectory',
            ACTION_EMOTE: 'Emote',
        }
        actionNames.update((upgradeClass.action, upgradeClass.name)
                           for upgradeClass in allUpgrades)

        # Organise the categories by column
        self.layout = [
            [movement, menus],
            [actions, display],
            [upgrades, misc],
        ]

        self.errorInfo = TextElement(self.app, '', self.font,
                                     ScaledLocation(512, 580, 'center'))
        self.text = [self.errorInfo]
        self.inputLookup = {}
        xPos = 210

        # Lay everything out automatically.
        keymapFont = self.app.screenManager.fonts.keymapFont
        keymapInputFont = self.app.screenManager.fonts.keymapInputFont
        for column in self.layout:  # Each column
            yPos = 200
            for category in column:  # Each category
                for action in category:  # Each action
                    # Draw action name (eg. Respawn)
                    self.text.append(
                        TextElement(self.app, actionNames[action], keymapFont,
                                    ScaledLocation(xPos, yPos + 6, 'topright'),
                                    self.app.theme.colours.headingColour))

                    # Create input box
                    box = prompt.KeycodeBox(self.app,
                                            ScaledArea(xPos + 10, yPos, 100,
                                                       30),
                                            font=keymapInputFont,
                                            acceptMouse=True)
                    box.onClick.addListener(self.setFocus)
                    box.onChange.addListener(self.inputChanged)
                    box.__action = action
                    self.inputLookup[action] = box

                    yPos += 35  # Between items
                yPos += 35  # Between categories
            xPos += 310  # Between columns

        self.elements = self.text + list(self.inputLookup.values()) + [
            button(app,
                   'restore default controls',
                   self.restoreDefaults, (0, -125),
                   'midbottom',
                   secondColour=app.theme.colours.white),
            button(app,
                   'save',
                   self.saveSettings, (-100, -75),
                   'midbottom',
                   secondColour=app.theme.colours.white),
            button(app,
                   'cancel',
                   self.cancel, (100, -75),
                   'midbottom',
                   secondColour=app.theme.colours.white),
        ]

        self.populateInputs()