# Copyright (C) 2011-2018 A S Lewis
#
# 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 3 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,
# see <http://www.gnu.org/licenses/>.
#
#
# Games::Axmud::Win::External
# Object handling 'external' windows (any window that can be placed on the workspace grid, but which
#   is not created/controlled by Axmud). Doesn't include 'free' windows
#
# Games::Axmud::Win::Internal
# Object handling 'internal' 'grid' windows ('grid' windows whose window type is 'main', 'protocol'
#   or 'custom'). Doesn't include 'map', 'fixed', 'external' or 'free' windows
#
# Games::Axmud::Win::Map
# The Automapper window object (separate and independent from the automapper object, GA::Obj::Map)

{ package Games::Axmud::Win::External;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(
        Games::Axmud::Generic::GridWin Games::Axmud::Generic::Win Games::Axmud
    );

    ##################
    # Constructors

    sub new {

        # Called by GA::Obj::Workspace->createGridWin and ->createSimpleGridWin
        # Creates an 'external' 'grid' window (any window that can be placed on the workspace grid,
        #   but which is not created/controlled by Axmud)
        #
        # Expected arguments
        #   $number     - Unique number for this window object
        #   $winType    - The window type, must be 'external'
        #   $winName    - The 'external' window's name (e.g. 'Notepad')
        #   $workspaceObj
        #               - The GA::Obj::Workspace object for the workspace in which this window is
        #                   created
        #
        # Optional arguments
        #   $owner      - The owner, if known. Can be any blessed reference, typically it's an
        #                   GA::Session or a task (inheriting from GA::Generic::Task); could also
        #                   be GA::Client
        #   $session    - The owner's session. If $owner is a GA::Session, that session. If it's
        #                   something else (like a task), the task's session. If $owner is 'undef',
        #                   so is $session
        #   $workspaceGridObj
        #               - The GA::Obj::WorkspaceGrid object into whose grid this window has been
        #                   placed. 'undef' in $workspaceObj->gridEnableFlag = FALSE
        #   $areaObj    - The GA::Obj::Area (a region of a workspace grid zone) which handles this
        #                   window. 'undef' in $workspaceObj->gridEnableFlag = FALSE
        #   $winmap     - On calls to GA::Win::Internal->new, this argument describes the ->name
        #                   of an a GA::Obj::Winmap object. 'external' windows don't use winmaps
        #                   so, if specified, this argument is ignored
        #
        # Return values
        #   'undef' on improper arguments
        #   Blessed reference to the newly-created object on success

        my (
            $class, $number, $winType, $winName, $workspaceObj, $owner, $session, $workspaceGridObj,
            $areaObj, $winmap, $check,
        ) = @_;

        # Check for improper arguments
        if (
            ! defined $class || ! defined $number || ! defined $winType || ! defined $winName
            || ! defined $workspaceObj || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($class . '->new', @_);
        }

        # Check that the $winType is valid
        if ($winType ne 'external') {

            return $axmud::CLIENT->writeError(
                'Internal window error: invalid \'external\' window type \'' . $winType . '\'',
                $class . '->new',
            );
        }

        # Setup
        my $self = {
            _objName                    => 'external_win_' . $number,
            _objClass                   => $class,
            _parentFile                 => undef,       # No parent file object
            _parentWorld                => undef,       # No parent file object
            _privFlag                   => TRUE,        # All IVs are private

            # Standard window object IVs
            # --------------------------

            # Unique number for this window object
            number                      => $number,
            # The window category - 'grid' or 'free'
            winCategory                 => 'grid',
            # The window type, must be 'external'
            winType                     => $winType,
            # The external window's name (e.g. 'Notepad')
            winName                     => $winName,
            # The GA::Obj::Workspace object for the workspace in which this window is created
            workspaceObj                => $workspaceObj,
            # The owner, if known ('undef' if not). If set, can be any blessed reference, typically
            #   it's a GA::Session or a task (inheriting from GA::Generic::Task). When there are
            #   no sessions running but there's a single 'main' window open, the owner is the
            #   GA::Client. When the window closes, the owner is informed via a call to its
            #   ->del_winObj function
            owner                       => $owner,
            # The owner's session ('undef' if not). If ->owner is a GA::Session, that session. If
            #   it's something else (like a task), the task's sesssion. If ->owner is 'undef', so is
            #   ->session
            session                     => $session,
            # When GA::Session->pseudoCmd is called to execute a client command, the mode in which
            #   it should be called (usually 'win_error' or 'win_only', which causes errors to be
            #   displayed in a 'dialogue' window)
            pseudoCmdMode               => 'win_error',

            # The window widget. For most window objects, the Gtk2::Window. For pseudo-windows, the
            #   parent 'main' window's Gtk2::Window
            # The code should use this IV when it wants to do something to the window itself
            #   (minimise it, make it active, etc)
            winWidget                   => undef,
            # The window container. For most window objects, the Gtk2::Window. For pseudo-windows,
            #   the parent GA::Table::PseudoWin table object
            # The code should use this IV when it wants to add, modify or remove widgets inside the
            #   window itself
            winBox                      => undef,
            # The Gnome2::Wnck::Window, if known
            wnckWin                     => undef,
            # Flag set to TRUE if the window actually exists (after a call to $self->winEnable),
            #   FALSE if not
            enabledFlag                 => FALSE,
            # Flag set to TRUE if the Gtk2 window itself is visible (after a call to
            #   $self->setVisible), FALSE if it is not visible (after a call to $self->setInvisible)
            visibleFlag                 => TRUE,
            # Registry hash of 'free' windows for which this window is the parent (always empty,
            #   because 'external' windows can be a parent window to a 'free' window)
            childFreeWinHash            => {},
            # When a child 'free' window (excluding 'dialogue' windows) is destroyed, this parent
            #   window is informed via a call to $self->del_childFreeWin
            # When the child is destroyed, this window might want to call some of its own functions
            #   to update various widgets and/or IVs, in which case this window adds an entry to
            #   this hash; a hash in the form
            #       $childDestroyHash{unique_number} = list_reference
            # ...where 'unique_number' is the child window's ->number, and 'list_reference' is a
            #   reference to a list in groups of 2, in the form
            #       (sub_name, argument_list_ref, sub_name, argument_list_ref...)
            childDestroyHash            => {},

            # The container widget into which all other widgets are packed (not required for an
            #   'external' window)
            packingBox                  => undef,

            # Standard IVs for 'grid' windows

            # The GA::Obj::WorkspaceGrid object into whose grid this window has been placed. 'undef'
            #   in $workspaceObj->gridEnableFlag = FALSE
            workspaceGridObj            => $workspaceGridObj,
            # The GA::Obj::Area object for this window. An area object is a part of a zone's
            #   internal grid, handling a single window (this one). Set to 'undef' in
            #   $workspaceObj->gridEnableFlag = FALSE
            areaObj                     => $areaObj,
            # For pseudo-windows (in which a window object is created, but its widgets are drawn
            #   inside a GA::Table::PseudoWin table object), the table object created. Always
            #   'undef' for 'external' windows which can't be pseudo-windows
            pseudoWinTableObj           => undef,
            # The name of the GA::Obj::Winmap object that specifies the Gtk2::Window's layout when
            #   it is first created. Always 'undef' for 'external' windows
            winmap                      => undef,

            # Standard IVs for 'external' windows

            # The position and size of the 'external' window, before it was grabbed onto one of
            #   Axmud's workspace grids by ;grabwindow
            prevXPosPixels              => undef,
            prevYPosPixels              => undef,
            prevWidthPixels             => undef,
            prevHeightPixels            => undef,
        };

        # Bless the object into existence
        bless $self, $class;

        return $self;
    }

    ##################
    # Methods

    sub winSetup {

        # Called by GA::Obj::Workspace->createGridWin or ->createSimpleGridWin
        # The actual window already exists, but we still need to update IVs
        #
        # Expected arguments
        #   $wnckWin    - The 'external' window's Gnome2::Wnck::Window
        #
        # Return values
        #   'undef' on improper arguments
        #   1 on success

        my ($self, $wnckWin, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->winSetup', @_);
        }

        # Update IVs
        $self->ivPoke('wnckWin', $wnckWin);

        return 1;
    }

    sub winEnable {

        # Called by GA::Obj::Workspace->createGridWin or ->createSimpleGridWin
        # Used for consistency with 'internal' windows
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $listRef    - Reference to a list of functions for 'internal' windows. If specified,
        #                   ignored
        #
        # Return values
        #   'undef' on improper arguments
        #   1 on success

        my ($self, $listRef, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->winEnable', @_);
        }

        # Set up ->signal_connects
        $self->setDeleteEvent();            # 'delete-event'
        $self->setWindowClosedEvent();      # 'window-closed'

        # Make the window appear on the desktop
        $self->winShowAll($self->_objClass . '->winEnable');
        $self->ivPoke('enabledFlag', TRUE);

        return 1;
    }

    sub winDisengage {

        # Called by GA::Cmd->BanishWindow->do
        #
        # Destroys the window object, but not the window itself, leaving the 'external' window free
        #   to pursue its own dreams
        # Marks the area of the zone the window used to occupy as free, and available for other
        #   workspace grid windows
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the window has already been disengaged in a previous
        #       call to this function
        #   1 if the window is disengaged

        my ($self, $check) = @_;

        # Local variables
        my ($zoneObj, $flag);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->winDisengage', @_);
        }

        if (! $self->winBox) {

            # Window already disengaged in a previous call to this function
            return undef;
        }

        # 'External' windows can't have child windows. But, the IV storing child windows exists, so
        #   in case someone decides to add some child windows anyway, close them
        foreach my $winObj ($self->ivValues('childFreeWinHash')) {

            $winObj->winDestroy();
        }

        # Inform the parent workspace grid object (if any)
        if ($self->workspaceGridObj) {

            $self->workspaceGridObj->del_gridWin($self);
        }

        # Inform the desktop object
        $axmud::CLIENT->desktopObj->del_gridWin($self);

        # Look for other 'grid' windows (besides this one) handling the same 'external' window. If
        #   there are none, it's safe to restore the 'external' window to its original size and
        #   position
        # (Don't bother if the original size/position isn't known)
        if (defined $self->prevXPosPixels) {

            OUTER: foreach my $winObj ($axmud::CLIENT->desktopObj->ivValues('gridWinHash')) {

                if (
                    ($self->winBox && $winObj->winBox && $self->winBox eq $winObj->winBox)
                    || (
                        $self->wnckWin && $winObj->wnckWin
                        && $self->wnckWin eq $winObj->wnckWin
                    )
                ) {
                    $flag = TRUE;
                    last OUTER;
                }
            }

            if (! $flag) {

                # Restore the 'external' window to its original size and position
                $self->workspaceObj->moveResizeWin(
                    $self,
                    $self->prevXPosPixels,
                    $self->prevYPosPixels,
                    $self->prevWidthPixels,
                    $self->prevHeightPixels,
                );

                # Minimise the window so that, visually, it appears to have been removed from
                #   Axmud's control
                if ($self->wnckWin) {

                    $self->wnckWin->minimize();
                }
            }
        }

        # Operation complete
        $self->ivUndef('winWidget');
        $self->ivUndef('winBox');

        return 1;
    }

    sub winDestroy {

        # Called by ->signal_connects in $self->setDeleteEvent and ->setWindowClosedEvent
        # Marks the area of the zone the window used to occupy as free, and available for other
        #   workspace grid windows
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the window has already been disengaged in a previous
        #       call to this function
        #   1 if the window is disengaged

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->winDestroy', @_);
        }

        if (! $self->winBox) {

            # Window already disengaged in a previous call to this function
            return undef;
        }

        # 'External' windows can't have child windows. But, the IV storing child windows exists,
        #   so in case someone decides to add some child windows anyway, close them
        foreach my $winObj ($self->ivValues('childFreeWinHash')) {

            $winObj->winDestroy();
        }

        # Inform the parent workspace grid object (if any)
        if ($self->workspaceGridObj) {

            $self->workspaceGridObj->del_gridWin($self);
        }

        # Inform the desktop object
        $axmud::CLIENT->desktopObj->del_gridWin($self);

        # Inform the ->owner, if there is one
        if ($self->owner) {

            $self->owner->del_winObj($self);
        }

        # Operation complete
        $self->ivUndef('winWidget');
        $self->ivUndef('winBox');

        return 1;
    }

#   sub winShowAll {}       # Inherited from GA::Generic::Win

#   sub drawWidgets {}      # Inherited from GA::Generic::Win

#   sub redrawWidgets {}    # Inherited from GA::Generic::Win

    # ->signal_connects

    sub setDeleteEvent {

        # Called by $self->winEnable
        # Set up a ->signal_connect to watch out for the user manually closing the 'external' window
        #   (in which case the window must be removed from its workspace grid)
        # This function uses $self->winBox; the next function uses $self->wnckWin (in case one of
        #   them is not set)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $winBox, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->setDeleteEvent', @_);
        }

        # (We don't assume that $self->winBox is known)
        if ($self->winBox) {

            $self->winBox->signal_connect('delete-event' => sub {

                # Prevent Gtk2 from taking action directly. Instead remove the window object from
                #   its workspace grid
                return $self->winDestroy();
            });
        }

        return 1;
    }

    sub setWindowClosedEvent {

        # Called by $self->winEnable
        # Set up a ->signal_connect to watch out for the user manually closing the 'external' window
        #   (in which case the window must be removed from its workspace grid)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $screen;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->setWindowClosedEvent', @_);
        }

        # (We don't assume that $self->wnckWin is known)
        if ($self->wnckWin) {

            $screen = $self->wnckWin->get_screen();

            $screen->signal_connect('window-closed' => sub {

                my ($screen, $closedWin) = @_;

                if ($closedWin eq $self->wnckWin) {

                    # Remove the window object from its workspace grid
                    return $self->winDestroy();
                }
            });
        }

        return 1;
    }

    ##################
    # Accessors - set

    sub set_oldPosn {

        my ($self, $xPos, $yPos, $width, $height, $check) = @_;

        # Check for improper arguments
        if (
            ! defined $xPos || ! defined $yPos || ! defined $width || ! defined $height
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->set_oldPosn', @_);
        }

        $self->ivPoke('prevXPosPixels', $xPos);
        $self->ivPoke('prevYPosPixels', $yPos);
        $self->ivPoke('prevWidthPixels', $width);
        $self->ivPoke('prevHeightPixels', $height);

        return 1;
    }

    ##################
    # Accessors - get

    sub prevXPosPixels
        { $_[0]->{prevXPosPixels} }
    sub prevYPosPixels
        { $_[0]->{prevYPosPixels} }
    sub prevWidthPixels
        { $_[0]->{prevWidthPixels} }
    sub prevHeightPixels
        { $_[0]->{prevHeightPixels} }
}

{ package Games::Axmud::Win::Internal;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(
        Games::Axmud::Generic::GridWin Games::Axmud::Generic::Win Games::Axmud
    );

    ##################
    # Constructors

    sub new {

        # Called by GA::Obj::Workspace->createGridWin and ->createSimpleGridWin
        # Creates an 'internal' 'grid' window (any window that can be placed on the workspace grid,
        #   and which is created/controlled by Axmud)
        #
        # Expected arguments
        #   $number     - Unique number for this window object
        #   $winType    - The window type, any of the keys in GA::Client->constGridWinTypeHash
        #                   (except 'external')
        #   $winName    - A name for the window:
        #                   $winType    $winName
        #                   --------    --------
        #                   main        main
        #                   protocol    Any string chosen by the protocol code (default value is
        #                                   'protocol')
        #                   custom      Any string chosen by the controlling code. For task windows,
        #                                   the name of the task (e.g. 'status_task', for other
        #                                   windows, default value is 'custom'
        #   $workspaceObj
        #               - The GA::Obj::Workspace object for the workspace in which this window is
        #                   created
        #
        # Optional arguments
        #   $owner      - The owner, if known ('undef' if not). Typically it's a GA::Session or a
        #                   task (inheriting from GA::Generic::Task); could also be GA::Client. iT
        #                   Should not be another window object (inheriting from GA::Generic::Win).
        #                   The owner should have its own ->del_winObj function which is called when
        #                   $self->winDestroy is called
        #   $session    - The owner's session. If $owner is a GA::Session, that session. If it's
        #                   something else (like a task), the task's session. If $owner is 'undef',
        #                   so is $session
        #   $workspaceGridObj
        #               - The GA::Obj::WorkspaceGrid object into whose grid this window has been
        #                   placed. 'undef' in $workspaceObj->gridEnableFlag = FALSE
        #   $areaObj    - The GA::Obj::Area (a region of a workspace grid zone) which handles this
        #                   window. 'undef' in $workspaceObj->gridEnableFlag = FALSE
        #   $winmap     - The ->name of the GA::Obj::Winmap object that specifies the Gtk2::Window's
        #                   layout when it is first created. If 'undef', a default winmap is used
        #
        # Return values
        #   'undef' on improper arguments
        #   Blessed reference to the newly-created object on success

        my (
            $class, $number, $winType, $winName, $workspaceObj, $owner, $session, $workspaceGridObj,
            $areaObj, $winmap, $check,
        ) = @_;

        # Check for improper arguments
        if (
            ! defined $class || ! defined $number || ! defined $winType || ! defined $winName
            || ! defined $workspaceObj || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($class . '->new', @_);
        }

        # Check that the $winType is valid
        if (
            ! $axmud::CLIENT->ivExists('constGridWinTypeHash', $winType)
            || ($winType ne 'main' && $winType ne 'protocol' && $winType ne 'custom')
        ) {
            return $axmud::CLIENT->writeError(
                'Internal window error: invalid \'internal\' window type \'' . $winType . '\'',
                $class . '->new',
            );
        }

        # Setup
        my $self = {
            _objName                    => 'internal_win_' . $number,
            _objClass                   => $class,
            _parentFile                 => undef,       # No parent file object
            _parentWorld                => undef,       # No parent file object
            _privFlag                   => TRUE,        # All IVs are private

            # Standard window object IVs
            # --------------------------

            # Unique number for this window object
            number                      => $number,
            # The window category - 'grid' or 'free'
            winCategory                 => 'grid',
            # The window type, any of the keys in GA::Client->constGridWinTypeHash (except
            #   'external')
            winType                     => $winType,
            # A name for the window:
            #       $winType    $winName
            #       --------    --------
            #       main        main
            #       protocol    Any string chosen by the protocol code (default value is 'protocol')
            #       custom      Any string chosen by the controlling code. For task windows, the
            #                       name of the task (e.g. 'status_task', for other windows, default
            #                       value is 'custom'
            winName                     => $winName,
            # The GA::Obj::Workspace object for the workspace in which this window is created
            workspaceObj                => $workspaceObj,
            # The owner, if known ('undef' if not). Typically it's a GA::Session or a task
            #   (inheriting from GA::Generic::Task); could also be GA::Client. It should not be
            #   another window object (inheriting from GA::Generic::Win). The owner must have its
            #   own ->del_winObj function which is called when $self->winDestroy is called
            owner                       => $owner,
            # The owner's session ('undef' if no owner). If ->owner is a GA::Session, that session.
            #   If it's something else (like a task), the task's sesssion. If ->owner is 'undef', so
            #   is ->session
            session                     => $session,
            # When GA::Session->pseudoCmd is called to execute a client command, the mode in which
            #   it should be called (usually 'win_error' or 'win_only', which causes errors to be
            #   displayed in a 'dialogue' window)
            pseudoCmdMode               => 'win_error',

            # The window widget. For most window objects, the Gtk2::Window. For pseudo-windows, the
            #   parent 'main' window's Gtk2::Window
            # The code should use this IV when it wants to do something to the window itself
            #   (minimise it, make it active, etc)
            winWidget                   => undef,
            # The window container. For most window objects, the Gtk2::Window. For pseudo-windows,
            #   the parent GA::Table::PseudoWin table object
            # The code should use this IV when it wants to add, modify or remove widgets inside the
            #   window itself
            winBox                      => undef,
            # The Gnome2::Wnck::Window, if known
            wnckWin                     => undef,
            # Flag set to TRUE if the window actually exists (after a call to $self->winEnable),
            #   FALSE if not
            enabledFlag                 => FALSE,
            # Flag set to TRUE if the Gtk2 window itself is visible (after a call to
            #   $self->setVisible), FALSE if it is not visible (after a call to $self->setInvisible)
            visibleFlag                 => TRUE,
            # Registry hash of 'free' windows (excluding 'dialogue' windows) for which this window
            #   is the parent, a subset of GA::Obj::Desktop->freeWinHash. Hash in the form
            #       $childFreeWinHash{unique_number} = blessed_reference_to_window_object
            childFreeWinHash            => {},
            # When a child 'free' window (excluding 'dialogue' windows) is destroyed, this parent
            #   window is informed via a call to $self->del_childFreeWin
            # When the child is destroyed, this window might want to call some of its own functions
            #   to update various widgets and/or IVs, in which case this window adds an entry to
            #   this hash; a hash in the form
            #       $childDestroyHash{unique_number} = list_reference
            # ...where 'unique_number' is the child window's ->number, and 'list_reference' is a
            #   reference to a list in groups of 2, in the form
            #       (sub_name, argument_list_ref, sub_name, argument_list_ref...)
            childDestroyHash            => {},

            # The container widget into which all other widgets are packed (usually a Gtk2::VBox or
            #   Gtk2::HBox, but any container widget can be used; takes up the whole window client
            #   area)
            packingBox                  => undef,

            # Standard IVs for 'grid' windows

            # The GA::Obj::WorkspaceGrid object into whose grid this window has been placed. 'undef'
            #   in $workspaceObj->gridEnableFlag = FALSE
            # For 'main' windows only, $self->setVisibleSession changes the value of this IV every
            #   time the visible session changes
            workspaceGridObj            => $workspaceGridObj,
            # The GA::Obj::Area object for this window. An area object is a part of a zone's
            #   internal grid, handling a single window (this one). Set to 'undef' in
            #   $workspaceObj->gridEnableFlag = FALSE
            areaObj                     => $areaObj,
            # For pseudo-windows (in which a window object is created, but its widgets are drawn
            #   inside a GA::Table::PseudoWin table object), the table object created. 'undef' if
            #   this window object is a real 'grid' window
            # NB 'main' windows can't be pseudo-windows, but 'protocol' and 'custom' windows can
            pseudoWinTableObj           => undef,
            # The ->name of the GA::Obj::Winmap object that specifies the Gtk2::Window's layout when
            #   it is first created. If 'undef', a default winmap is used
            winmap                      => $winmap,

            # IVs for 'internal' windows

            # Hash of strip objects (inheriting from GA::Generic::Strip) currently packed into
            #   $self->packingBox. Hash in the form
            #       $stripHash{number} = blessed_reference_to_strip_object
            stripHash                   => {},
            # A hash of the first instance of each type of strip object that was packed into
            #   $self->packingBox and is still there (a subset of $self->stripHash)
            # For 'jealous' strip objects, we can use this hash to find a strip object of a
            #   particular type quickly. For other strip, we can use this hash to treat the first
            #   instance of a particular type as the default one
            # Hash in the form
            #   $firstStripHash{object_class} = blessed_reference_to_strip_object
            firstStripHash              => {},
            # Number of strip objects ever created for this window (used to give every strip object
            #   a number unique to the window)
            stripCount                  => 0,
            # A list of strip objects in the order in which they were packed into the window (the
            #   order in which they were created by $self->drawWidgets or the order in which they
            #   were re-packed by $self->redrawWidgets, ->addStripObj, ->hideStripObj etc)
            stripList                   => [],
            # A shortcut to the compulsory GA::Strip::Table object (also stored as a value in
            #   $self->stripHash)
            tableStripObj               => undef,
            # Leave a small gap between strip objects
            stripSpacingPixels          => 2,

            # Other IVs

            # If GA::CLIENT->shareMainWinFlag = TRUE, all sessions share the same default pane
            #   object (GA::Table::Pane) in a single shared 'main' window. In that default pane
            #   object, only one session's default textview object is visible; that session is the
            #   visible session
            # If GA::Client->shareMainWinFlag = FALSE, the GA::Session which controls this 'main'
            #   window
            # In both cases, set to 'undef' for windows that aren't 'main' windows or if there are
            #   no sessions running at all
            visibleSession              => undef,

            # Whenever some of other Axmud function wants to set (or reset) the text used in the
            #    the GA::Strip::ConnectInfo strip object, it calls $self->setHostLabel and/or
            #   ->setTimeLabel. The text is stored here so it's available immediately, if the strip
            #   object is brought into existence; if the strip object already exists, it is updated
            hostLabelText               => '',
            timeLabelText               => '',

            # Flags capturing keypresses. When these flags are TRUE, the key is held down; they're
            #   set to FALSE when the key is released (or when this window loses focus)
            # Flags set by $self->setKeyPressEvent and reset by ->setKeyReleaseEvent
            ctrlKeyFlag                 => FALSE,
            shiftKeyFlag                => FALSE,
            altKeyFlag                  => FALSE,
            altGrKeyFlag                => FALSE,
            # Flag set to FALSE if none of the CTRL, SHIFT, ALT and ALT-GR keys are held down; set
            #   to TRUE if one of those keys is held down
            modifierKeyFlag             => FALSE,

            # The actual window size (in pixels). $self->winEnable sets up a ->signal_connect to
            #   react when the window size changes, but the same ->signal_connect fires when (for
            #   example) text is written to a Gtk2::TextView
            # The new size is stored in these IVs every time the ->signal_connect fires, so we can
            #   tell if the window's actual size has changed, or not
            actualWinWidth              => undef,
            actualWinHeight             => undef,
            # A flag set to TRUE when the window is maximised, then set back to FALSE when it is
            #   unmaximised. Required for drawing gauges correctly
            maximisedFlag               => FALSE,
            # A flag set to TRUE when the window receives keyboard focus, then back to FALSE when it
            #   loses the focus
            focusFlag                   => FALSE,
        };

        # Bless the object into existence
        bless $self, $class;

        return $self;
    }

    ##################
    # Methods

    # Standard window object functions

    sub winSetup {

        # Called by GA::Obj::Workspace->createGridWin or ->createSimpleGridWin
        # Creates the Gtk2::Window itself
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $title      - The window title. If 'undef' or an empty string, a default title will be
        #                   used
        #   $listRef    - Reference to a list of functions to call, just after the Gtk2::Window is
        #                   created (can be used to set up further ->signal_connects, if this
        #                   window needs them)
        #
        # Return values
        #   'undef' on improper arguments or if the window can't be opened
        #   1 on success

        my ($self, $title, $listRef, $check) = @_;

        # Local variables
        my $iv;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->winSetup', @_);
        }

        # Don't create a new window, if it already exists
        if ($self->enabledFlag) {

            return undef;
        }

        # Create the Gtk2::Window
        my $winWidget = Gtk2::Window->new('toplevel');
        if (! $winWidget) {

            return undef;

        } else {

            # Store the IV now, as subsequent code needs it
            $self->ivPoke('winWidget', $winWidget);
            $self->ivPoke('winBox', $winWidget);
        }

        # Set up ->signal_connects (other ->signal_connects are set up in the call to
        #   $self->winEnable() )
        $self->setDeleteEvent();            # 'delete-event'
        $self->setKeyPressEvent();          # 'key-press-event'
        $self->setKeyReleaseEvent();        # 'key-release-event'
        # Set up ->signal_connects specified by the calling function, if any
        if ($listRef) {

            foreach my $func (@$listRef) {

                $self->$func();
            }
        }

        # Set the window title. If $title wasn't specified, use a suitable default title
        if (! $title) {

            if ($self->winType eq 'main') {

                $title = $axmud::SCRIPT;

            } elsif ($self->winName) {

                $title = $self->winName;

            } else {

                # Emergency fallback - $self->winName should be set
                $title = 'Untitled window';
            }
        }

        $winWidget->set_title($title);

        # Set the window's default size and position (this will almost certainly be changed before
        #   the call to $self->winEnable() )
        if ($self->winType eq 'main') {

            $winWidget->set_default_size(
                $axmud::CLIENT->customMainWinWidth,
                $axmud::CLIENT->customMainWinHeight,
            );

            $winWidget->set_border_width($axmud::CLIENT->constMainBorderPixels);

            # When workspace grids are disabled, 'main' windows should appear in the middle of the
            #   desktop
            if (! $self->workspaceObj->gridEnableFlag) {

                $winWidget->set_position('center');
            }

        } else {

            $winWidget->set_default_size(
                $axmud::CLIENT->customGridWinWidth,
                $axmud::CLIENT->customGridWinHeight,
            );

            $winWidget->set_border_width($axmud::CLIENT->constGridBorderPixels);
        }

        # Set the icon list for this window
        $iv = $self->winType . 'WinIconList';
        $winWidget->set_icon_list($axmud::CLIENT->desktopObj->$iv);

        # Draw the widgets used by this window
        if (! $self->drawWidgets()) {

            return undef;
        }

        # The calling function can now move the window into position, before calling
        #   $self->winEnable to make it visible, and to set up any more ->signal_connects()
        return 1;
    }

    sub winEnable {

        # Called by GA::Obj::Workspace->createGridWin or ->createSimpleGridWin
        # After the Gtk2::Window has been setup and moved into position, makes it visible and calls
        #   any further ->signal_connects that must be not be setup until the window is visible
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $listRef    - Reference to a list of functions to call, just after the Gtk2::Window is
        #                   created (can be used to set up further ->signal_connects, if this
        #                   window needs them)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 on success

        my ($self, $listRef, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->winEnable', @_);
        }

        # Make the window appear on the desktop
        $self->winShowAll($self->_objClass . '->winEnable');
        $self->ivPoke('enabledFlag', TRUE);

        # For windows about to be placed on a grid, briefly minimise the window so it doesn't
        #   appear in the centre of the desktop before being moved to its correct workspace, size
        #   and position
        if ($self->workspaceGridObj && $self->winWidget eq $self->winBox) {

            $self->winWidget->iconify();
        }

        # Set up ->signal_connects that must not be set up until the window is visible
        $self->setCheckResizeEvent();       # 'check-resize'
        $self->setWindowStateEvent();       # 'window-state-event'
        $self->setFocusInEvent();           # 'focus-in-event'
        $self->setFocusOutEvent();          # 'focus-out-event'
        # Set up ->signal_connects specified by the calling function, if any
        if ($listRef) {

            foreach my $func (@$listRef) {

                $self->$func();
            }
        }

        return 1;
    }

    sub winDestroy {

        # Called by GA::Obj::WorkspaceGrid->stop or by any other function
        # Informs the window's strip objects of their imminent demise, informs the parent workspace
        #   grid (if this 'grid' window is on a workspace grid) and the desktop object, and then
        #   destroys the Gtk2::Window (if it is open)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the window can't be destroyed or if it has already
        #       been destroyed
        #   1 on success

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->winDestroy', @_);
        }

        if (! $self->winBox) {

            # Window already destroyed in a previous call to this function
            return undef;
        }

        # Inform this window's strip objects of their imminent demise (in the order in which they
        #   were created
        foreach my $stripObj (sort {$a->number <=> $b->number} ($self->ivValues('stripHash'))) {

            $stripObj->objDestroy();
        }

        # Close any 'free' windows for which this window is a parent
        foreach my $winObj ($self->ivValues('childFreeWinHash')) {

            $winObj->winDestroy();
        }

        # Inform the parent workspace grid object (if any)
        if ($self->workspaceGridObj) {

            $self->workspaceGridObj->del_gridWin($self);
        }

        # Inform the desktop object
        $axmud::CLIENT->desktopObj->del_gridWin($self);

        # Destroy the Gtk2::Window
        eval { $self->winBox->destroy(); };
        if ($@) {

            # Window can't be destroyed
            return undef;

        } else {

            $self->ivUndef('winWidget');
            $self->ivUndef('winBox');
        }

        # Inform the ->owner, if there is one
        if ($self->owner) {

            $self->owner->del_winObj($self);
        }

        return 1;
    }

    sub winDisengage {

        # Called by GA::Obj::Desktop->removeSessionWindows and GA::Obj::WorkspaceGrid->stop
        # Removes this window object from its workspace grid, but does not close the window
        # Should not be called, in general
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $session    - When called by the functions listed above, the GA::Session which is
        #                   closing; otherwise 'undef'
        #
        # Return values
        #   'undef' on improper arguments or if the window can't be disengaged
        #   1 on success

        my ($self, $session, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->winDisengage', @_);
        }

        # Close any 'free' windows for which this window is a parent (but not when a session is
        #   closing; in that case, only close 'free' windows connected to that session)
        foreach my $winObj ($self->ivValues('childFreeWinHash')) {

            if (! $session || ($winObj->session && $winObj->session eq $session)) {

                $winObj->winDestroy();
            }
        }

        # Inform the parent workspace grid object (if any)
        if ($self->workspaceGridObj) {

            $self->workspaceGridObj->del_gridWin($self);
        }

        # Update IVs
        $self->ivUndef('workspaceGridObj');
        $self->ivUndef('areaObj');
        $self->ivUndef('session');
        if ($self->owner && $session && $self->owner eq $session) {

            $self->ivUndef('owner');
        }

        if ($self->visibleSession && $session && $self->visibleSession eq $session) {

            # Don't close tabs - assume that GA::Session->close is about to apply a new winmap
            $self->ivUndef('visibleSession');
        }

        return 1;
    }

#   sub winShowAll {}           # Inherited from GA::Generic::Win

    sub drawWidgets {

        # Called by $self->winSetup (also by $self->resetWinmap)
        # Sets up the Gtk2::Window by drawing the strip objects and table objects specified by
        #   $self->winmap (the name of a GA::Obj::Winmap object)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the winmap can't be found or if any of the widgets are
        #       not drawn
        #   1 on success

        my ($self, $check) = @_;

        # Local variables
        my (
            $winmap, $winmapObj, $matchFlag, $count,
            @initList, @objList, @winzoneList,
            %checkHash,
        );

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->drawWidgets', @_);
        }

        # This function shouldn't be called by anything but $self->winSetup, but reset IVs
        #   anyway, just in case
        $self->ivUndef('packingBox');
        $self->ivEmpty('stripHash');
        $self->ivEmpty('firstStripHash');
        $self->ivPoke('stripCount', 0);
        $self->ivEmpty('stripList');
        $self->ivUndef('tableStripObj');

        # If no winmap was specified, use a default one
        if (! $self->winmap) {

            if ($self->winType eq 'main') {

                if ($axmud::CLIENT->activateGridFlag) {
                    $winmap = $axmud::CLIENT->defaultEnabledWinmap;
                } else {
                    $winmap = $axmud::CLIENT->defaultDisabledWinmap;
                }

            } else {

                $winmap = $axmud::CLIENT->defaultInternalWinmap;
            }

        } else {

            $winmap = $self->winmap;
        }

        $winmapObj = $axmud::CLIENT->ivShow('winmapHash', $winmap);
        if (! $winmapObj) {

            return undef;

        } else {

            # (Set this IV for the first time)
            $self->ivPoke('winmap', $winmapObj->name);
        }

        # Create a packing box
        my $packingBox;
        if ($winmapObj->orientation eq 'top' || $winmapObj->orientation eq 'bottom') {
            $packingBox = Gtk2::VBox->new(FALSE, 0);
        } else {
            $packingBox = Gtk2::HBox->new(FALSE, 0);
        }

        $self->winBox->add($packingBox);
        $packingBox->set_border_width(0);

        # Update IVs immediately
        $self->ivPoke('packingBox', $packingBox);

        # Check the list of strip objects specified by the winmap. It must contain the compulsory
        #   GA::Strip::Table object; if not, add that object to the beginning of the list (so it
        #   appears first; normally at the top of the window)
        @initList = $winmapObj->stripInitList;
        if (@initList) {

            do {

                my ($packageName, $hashRef);

                $packageName = shift @initList;
                $hashRef = shift @initList;

                if ($packageName eq 'Games::Axmud::Strip::Table') {

                    $matchFlag = TRUE;
                }

            } until ($matchFlag || ! @initList);
        }

        @initList = $winmapObj->stripInitList;
        if (! $matchFlag) {

            unshift(@initList, 'Games::Axmud::Strip::Table');
        }

        # Add each strip object in turn
        $count = 0;
        do {

            my ($packageName, $hashRef, $stripObj, $spacing);

            $packageName = shift @initList;
            $hashRef = shift @initList;

            $count++;

            # Strip objects must inherit from GA::Generic::Strip and must exist (in the case of
            #   strip objects loaded from a plugin)
            if (
                $packageName =~ m/^Games\:\:Axmud\:\:Strip\:\:/
                && $axmud::CLIENT->ivExists('customStripHash', $packageName)
            ) {
                $stripObj = $packageName->new($self->stripCount, $self, %$hashRef);
                if ($stripObj) {

                    # Some strip objects are 'jealous' (only one can be opened per window)
                    if ($stripObj->jealousyFlag) {

                        # If another strip object of this type has already been created, discard the
                        #   newest one
                        if (exists $checkHash{$packageName}) {

                            $stripObj = undef;
                        }
                    }

                    # Some strip objects can't be added in Axmud blind mode
                    if ($stripObj && ! $stripObj->blindFlag && $axmud::BLIND_MODE_FLAG) {

                        $stripObj = undef;
                    }

                    # This strip object can be added
                    if ($stripObj && $stripObj->objEnable($winmapObj)) {

                        $checkHash{$packageName} = undef;

                        if ($stripObj->packingBox) {

                            if ($stripObj->allowFocusFlag) {
                                $stripObj->packingBox->can_focus(TRUE);
                            } else {
                                $stripObj->packingBox->can_focus(FALSE);
                            }
                        }

                        # Update IVs
                        $self->ivIncrement('stripCount');
                        $self->ivPush('stripList', $stripObj);
                        $self->ivAdd('stripHash', $stripObj->number, $stripObj);
                        if (! $self->ivExists('firstStripHash', $stripObj->_objClass)) {

                            $self->ivAdd('firstStripHash', $stripObj->_objClass, $stripObj);
                        }

                        if (
                            $packageName eq 'Games::Axmud::Strip::Table'
                            && ! $self->tableStripObj
                        ) {
                            $self->ivPoke('tableStripObj', $stripObj);
                        }

                        # Set the spacing between this strip object and adjacent ones
                        if (! $stripObj->spacingFlag || $count == 1 || ! @initList) {
                            $spacing = 0;
                        } else {
                            $spacing = $self->stripSpacingPixels;
                        }

                        # Add the strip object to the packing box
                        if ($stripObj->visibleFlag) {

                            if (
                                $winmapObj->orientation eq 'top'
                                || $winmapObj->orientation eq 'left'
                            ) {
                                $packingBox->pack_start(
                                    $stripObj->packingBox,
                                    $stripObj->expandFlag,
                                    $stripObj->fillFlag,
                                    $spacing,
                                );

                            } else {

                                $packingBox->pack_end(
                                    $stripObj->packingBox,
                                    $stripObj->expandFlag,
                                    $stripObj->fillFlag,
                                    $spacing,
                                );
                            }
                        }

                        # Inform all existing strip objects of this strip object's birth
                        foreach my $otherStripObj ($self->stripList) {

                            if ($stripObj ne $otherStripObj) {

                                $stripObj->notify_addStripObj($otherStripObj);
                            }
                        }
                    }
                }
            }

        } until (! @initList);

        # Now draw table objects on the GA::Strip::Table, using the layout specified by the winmap's
        #   winzones. We assume that the winmap has already checked that its winzones have valid
        #   sizes and don't overlap each other
        @winzoneList = sort {$a->number <=> $b->number} ($winmapObj->ivValues('zoneHash'));
        foreach my $winzoneObj (@winzoneList) {

            $self->tableStripObj->addTableObj(
                $winzoneObj->packageName,
                $winzoneObj->left,
                $winzoneObj->right,
                $winzoneObj->top,
                $winzoneObj->bottom,
                $winzoneObj->objName,
                $winzoneObj->initHash,
            );
        }

        # Sensitise/desensitise widgets according to current conditions
        $self->restrictMenuBars();
        $self->restrictToolbars();

        return 1;
    }

    sub redrawWidgets {

        # Can be called by anything
        # Resets the Gtk2::Window by re-packing the strip objects in $self->stripList
        # When called by $self->addStripObj, the new strip object will be packed for the first time.
        #   When called by $self->removeStripObj, the removed strip object won't be packed at all
        #
        # NB This is a legacy function that probably should not be called at all. Due to Gtk
        #   performance issues, it's nearly always better to call $self->hideStripObj,
        #   ->revealStripObj or ->replaceStripObj instead
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 on success

        my ($self, $check) = @_;

        # Local variables
        my (
            $winmapObj, $gaugeStripObj,
            @modList,
        );

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->redrawWidgets', @_);
        }

        # Get the winmap object specified by $self->winmap, or a default winmap, if $self->winmap
        #   is 'undef'
        $winmapObj = $self->getWinmap();

        # Empty the packing box of existing strips
        foreach my $child ($self->packingBox->get_children()) {

            $self->packingBox->remove($child);
        }

        # Get a list of existing strips that are actually visible
        foreach my $stripObj ($self->stripList) {

            if ($stripObj->visibleFlag) {

                push (@modList, $stripObj);
            }
        }

        # Re-pack all visible strips, leaving a gap between strips that aren't at the beginning or
        #   end of the list
        if (@modList) {

            for (my $count = 0; $count < (scalar @modList); $count++) {

                my ($stripObj, $spacing);

                $stripObj = $modList[$count];
                if (! $stripObj->spacingFlag || $count == 0 || $count == (scalar @modList - 1)) {
                    $spacing = 0;
                } else {
                    $spacing = $self->stripSpacingPixels;
                }

                if (
                    $winmapObj->orientation eq 'top'
                    || $winmapObj->orientation eq 'left'
                ) {
                    $self->packingBox->pack_start(
                        $stripObj->packingBox,
                        $stripObj->expandFlag,
                        $stripObj->fillFlag,
                        $spacing,
                    );

                } else {

                    $self->packingBox->pack_end(
                        $stripObj->packingBox,
                        $stripObj->expandFlag,
                        $stripObj->fillFlag,
                        $spacing,
                    );
                }
            }
        }

        # Sensitise/desensitise widgets according to current conditions
        $self->restrictMenuBars();
        $self->restrictToolbars();

        # Any Gtk2::TextViews will now be using the colours of the most recently-created
        #   Gtk2::TextView, not the colours that we want them to use. Update colours for all pane
        #   objects (GA::Table::Pane) before the forthcoming call to ->winShowAll
        $self->updateColourScheme(undef, TRUE);

        # Make everything visible
        $self->winShowAll($self->_objClass . '->redrawWidgets');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->redrawWidgets');

        # Adding/removing widgets upsets the position of the scrollbar in each tab's textview.
        #   Make sure all the textviews are scrolled to the bottom
        $self->rescrollTextViews();

        # Redraw any visible gauges, otherwise the gauge box will be visible, but the gauges
        #   themselves will have disappeared
        $gaugeStripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::GaugeBox');
        if ($gaugeStripObj && $gaugeStripObj->visibleFlag) {

            $gaugeStripObj->updateGauges();
            # (Need to call this a second time, or the re-draw doesn't work...)
            $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->redrawWidgets');
        }

        return 1;
    }

    # ->signal_connects

    sub setDeleteEvent {

        # Called by $self->winSetup
        # Set up a ->signal_connect to watch out for the user manually closing the 'internal' window
        # If it's a 'main' window and there are no 'main' windows left, halt the client
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->setDeleteEvent', @_);
        }

        if ($self->winType eq 'main' && $axmud::CLIENT->shareMainWinFlag) {

            $self->winBox->signal_connect('delete-event' => sub {

                # Prompt the user for confirmation, if required
                if (! $self->checkConnectedSessions()) {

                    # Don't close the 'main' window
                    return 1;
                }

                # Inform this window's strip objects of their imminent demise (in the order in which
                #   they were created
                foreach my $stripObj (
                    sort {$a->number <=> $b->number} ($self->ivValues('stripHash'))
                ) {
                    $stripObj->objDestroy();
                }

                # Close any 'free' windows for which this window is a parent
                foreach my $winObj ($self->ivValues('childFreeWinHash')) {

                    $winObj->winDestroy();
                }

                # Inform the parent workspace grid object (if any)
                if ($self->workspaceGridObj) {

                    $self->workspaceGridObj->del_gridWin($self);
                }

                # Inform the desktop object
                $axmud::CLIENT->desktopObj->del_gridWin($self);

                # Halt the client
                $axmud::CLIENT->stop();

                # Allow Gtk2 to close the window directly
                return undef;
            });

        } else {

            $self->winBox->signal_connect('delete-event' => sub {

                # Prompt the user for confirmation, if required
                if ($self->winType eq 'main' && ! $self->checkConnectedSessions()) {

                    # Don't close the 'main' window
                    return 1;
                }

                # Prevent Gtk2 from taking action directly. Instead redirect the request to
                #   $self->winDestroy, which does things like resetting a portion of the workspace
                #   grid, as well as actually destroying the window
                return $self->winDestroy();
            });
        }

        return 1;
    }

    sub setKeyPressEvent {

        # Called by $self->winSetup
        # Set up a ->signal_connect to watch out for certain key presses
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the ->signal_connect doesn't interfere with the key
        #       press
        #   1 if the ->signal_connect does interfere with the key press, or when the
        #       ->signal_connect is first set up

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->setKeyPressEvent', @_);
        }

        $self->winBox->signal_connect('key-press-event' => sub {

            my ($widget, $event) = @_;

            # Local variables
            my (
                $keycode, $standard, $stripObj, $paneObj, $tabObj, $splitScreenMode, $textView,
                $slice, $clipboard, $vAdjust, $high, $length, $modValue, $string, $entryText,
                $bufferObj, $startIter, $endIter,
                @list,
            );

            # Get the system keycode for this keypress
            $keycode = Gtk2::Gdk->keyval_name($event->keyval);

            # Translate it into a standard Axmud keycode
            $standard = $axmud::CLIENT->currentKeycodeObj->reverseKeycode($keycode);
            if (! $standard) {

                # Not a standard Axmud keycode (which includes all letters and numbers), so just
                #   use the system keycode
                $standard = $keycode;
            }

            # If it's a CTRL, SHIFT, ALT or ALT-GR keypress, set IVs
            if ($standard eq 'ctrl') {

                $self->ivPoke('ctrlKeyFlag', TRUE);
                $self->ivPoke('modifierKeyFlag', TRUE);

            } elsif ($standard eq 'shift') {

                $self->ivPoke('shiftKeyFlag', TRUE);
                $self->ivPoke('modifierKeyFlag', TRUE);

            } elsif ($standard eq 'alt') {

                $self->ivPoke('altKeyFlag', TRUE);
                $self->ivPoke('modifierKeyFlag', TRUE);

            } elsif ($standard eq 'alt_gr') {

                $self->ivPoke('altGrKeyFlag', TRUE);
                $self->ivPoke('modifierKeyFlag', TRUE);
            }

            # Now, create a keycode string, containing a sequence of keycodes (for example, the F5
            #   key creates a keycode string containing a single keycode - 'F5' - but CTRL+SHIFT+F5
            #   produces the keycode string 'ctrl shift f5')
            if ($self->ctrlKeyFlag) {

                if ($standard ne 'ctrl') {

                    push (@list, 'ctrl');
                }
            }

            if ($self->shiftKeyFlag) {

                if ($standard ne 'shift') {

                    push (@list, 'shift');
                }
            }

            if ($self->altKeyFlag) {

                if ($standard ne 'alt') {

                    push (@list, 'alt');
                }
            }

            if ($self->altGrKeyFlag) {

                if ($standard ne 'alt_gr') {

                    push (@list, 'alt_gr');
                }
            }

            push (@list, $standard);

            # (Below, use a keycode string, consisting of one or more standard keycodes
            #   separated by a space)
            $string = '' . join(' ', @list);

            # We don't want to call GA::Session->checkMacros for every keypress (that would be
            #   inefficient)
            # Instead, call it if this is a 'main' window, and if this keycode string is being used
            #   by any macro in any session (not perfectly efficient, but better)
            if (
                $self->visibleSession
                && $axmud::CLIENT->ivExists('activeKeycodeHash', $string)
                && $self->visibleSession->checkMacros($string)
            ) {
                # Return 1 to show that we have interfered with this keypress (by firing a macro)
                return 1;
            }

            # If no macro fired as a result of the keypress, then we can process some special keys

            # Get the entry strip object; the first pane object in its ->paneObjList is the one
            #   to which keypresses are applied
            $stripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::Entry');
            if ($stripObj) {

                $paneObj = $stripObj->ivFirst('paneObjList');
                if ($paneObj) {

                    # Get the pane's visible tab and the scrollable Gtk2::Textview
                    $tabObj = $paneObj->getVisibleTab();
                    if ($tabObj) {

                        $splitScreenMode = $tabObj->textViewObj->splitScreenMode;
                        if ($splitScreenMode eq 'split') {
                            $textView = $tabObj->textViewObj->textView2;
                        } else {
                            $textView = $tabObj->textViewObj->textView;
                        }
                    }
                }
            }

            # The CTRL+C combination tends to be CTRL+SHIFT+C in Gtk, which is very inconvenient.
            #   Implement a quic-and-dirty CTRL+C instead
            if ($string eq 'ctrl c' && $tabObj) {

                ($startIter, $endIter) = $tabObj->textViewObj->buffer->get_selection_bounds();
                if (defined $endIter) {

                    $slice = $tabObj->textViewObj->buffer->get_slice($startIter, $endIter, FALSE);
                    if (defined $slice && $slice ne '') {

                        $clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
                        if ($clipboard) {

                            $clipboard->set_text($slice);

                            # Return 1 to show that we have interfered with this keypress
                            return 1;
                        }
                    }
                }

            # If the page up/page down/home/end keys have been pressed, scroll the vertical
            #   scrollbar containing the textview object
            } elsif (
                (
                    $standard eq 'kp_page_up' || $standard eq 'page_up'
                    || $standard eq 'kp_page_down' || $standard eq 'page_down'
                    || $standard eq 'kp_home' || $standard eq 'home'
                    || $standard eq 'kp_end' || $standard eq 'end'
                )
                && $tabObj
                && $axmud::CLIENT->useScrollKeysFlag
            ) {
                $vAdjust = $textView->get_vadjustment();

                # Get the lowest and highest vertical positions of the scrollbar, and the distance
                #   between them
                $high = ($vAdjust->upper - $vAdjust->page_size);
                $length = $high - $vAdjust->lower;
                $modValue = $vAdjust->value;

                # Set the new position of the vertical scrollbar (assuming one is visible)
                if ($length) {

                    if (
                        $axmud::CLIENT->autoSplitKeysFlag
                        && $modValue >= $high
                        && (
                            (
                                $splitScreenMode ne 'split'
                                && (
                                    $standard eq 'kp_page_up' || $standard eq 'page_up'
                                    || $standard eq 'kp_home' || $standard eq 'home'
                                )
                            ) || (
                                $splitScreenMode eq 'split'
                                && (
                                    $standard eq 'kp_page_down' || $standard eq 'page_down'
                                    || $standard eq 'kp_end' || $standard eq 'end'
                                )
                            )
                        )
                    ) {
                        # Engage/disengage the textview object's split screen mode, if necessary
                        $paneObj->toggleSplitScreen();

                    } else {

                        if ($standard eq 'kp_page_up' || $standard eq 'page_up') {

                            # Scroll up
                            if (! $axmud::CLIENT->smoothScrollKeysFlag) {
                                $modValue -= $vAdjust->page_size;
                            } else {
                                $modValue -= $vAdjust->page_increment;
                            }

                            if ($modValue < 0) {

                                $modValue = 0;
                            }

                        } elsif ($standard eq 'kp_page_down' || $standard eq 'page_down') {

                            # Scroll down
                            if (! $axmud::CLIENT->smoothScrollKeysFlag) {
                                $modValue += $vAdjust->page_size;
                            } else {
                                $modValue += $vAdjust->page_increment;
                            }

                            if ($modValue > $high) {

                                $modValue = $high;
                            }

                        } elsif ($standard eq 'kp_home' || $standard eq 'home') {

                            # Scroll to top
                            $modValue = 0;

                        } else {

                            # (End key) scroll to bottom
                            $modValue = $high;
                        }

                        $vAdjust->set_value($modValue);
                    }
                }

                # Return 1 to show that we have interfered with this keypress
                return 1;

            # If the up/down/tab arrow keys have been pressed and the client's auto-complete mode is
            #   on (i.e. set to 'auto'), apply auto-complete or navigate through command buffers
            # However, for CTRL+TAB, switch between sessions
            } elsif ($standard eq 'up' || $standard eq 'down' || $standard eq 'tab') {

                if (
                    $self->ctrlKeyFlag
                    && $standard eq 'tab'
                    && $self->visibleSession
                    && $paneObj
                    && $paneObj->notebook
                    && $axmud::CLIENT->useSwitchKeysFlag
                ) {
                    $paneObj->switchVisibleTab();

                    # Return 1 to show that we have interfered with this keypress
                    return 1;

                } elsif (
                    ! $self->modifierKeyFlag
                    && $self->visibleSession
                    && $stripObj
                    && $stripObj->entry
                    && $axmud::CLIENT->autoCompleteMode eq 'auto'
                    && $axmud::CLIENT->useCompleteKeysFlag
                ) {
                    $entryText = $stripObj->entry->get_text();
                    if (! defined $stripObj->originalEntryText) {

                        $stripObj->set_originalEntryText($entryText);
                    }

                    if ($standard eq 'tab') {

                        # Check the instruction/world command buffer for matching instructions/world
                        #   commands so we can auto-complete the instruction/world command displayed
                        #   in the command entry box
                        $bufferObj = $self->visibleSession->autoCompleteBuffer(
                            $entryText,
                            $stripObj->originalEntryText,
                        );

                    } else {

                        # Navigate through the instruction/world command buffer to find the
                        #   instruction/world command to display in the command entry box
                        $bufferObj = $self->visibleSession->navigateBuffer($standard);
                    }

                    if ($bufferObj) {

                        # Display this previous instruction in the command entry box, and select
                        #   all text
                        if ($axmud::CLIENT->autoCompleteType eq 'instruct') {
                            $stripObj->entry->set_text($bufferObj->instruct);
                        } else {
                            $stripObj->entry->set_text($bufferObj->cmd);
                        }

                        $stripObj->entry->grab_focus();

                    } else {

                        # The returned $bufferObj will be 'undef' (when GA::Client->autoCompleteMode
                        #   = 'auto') when the buffer contains nothing shorter than the text in the
                        #   command entry box
                        # $bufferObj will also be 'undef' if the buffer object is missing, for some
                        #   reason
                        $stripObj->entry->set_text('');
                    }

                    # Return 1 to show that we have interfered with this keypress
                    return 1;
                }
            }

            # Return 'undef' to show that we haven't interfered with this keypress
            return undef;
        });

        return 1;
    }

    sub setKeyReleaseEvent {

        # Called by $self->winSetup
        # Set up a ->signal_connect to watch out for certain key releases
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the ->signal_connect doesn't interfere with the key
        #       release
        #   1 if the ->signal_connect does interfere with the key release, or when the
        #       ->signal_connect is first set up

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->setKeyReleaseEvent', @_);
        }

        $self->winBox->signal_connect('key-release-event' => sub {

            my ($widget, $event) = @_;

            # Local variables
            my ($keycode, $standard);

            # Get the system keycode for this keypress
            $keycode = Gtk2::Gdk->keyval_name($event->keyval);
            # Translate it into a standard Axmud keycode
            $standard = $axmud::CLIENT->currentKeycodeObj->reverseKeycode($keycode);
            if ($standard) {

                # If it's a CTRL, SHIFT, ALT or ALT-GR keypress, set IVs
                if ($standard eq 'ctrl') {
                    $self->ivPoke('ctrlKeyFlag', FALSE);
                } elsif ($standard eq 'shift') {
                    $self->ivPoke('shiftKeyFlag', FALSE);
                } elsif ($standard eq 'alt') {
                    $self->ivPoke('altKeyFlag', FALSE);
                } elsif ($standard eq 'alt_gr') {
                    $self->ivPoke('altGrKeyFlag', FALSE);
                }

                if (
                    ! $self->ctrlKeyFlag
                    && ! $self->shiftKeyFlag
                    && ! $self->altKeyFlag
                    && ! $self->altGrKeyFlag
                ) {
                    $self->ivPoke('modifierKeyFlag', FALSE);
                }
            }

            # Return 'undef' to show that we haven't interfered with this keypress
            return undef;
        });

        return 1;
    }

    sub setCheckResizeEvent {

        # Called by $self->winEnable
        # Set up a ->signal_connect to watch out for changes in the window size
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->setCheckResizeEvent', @_);
        }

        $self->winBox->signal_connect('check-resize' => sub {

            my ($widget, $event) = @_;

            # Local variables
            my ($width, $height, $changeFlag, $stripObj);

            # Has the window size actually changed? (This callback fires not only when the window
            #   size changes, but when text is drawn in a Gtk2::TextView, and maybe for other things
            #   too)
            ($width, $height) = $self->winBox->get_size();
            if (
                ! defined $self->actualWinWidth            # Windows size checked for the first time
                || $width != $self->actualWinWidth
                || $height != $self->actualWinHeight
            ) {
                $self->ivPoke('actualWinWidth', $width);
                $self->ivPoke('actualWinHeight', $height);
                $changeFlag = TRUE;
            }

            # For 'main' windows, ask every session using this window to check the size of its
            #   default textview object and, if it has changed, to inform the server (if NAWS is
            #   turned on)
            # NB During a GA::Table::Pane operation to convert a simple tab into a normal tab,
            #   this signal fires before the IVs have been set, so don't do anything
            if ($self->visibleSession) {

                foreach my $session ($axmud::CLIENT->listSessions()) {

                    if (
                        $session->mainWin
                        && $session->mainWin eq $self
                        && ! $session->defaultTabObj->paneObj->tabConvertFlag
                    ) {
                        $session->checkTextViewSize();
                    }
                }
            }

            # If the 'main' window itself has changed size and the gauge box is visible, redraw
            #   gauges
            $stripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::GaugeBox');
            if ($stripObj && $stripObj->canvas) {

                $self->updateGauges();
            }
        });

        return 1;
    }

    sub setWindowStateEvent {

        # Called by $self->winEnable
        # Set up a ->signal_connect to watch out for when the window is maximised and unmaximised
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->setWindowStateEvent', @_);
        }

        $self->winBox->signal_connect('window-state-event' => sub {

            # When the window is maximised and unmaximised, gauges in the gauge box (if visible) are
            #   not redrawn properly. This block of code fixes that problem (but I don't know why it
            #   works)

            my ($widget, $event) = @_;

            # Local variables
            my ($state, $stripObj);

            $state = $event->changed_mask();
            $stripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::GaugeBox');

            if ($state =~ m/maximized/ && $stripObj && $stripObj->canvas) {

                $stripObj->updateGauges();
                $axmud::CLIENT->desktopObj->updateWidgets(
                    $self->_objClass . '->setWindowStateEvent',
                );

                $stripObj->updateGauges();

                # This code makes sure textview(s) in pane object(s) are scrolled to the bottom
                #   after un-maximising a window (otherwise, the scrollbar position moves
                #   confusingly)
                if (! $self->maximisedFlag) {

                    $self->ivPoke('maximisedFlag', TRUE);

                } else {

                    $self->ivPoke('maximisedFlag', FALSE);
                    $self->rescrollTextViews();
                }
            }
        });
    }

    sub setFocusInEvent {

        # Called by $self->winEnable
        # Set up a ->signal_connect to watch out for the 'internal' window receiving the focus,
        #   which should be redirected towards the GA::Strip::Entry object's Gtk2::Entry (if there
        #   is one)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->setFocusInEvent', @_);
        }

        $self->winBox->signal_connect('focus-in-event' => sub {

            my ($widget, $event) = @_;

            # Update IVs
            $self->ivPoke('focusFlag', TRUE);

            # Update the command entry box (if visible)
            my $stripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::Entry');
            if ($stripObj && $stripObj->entry) {

                $stripObj->entry->grab_focus();
            }

            # For 'main' windows, check active hook interfaces for all sessions using this window
            #   and fire hooks that are using the 'get_focus' hook event
            if ($self->visibleSession) {

                foreach my $session ($axmud::CLIENT->listSessions()) {

                    if ($session->mainWin && $session->mainWin eq $self) {

                        $session->checkHooks('get_focus');
                    }
                }
            }
        });

        return 1;
    }

    sub setFocusOutEvent {

        # Called by $self->winEnable
        # Set up a ->signal_connect to watch out for the 'internal' window losing the focus
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->setFocusInEvent', @_);
        }

        $self->winBox->signal_connect('focus-out-event' => sub {

            my ($widget, $event) = @_;

            # Update IVs
            $self->ivPoke('focusFlag', FALSE);

            # For 'main' windows, check active hook interfaces for all sessions using this window
            #   and fire hooks that are using the 'lose_focus' hook event
            if ($self->visibleSession) {

                foreach my $session ($axmud::CLIENT->listSessions()) {

                    if ($session->mainWin && $session->mainWin eq $self) {

                        $session->checkHooks('lose_focus');
                    }
                }
            }

            # When the window loses focus, we no longer track SHIFT, ALT, CTRL or NUM LOCK
            #   keypresses. Set IVs
            $self->ivPoke('shiftKeyFlag', FALSE);
            $self->ivPoke('altKeyFlag', FALSE);
            $self->ivPoke('altGrKeyFlag', FALSE);
            $self->ivPoke('ctrlKeyFlag', FALSE);
            $self->ivPoke('modifierKeyFlag', FALSE);
        });

        return 1;
    }

    # Other functions

    sub updateColourScheme {

        # Called by GA::Cmd::UpdateColourScheme->do (usually after a colour scheme is modified) and
        #   by GA::Cmd::SetXTerm->do (when the xterm colour cube is switched)
        # Also called by $self->redrawWidgets, to neutralise Gtk2's charming tendency to redraw all
        #   our textviews the wrong colour
        # Checks all textview objects in all pane objects in this window's Gtk2::Table and updates
        #   colours for any textview objects that use the specified colour scheme
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $colourScheme   - The name of the colour scheme (matches a key in
        #                       GA::Client->colourSchemeHash). If 'undef', all pane objects are
        #                       updated using their existing colour scheme
        #   $noDrawFlag     - TRUE when called by $self->redrawWidgets, meaning that the pane
        #                       objects are told not to call GA::Win::Generic->winShowAll or
        #                       GA::Obj::Desktop->updateWidgets as they normally would, so that
        #                       ->redrawWidgets can do it when ready. FALSE (or 'undef') otherwise
        #
        # Return values
        #   'undef' on improper arguments
        #   1 on success

        my ($self, $colourScheme, $noDrawFlag, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->updateColourScheme', @_);
        }

        # If $colourScheme was specified, check it's a recognised colour scheme
        if (defined $colourScheme) {

            if (! $axmud::CLIENT->ivExists('colourSchemeHash', $colourScheme)) {

                # Update all textview objects
                $colourScheme = undef;
            }
        }

        foreach my $tableObj ($self->tableStripObj->ivValues('tableObjHash')) {

            if ($tableObj->type eq 'pane') {

                $tableObj->updateColourScheme($colourScheme, $noDrawFlag);
            }
        }

        return 1;
    }

    sub applyColourScheme {

        # Called by GA::Cmd::ApplyColourScheme->do
        # Applies a colour scheme to all textview objects in all pane objects in this window's
        #   Gtk2::Table, replacing any colour schemes used before
        #
        # Expected arguments
        #   $colourScheme   - The name of the colour scheme to apply (matches a key in
        #                       GA::Client->colourSchemeHash)
        #
        # Return values
        #   'undef' on improper arguments or if $colourScheme doesn't exist
        #   1 on success

        my ($self, $colourScheme, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->applyColourScheme', @_);
        }

        # Check that $colourScheme exists
        if (! $axmud::CLIENT->ivExists('colourSchemeHash', $colourScheme)) {

            return undef;
        }

        foreach my $tableObj ($self->tableStripObj->ivValues('tableObjHash')) {

            if ($tableObj->type eq 'pane') {

                $tableObj->applyColourScheme(
                    undef,                  # Apply to all tabs in the pane object
                    $colourScheme,
                );
            }
        }

        return 1;
    }

    sub rescrollTextViews {

        # Can be called by anyting
        # After a change in position of any window widgets, make sure any textview(s) in pane
        #   object(s) are scrolled to the bottom (this saves a lot of user frustration)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if this isn't the 'main' window
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $stripObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->rescrollTextViews', @_);
        }

        foreach my $tableObj ($self->tableStripObj->ivValues('tableObjHash')) {

            if ($tableObj->type eq 'pane') {

                foreach my $tabObj ($tableObj->ivValues('tabObjHash')) {

                    $tabObj->textViewObj->scrollToBottom();
                }
            }
        }

        # After drawing gauges or unmaximising the window, the focus is lost from the command entry
        #   box, so restore it
        $stripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::Entry');
        if ($stripObj && $stripObj->entry) {

            $stripObj->entry->grab_focus();
        }

        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->rescrollTextViews');

        return 1;
    }

    sub findTableObj {

        # Can be called by anything
        # Finds the first table object of a particular type created in the compulsory table strip
        #   object, if there is one
        #
        # Expected arguments
        #   $type   - The type of table object; matches the table object's type (e.g. 'pane' for
        #               pane objects)
        #
        # Return values
        #   'undef' on improper arguments or if there is no table object of the specified type in
        #       the Gtk2::Table
        #   Otherwise returns the table object of the specified type with the lowest ->number

        my ($self, $type, $check) = @_;

        # Local variables
        my @tableObjList;

        # Check for improper arguments
        if (! defined $type || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->findFirstTableObj', @_);
        }

        @tableObjList = sort {$a->number <=> $b->number}
                                ($self->tableStripObj->ivValues('tableObjHash'));

        foreach my $tableObj (@tableObjList) {

            if ($tableObj->type eq $type) {

                return $tableObj;
            }
        }

        # No table object of the right $type found
        return undef;
    }

    sub setVisibleSession {

        # Called by GA::Table::Pane->respondVisibleTab, when a session's default tab becomes the
        #   visible tab in that pane. Only called when this window is a 'main' window
        # Should not be called by anything else - call GA::Table::Pane->setVisibleTab instead
        #
        # Sets $self->visibleSession and performs a number of housekeeping duties
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $session    - The GA::Session that is the new visible session. If 'undef', there is no
        #                   visible session in this window
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $session, $check) = @_;

        # Local variables
        my ($tabObj, $paneObj, $visibleTabObj);

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->setVisibleSession', @_);
        }

        # This function shouldn't be called unless this is a 'main' window
        if ($self->winType ne 'main') {

            return $self->writeError(
                'Cannot set a visible session outside a \'main\' window',
                $self->_objClass . '->setVisibleSession',
            );
        }

        # Cease navigating through instruction/world command buffers in all windows (if the user has
        #   been doing that using the 'up'/'down' arrow keys in any window)
        $axmud::CLIENT->set_instructBufferPosn();
        $axmud::CLIENT->set_cmdBufferPosn();
        foreach my $otherSession ($axmud::CLIENT->listSessions()) {

            $otherSession->set_instructBufferPosn();
            $otherSession->set_cmdBufferPosn();
        }

        # No visible session
        if (! $session) {

            # Update certain strip objects in any 'internal' window used by the old visible session
            #   (if any; the TRUE flag means 'return 'internal' windows only)
            foreach my $winObj (
                $axmud::CLIENT->desktopObj->listSessionGridWins($self->visibleSession, TRUE)
            ) {
                # Update information stored in the window's connection info strip, if visible
                $winObj->setHostLabel('');
                $winObj->setTimeLabel('');
                # Reset the window's entry box and blinkers, if visible
                $winObj->resetEntry();
                $winObj->resetBlinkers();
            }

            # Update the GA::Client's IVs
            if (
                $axmud::CLIENT->currentSession
                && $self->visibleSession
                && $axmud::CLIENT->currentSession eq $self->visibleSession
            ) {
                $axmud::CLIENT->setCurrentSession();
            }

            # Update this window's IVs
            $self->ivUndef('visibleSession');
            if ($self->winType eq 'main') {

                $self->ivUndef('workspaceGridObj');
            }

        # New visible session
        } else {

            # If this window is a 'main' window shared by all sessions...
            if (
                $axmud::CLIENT->shareMainWinFlag
                && $self->visibleSession
                && $self->visibleSession ne $session
            ) {
                # ...need to hide windows on the former visible session's workspace grids (though
                #   obviously not the shared 'main' window)
                $axmud::CLIENT->desktopObj->hideGridWins($self->visibleSession);
                # The new visible session's windows, on the other hand, need to be un-hidden
                $axmud::CLIENT->desktopObj->revealGridWins($session);

                # Fire any hooks that are using the 'not_visible' hook event
                $self->visibleSession->checkHooks('not_visible', $session->number);
            }

            # Update the GA::Client's IVs
            # ->currentSession is modified only when a 'main' window that's in focus changes its
            #   ->visibleSession (exception - if the IV isn't set at all, set it regardless of
            #   whether this window has the focus, or not)
            if (! $axmud::CLIENT->currentSession || $self->focusFlag) {

                $axmud::CLIENT->setCurrentSession($session);
            }

            # Update this window's IVs
            $self->ivPoke('visibleSession', $session);
            # For 'main' windows only, find the workspace grid used by this session (might be a
            #   shared grid), and update the IV
            if ($self->winType eq 'main' && $self->workspaceObj) {

                # (In case the grid can't be found, use a default 'undef' value)
                $self->ivUndef('workspaceGridObj');

                OUTER: foreach my $gridObj (
                    sort {$a->number <=> $b->number}
                    ($self->workspaceObj->ivValues('gridHash'))
                ) {
                    if (
                        ! defined $gridObj->owner       # Shared workspace grid
                        || $gridObj->owner eq $session
                    ) {
                        $self->ivPoke('workspaceGridObj', $gridObj);
                        last OUTER;
                    }
                }
            }

            # In case the change of visible session isn't the result of the user clicking a
            #   session's default tab, make the new visible session's default tab the visible one.
            #   If that default tab is a simple tab, then there's nothing to do
            # ($session->defaultTabObj won't be set yet, if $session->start is still executing)
            if ($session->defaultTabObj) {

                $paneObj = $session->defaultTabObj->paneObj;
                $tabObj = $paneObj->findSession($session);
                $visibleTabObj = $paneObj->getVisibleTab();
                if ($paneObj->notebook && $tabObj && $visibleTabObj && $tabObj ne $visibleTabObj) {

                    $paneObj->setVisibleTab($tabObj);
                }
            }

            # If the session's tab label is in a different colour, meaning that text had been
            #   received from the world but hadn't been viewed by the user yet, reset the flag to
            #   show that the text is now visible to the user
            $session->reset_showNewTextFlag();

            # Update information stored in the 'main' window's connection info strip, if visible
            $self->setHostLabel($session->getHostLabelText());
            $self->setTimeLabel($session->getTimeLabelText());

            # Update certain strip objects in any 'internal' window used by the new visible session
            #   (if any; the TRUE flag means 'return 'internal' windows only)
            foreach my $winObj ($axmud::CLIENT->desktopObj->listSessionGridWins($session, TRUE)) {

                # Reset the 'internal' window's entry box and blinkers, if any
                $winObj->resetEntry();
                $winObj->resetBlinkers();
            }

            # Fire any hooks that are using the 'visible_session' hook event
            $session->checkHooks('visible_session', undef);
            # Fire any hooks that are using the 'change_visible' hook event
            foreach my $otherSession ($axmud::CLIENT->listSessions()) {

                if ($otherSession ne $session) {

                    $otherSession->checkHooks('change_visible', $session->number);
                }
            }
        }

        # Update all 'internal' windows
        foreach my $winObj ($axmud::CLIENT->desktopObj->listGridWins()) {

            if (
                $winObj->winType eq 'main'
                || $winObj->winType eq 'protocol'
                || $winObj->winType eq 'custom'
            ) {
                # Sensitise/desensitise menu items in 'internal' windows, as appropriate
                $winObj->restrictMenuBars();
                $winObj->restrictToolbars();
                # Update the gauge box, if visible
                $winObj->updateGauges();

                # Sensitise or desensitise widgets, as appropriate
                $winObj->setWidgetsIfSession();
                # Update widgets, as appropriate
                $winObj->setWidgetsChangeSession();

                # Make any changes visible
                $winObj->winShowAll($self->_objClass . '->setVisibleSession');
            }
        }

        return 1;
    }

    sub checkConnectedSessions {

        # Called by $self->setDeleteEvent
        # If the user tries to manually close a 'main' window, counts the number of connected
        #   sessions (not including disconencted or 'connect offline' mode sessions) using this
        #   window as their 'main' window
        # If any are found, prompts the user for confirmation (if the GA::Client flag requires us
        #   to do that)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the window should not be closed
        #   1 if the window can be closed

        my ($self, $check) = @_;

        # Local variables
        my ($count, $choice, $msg);

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->checkConnectedSessions',
                @_,
            );
        }

        if (! $axmud::CLIENT->confirmCloseMainWinFlag) {

            # The window can be closed - no confirmation required
            return 1;
        }

        # Otherwise, count connected sessions
        $count = 0;
        foreach my $session ($axmud::CLIENT->ivValues('sessionHash')) {

            if (
                $session->mainWin
                && $session->mainWin eq $self
                && $session->status eq 'connected'
            ) {
                $count++;
            }
        }

        if ($count) {

            if ($count == 1) {
                $msg = '1 connected session';
            } else {
                $msg = $count . ' connected sessions';
            }

            $choice = $self->showMsgDialogue(
                'Close \'main\' window',
                'question',
                'This window is in use by ' . $msg . '. Are you sure you want to close it?',
                'yes-no',
            );

            if ($choice && $choice eq 'yes') {

                # Allow the window to close
                return 1;

            } else {

                # Don't allow the window to close
                return undef;
            }

        } else {

            # No connected sessions, allow the window to close
            return 1;
        }
    }

    sub updateGauges {

        # Called by various functions as a shortcut to GA::Strip::GaugeBox->updateGauges
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $winmapName, $check) = @_;

        # Local variables
        my $stripObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->updateGauges', @_);
        }

        $stripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::GaugeBox');
        if ($stripObj) {

            $stripObj->updateGauges();
        }

        return 1;
    }

    sub resetEntry {

        # Called by GA::Client->set_autoCompleteMode, ->set_autoCompleteType and
        #   ->set_autoCompleteParent or any other code which needs to reset the entry box (for some
        #   reason)
        # The user can press the 'up'/'down' arrow keys to change the command displayed in the
        #   GA::Strip::Entry's command entry box
        # The new command depends on various client IVs. If those IVs are changed, the entry box
        #   must be reset
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $stripObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->resetEntry', @_);
        }

        $stripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::Entry');
        if ($stripObj) {

            $stripObj->set_originalEntryText();
            if ($stripObj->entry) {

                $stripObj->entry->set_text('');
            }
        }

        return 1;
    }

    sub setHostLabel {

        # Can be called by anything (but usually called by various functions in GA::Session, and
        #   also by $self->setVisibleSession)
        # Sets the text used in the GA::Strip::ConnectInfo strip object to display information
        #   about the current host (world)
        # If the strip object hasn't been added to the window, still store the text in
        #   $self->hostLabelText so it's available instantly if the strip object is added later
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $text       - The text to display in this label. If set to 'undef', an empty string is
        #                   displayed
        #   $tooltip    - The text to display as a tooltip. If set to 'undef', no tooltip is
        #                   displayed
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $text, $tooltip, $check) = @_;

        # Local variables
        my $stripObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setHostLabel', @_);
        }

        if (! $text) {

            $text = '';
        }

        $self->ivPoke('hostLabelText', $text);

        $stripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::ConnectInfo');
        if ($stripObj) {

            $stripObj->set_hostLabel($text, $tooltip);
        }

        return 1;
    }

    sub setTimeLabel {

        # Can be called by anything (but usually called by GA::Client->spinClientLoop, various
        #   functions in GA::Session, and also by $self->setVisibleSession)
        # Sets the text used in the GA::Strip::ConnectInfo strip object to display information about
        #   the current time
        # If the strip object hasn't been added to the window, still store the text in
        #   $self->timeLabelText so it's available instantly if the strip object is added later
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $text       - The text to display in this label. If set to 'undef', an empty string is
        #                   displayed
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $text, $check) = @_;

        # Local variables
        my $stripObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setTimeLabel', @_);
        }

        if (! $text) {

            $text = '';
        }

        $self->ivPoke('timeLabelText', $text);

        $stripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::ConnectInfo');
        if ($stripObj) {

            $stripObj->set_timeLabel($text);
        }

        return 1;
    }

    sub resetBlinkers {

        # Can be called by anything (but usually called by GA::Session->reactDisconnect and
        #   $self->setVisibleSession)
        # Resets the blinkers drawn in the GA::Strip::ConnectInfo strip object
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $stripObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->resetBlinkers', @_);
        }

        $stripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::ConnectInfo');
        if ($stripObj) {

            $stripObj->drawBlinker(-1, FALSE);
        }

        return 1;
    }

    sub setMainWinTitle {

        # Called by GA::Client->checkMainWinTitles to change each 'main' window's title to something
        #   like '*Axmud' to show that there are client files that need to be saved, or to something
        #   like 'Axmud' to show there are no client files that need to be saved
        # (Session files are handles separately by GA::Session->checkTabLabels)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $flag       - If set to TRUE, show an asterisk. If set to FALSE (or 'undef'), don't show
        #                   an asterisk
        #
        # Return values
        #   'undef' on improper arguments or if this window isn't a 'main' window
        #   1 otherwise

        my ($self, $flag, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setMainWinTitle', @_);
        }

        # Ignore this function call for other types of 'grid' window
        if ($self->winType eq 'main') {

            return undef;

        # Set the window title
        } elsif ($flag) {

            $self->winWidget->set_title('*' . $axmud::SCRIPT);

        } else {

            $self->winWidget->set_title($axmud::SCRIPT);
        }

        return 1;
    }

    sub setWinTitle {

        # Can be called by anything to change the text in the window's title bar
        # Does nothing if this window is a 'main' window (code should call $self->setMainWinTitle
        #   instead)
        #
        # Expected arguments
        #   $text       - The text to use
        #
        # Return values
        #   'undef' on improper arguments or if this window isn't a 'main' window
        #   1 otherwise

        my ($self, $text, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setWinTitle', @_);
        }

        if ($self->enabledFlag && $self->winType ne 'main' && $self->winWidget eq $self->winBox) {

            $self->winWidget->set_title($text);
        }

        return 1;
    }

    sub restrictMenuBars {

        # Called by GA::Obj::Desktop->restrictWidgets
        # Sensitise or desensitise the menu bar in this 'internal' window, depending on current
        #   conditions. (Don't do anything if this window doesn't use a strip object for a menu bar)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if this window doesn't use a strip object for a menu
        #       bar
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $stripObj, $openFlag,
            @list, @sensitiseList, @desensitiseList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->restrictMenuBars', @_);
        }

        # Get the strip object
        $stripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::MenuBar');
        if (! $stripObj) {

            # Nothing to sensitise/desensitise
            return undef;
        }

        # Test whether this is a 'main' window with a visible session whose status is 'connected' or
        #   'offline' (for the sake of efficiency)
        if (
            $self->visibleSession
            && (
                $self->visibleSession->status eq 'connected'
                || $self->visibleSession->status eq 'offline'
            )
        ) {
            $openFlag = TRUE;
        }

        # Menu bar items that require a 'main' window with a visible session
        @list = (
            # 'World' column
            'reconnect', 'reconnect_offline',
            'xconnect', 'xconnect_offline',
            'quit_all',
            'exit_all',
            'stop_session',
            # 'File' column
            'test_file',
            'show_files', 'show_file_meta',
            # 'Display' column
            'session_screenshot',
            # 'Axbasic' column
            'check_script', 'edit_script',
            # 'Plugins' column
            'load_plugin', 'show_plugin',
        );

        if ($self->visibleSession) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline'
        @list = (
            # 'World' column
            'login',
            'quit', 'qquit',
            'exit', 'xxit',
            # 'File' column
            'load_all', 'load_file',
            'save_all', 'save', 'save_options',
            'import_files',
            'export_all_files', 'export_file',
            'import_data',
            'export_data',
            'disable_world_save', 'disable_save_load',
            # 'Edit' column
            'edit_triggers', 'edit_aliases', 'edit_macros', 'edit_timers', 'edit_hooks',
            'active_interfaces',
            'edit_cmds', 'edit_routes',
            'simulate',
            'run_locator_wiz', 'edit_world_model',
            'edit_quick_prefs', 'edit_client_prefs', 'edit_session_prefs', 'edit_current_prof',
            # 'Tasks' column
            'freeze_tasks', 'chat_task',
            'run_locator_wiz_2',
            'other_task',
            # 'Display' column
            'open_automapper', 'open_object_viewer',
            'activate_grid', 'activate_grid_with', 'reset_grid', 'disactivate_grid',
            'win_components', 'current_layer',
            'test_controls', 'test_panels',
            # 'Commands' column
            'repeat_cmd', 'repeat_second', 'repeat_interval',
            'cancel_repeat',
            # 'Recordings' column
            'start_stop_recording', 'pause_recording',
            # 'Axbasic' column
            'run_script', 'run_script_task',
        );

        if ($openFlag) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and whose ->currentGuild is defined
        @list = (
            # 'Edit' column
            'edit_current_guild',
        );

        if ($openFlag && $self->visibleSession->currentGuild) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and whose ->currentRace is defined
        @list = (
            # 'Edit' column
            'edit_current_race',
        );

        if ($openFlag && $self->visibleSession->currentRace) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and whose ->currentChar is defined
        @list = (
            # 'Edit' column
            'edit_current_char',
        );

        if ($openFlag && $self->visibleSession->currentChar) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and $stripObj->saveAllSessionsFlag set to FALSE
        @list = (
            # 'File' column
            'save_file',
        );

        if ($openFlag && ! $stripObj->saveAllSessionsFlag) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and an Advance task
        @list = (
            # 'Tasks' column
            'edit_advance_task',
        );

        if ($openFlag && $self->visibleSession->advanceTask) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and an Attack task
        @list = (
            # 'Tasks' column
            'edit_attack_task',
        );

        if ($openFlag && $self->visibleSession->attackTask) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and a Chat task
        @list = (
            # 'Tasks' column
            'edit_chat_task',
        );

        if ($openFlag && $self->visibleSession->chatTask) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and a debugger task
        @list = (
            # 'Tasks' column
            'edit_debugger_task',
        );

        if ($openFlag && $self->visibleSession->debuggerTask) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and a TaskList task
        @list = (
            # 'Tasks' column
            'edit_task_list_task',
        );

        if ($openFlag && $self->visibleSession->taskListTask) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and a Compass task
        @list = (
            # 'Tasks' column
            'compass_task',
        );

        if ($openFlag && $self->visibleSession->compassTask) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and a Divert task
        @list = (
            # 'Tasks' column
            'divert_task',
        );

        if ($openFlag && $self->visibleSession->divertTask) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and an Inventory task
        @list = (
            # 'Tasks' column
            'inventory_task',
        );

        if ($openFlag && $self->visibleSession->inventoryTask) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and a Locator task
        @list = (
            # 'Tasks' column
            'locator_task',
        );

        if ($openFlag && $self->visibleSession->locatorTask) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and a Status task
        @list = (
            # 'Tasks' column
            'status_task',
        );

        if ($openFlag && $self->visibleSession->statusTask) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline' and a Watch task
        @list = (
            # 'Tasks' column
            'watch_task',
        );

        if ($openFlag && $self->visibleSession->watchTask) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require a require a 'main' window with a visible session whose status
        #   is 'connected' or 'offline' and GA::Session->recordingFlag
        @list = (
            # 'Recordings' column
            'pause_resume_recording',
            'recording_add_line', 'recording_add_break',
            'recording_set_insertion', 'recording_cancel_insertion',
            'recording_delete_line', 'recording_delete_multi', 'recording_delete_last',
            'show_recording', 'copy_recording',
        );

        if ($openFlag && $self->visibleSession->recordingFlag) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu bar items that require GA::Client->browserCmd
        @list = (
            # 'Help' column
            'go_website',
        );

        if ($axmud::CLIENT->browserCmd) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Sensitise/desensitise menu bar items
        $stripObj->sensitiseWidgets(@sensitiseList);
        $stripObj->desensitiseWidgets(@desensitiseList);

        # All menu items added by plugins require a 'main' window with a visible session, and the
        #   plugin itself must be must be enabled
        foreach my $plugin ($stripObj->ivKeys('pluginMenuItemHash')) {

            my ($widget, $pluginObj);

            $widget = $stripObj->ivShow('pluginMenuItemHash', $plugin);
            $pluginObj = $axmud::CLIENT->ivShow('pluginHash', $plugin);

            if ($self->visibleSession && $pluginObj->enabledFlag) {
                $widget->set_sensitive(TRUE);
            } else {
                $widget->set_sensitive(FALSE);
            }
        }

        return 1;
    }

    sub restrictToolbars {

        # Called by GA::Obj::Desktop->restrictWidgets
        # Sensitise or desensitise the toolbar in this 'internal' window, depending on current
        #   conditions. (Don't do anything if this window doesn't use a strip object for a toolbar)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if this window doesn't use a strip object for a toolbar
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $stripObj,
            @list, @sensitiseList, @desensitiseList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->restrictToolbars', @_);
        }

        # Get the strip object
        $stripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::Toolbar');
        if (! $stripObj) {

            # Nothing to sensitise/desensitise
            return undef;
        }

        # Toolbar buttons that require a 'main' window with a visible session
        @list = $stripObj->ivKeys('requireSessionHash');

        if ($self->visibleSession) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Toolbar buttons that require a 'main' window with a visible session whose status is
        #   'connected' or 'offline'
        @list = $stripObj->ivKeys('requireConnectHash');

        if (
            $self->visibleSession
            && (
                $self->visibleSession->status eq 'connected'
                || $self->visibleSession->status eq 'offline'
            )
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Sensitise/desensitise toolbar items
        $stripObj->sensitiseWidgets(@sensitiseList);
        $stripObj->desensitiseWidgets(@desensitiseList);

        return 1;
    }

    sub setWidgetsIfSession {

        # Called by $self->setVisibleSession
        # Calls each strip object (and, for GA::Strip::Table, any child table objects) so they can
        #   sensitise or desensitise their widgets, depending on whether this window has a
        #   ->visibleSession, or not
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $flag;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setWidgetsIfSession', @_);
        }

        if ($self->visibleSession) {
            $flag = TRUE;
        } else {
            $flag = FALSE;
        }

        foreach my $stripObj ($self->ivValues('stripHash')) {

            $stripObj->setWidgetsIfSession($flag);
        }

        return 1;
    }

    sub setWidgetsChangeSession {

        # Called by $self->setVisibleSession
        # Calls each strip object (and, for GA::Strip::Table, any child table objects) so they can
        #   update their widgets when any 'main' window's ->visibleSession changes
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->setWidgetsChangeSession',
                @_,
            );
        }

        foreach my $stripObj ($self->ivValues('stripHash')) {

            $stripObj->setWidgetsChangeSession();
        }

        return 1;
    }

    # Strip object functions

    sub resetWinmap {

        # Returns the window to its initial state by removing all table objects and strip objects,
        #   and applying the specified winmap (which might be the same as the previous one)
        # Useful for 'main' windows when the 'main_wait' winmap should be applied, or replaced
        #   after being replied
        # Probably not useful for other 'internal' windows, though if code needs to apply the
        #   equivalent 'internal_wait' winmap and, at some later time, replace it with something
        #   else, it can
        #
        # Expected arguments
        #   $winmapName     - The name of the new winmap
        #
        # Return values
        #   'undef' on improper arguments or if the specified winmap doesn't exist
        #   1 otherwise

        my ($self, $winmapName, $check) = @_;

        # Local variables
        my $winmapObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->resetWinmap', @_);
        }

        # Check the specified winmap exists
        $winmapObj = $axmud::CLIENT->ivShow('winmapHash', $winmapName);
        if (! $winmapObj) {

            return undef;
        }

        # Remove all strip objects (table objects in the compulsory Gtk2::Table are also destroyed)
        foreach my $stripObj ($self->ivValues('stripHash')) {

            $stripObj->objDestroy();

            if ($stripObj->visibleFlag) {

                $axmud::CLIENT->desktopObj->removeWidget($self->packingBox, $stripObj->packingBox);
            }
        }

        # Remove the Gtk2::HBox or Gtk2::VBox into which everything is packed
        $axmud::CLIENT->desktopObj->removeWidget($self->winBox, $self->packingBox);

        # Update IVs
        $self->ivUndef('packingBox');
        $self->ivEmpty('stripHash');
        $self->ivEmpty('firstStripHash');
        $self->ivPoke('stripCount', 0);
        $self->ivEmpty('stripList');
        $self->ivUndef('tableStripObj');

        $self->ivPoke('winmap', $winmapObj->name);

        # Set up the window with its strip objects and table objects as if the window had just been
        #   created
        $self->drawWidgets();
        $self->winShowAll($self->_objClass . '->resetWinmap');
        # This should already be set
        $self->ivPoke('enabledFlag', TRUE);

        return 1;
    }

    sub getWinmap {

        # Called by $self->addStripObj and ->revealStripObj
        # If $self->winmap is set (to the name of a winmap object), returns the winmap object
        #   (GA::Obj::Winmap) itself
        # Otherwise returns an appropirate default winmap object
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   Otherwise returns a winmap object (GA::Obj::Winmap)

        my ($self, $check) = @_;

        # Local variables
        my $winmapObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->getWinmap', @_);
        }

        $winmapObj = $axmud::CLIENT->ivShow('winmapHash', $self->winmap);
        if (! $winmapObj) {

            if ($self->winType eq 'main') {

                if ($axmud::CLIENT->activateGridFlag) {

                    $winmapObj = $axmud::CLIENT->ivShow(
                        'winmapHash',
                        $axmud::CLIENT->defaultEnabledWinmap,
                    );

                } else {

                    $winmapObj = $axmud::CLIENT->ivShow(
                        'winmapHash',
                        $axmud::CLIENT->defaultDisabledWinmap,
                    );
                }

            } else {

                $winmapObj = $axmud::CLIENT->ivShow(
                    'winmapHash',
                    $axmud::CLIENT->defaultInternalWinmap,
                );
            }
        }

        return $winmapObj;
    }

    sub addStripObj {

        # Can be called by anything (strip objects are also created by $self->drawWidgets() )
        # Adds a strip object (inheriting from GA::Generic::Strip) to the list of strip objects that
        #   can be displayed in this window (whether the Gtk widget is actually drawn, or not,
        #   depends on the value of the strip object's ->visibleFlag)
        #
        # Expected arguments
        #   $packageName    - The package name for the strip object to add, e.g.
        #                       GA::Strip::GaugeBox. This function can't be used to add the
        #                       compulsory GA::Strip::Table which already exists
        #
        # Optional arguments
        #   $index           - The new strip object's position in the window. Strip objects are
        #                       stored in $self->stripList, in the order in which they're drawn
        #                       (which could be top to bottom, bottom to top, left to right or right
        #                       to left). $index specifies the position in that list at which the
        #                       new strip object is inserted. If $index is 0, the strip object is
        #                       inserted at the beginning of the list. If $index is 1, it's inserted
        #                       second, if $index is 2, it's inserted third, and so on. If $index is
        #                       -1, it's inserted last. If $index is 'undef', or if its index is
        #                       outside the list, the strip object is inserted at the beginning of
        #                       the list
        #   %initHash        - If specified, a reference to a hash containing arbitrary data to use
        #                       as the strip object's initialisation settings (may be an empty
        #                       hash). The strip object should use default initialisation settings
        #                       unless it can succesfully interpret one or more of the key-value
        #                       pairs in the hash, if there are any)
        #
        # Return values
        #   'undef' on improper arguments or if the strip object can't be added
        #   Otherwise returns the strip object added

        my ($self, $packageName, $index, %initHash) = @_;

        # Local variables
        my ($winmapObj, $stripObj, $spacing, $posn, $gaugeStripObj);

        # Check for improper arguments
        if (! defined $packageName) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->addStripObj', @_);
        }

        # Get the winmap object specified by $self->winmap, or a default winmap, if $self->winmap
        #   is 'undef'
        $winmapObj = $self->getWinmap();

        # Set the position in the ordered list of strip objects at which the new strip object will
        #   be inserted
        if (
            ! defined $index
            || $index < -1           # Sanity check
            || $index >= scalar ($self->stripList)
        ) {
            $index = 0;
        }

        # Strip objects must inherit from GA::Generic::Strip and must exist (in the case of strip
        #   objects loaded from a plugin)
        if (
            ! $packageName =~ m/^Games\:\:Axmud\:\:Strip\:\:/
            || ! $axmud::CLIENT->ivExists('customStripHash', $packageName)
        ) {
            return undef;
        }

        # Create the strip object
        $stripObj = $packageName->new($self->stripCount, $self, %initHash);
        if (! $stripObj) {

            return undef;
        }

        # Some strip objects are 'jealous' (only one can be opened per window). If so, and if
        #   another strip object of this type has already been created, discard the new one
        if ($stripObj->jealousyFlag && $self->ivExists('firstStripHash', $packageName)) {

            return undef;
        }

        # Some strip objects can't be added in Axmud blind mode
        if (! $stripObj->blindFlag && $axmud::BLIND_MODE_FLAG) {

            return undef;
        }

        # Draw the strip object's widgets
        if (! $stripObj->objEnable($winmapObj)) {

            return undef;

        } else {

            if ($stripObj->allowFocusFlag) {
                $stripObj->packingBox->can_focus(TRUE);
            } else {
                $stripObj->packingBox->can_focus(FALSE);
            }
        }

        # Make the strip object visible, if the flag is set
        if ($stripObj->visibleFlag) {

            # Pack the newly-visible strip, leaving a gap if it's not at the beginning or end of the
            #   list
            # If $index is -1, it means 'pack at the end'
            if ($index == 0 || $index == -1) {
                $spacing = 0;
            } else {
                $spacing = $self->stripSpacingPixels;
            }

            if ($index > -1) {

                $self->packingBox->pack_start(
                    $stripObj->packingBox,
                    $stripObj->expandFlag,
                    $stripObj->fillFlag,
                    $spacing,
                );

                if ($index > 0) {

                    $self->packingBox->reorder_child($stripObj->packingBox, $posn);
                }

            } else {

                $self->packingBox->pack_end(
                    $stripObj->packingBox,
                    $stripObj->expandFlag,
                    $stripObj->fillFlag,
                    $spacing,
                );
            }
        }

        # Update IVs
        $self->ivAdd('stripHash', $stripObj->number, $stripObj);
        if (! $self->ivExists('firstStripHash', $packageName)) {

            $self->ivAdd('firstStripHash', $packageName, $stripObj);
        }

        $self->ivIncrement('stripCount');
        $self->ivSplice('stripList', $index, 0, $stripObj);

        if ($packageName eq 'Games::Axmud::Strip::Table') {

            $self->ivPoke('tableStripObj', $stripObj);
        }

        # Notify all other strip objects of the new strip object's birth
        foreach my $otherStripObj (
            sort {$a->number <=> $b->number} ($self->ivValues('stripHash'))
        ) {
            if ($otherStripObj ne $stripObj) {

                $otherStripObj->notify_addStripObj($stripObj);
            }
        }

        # Sensitise/desensitise widgets according to current conditions
        $self->restrictMenuBars();
        $self->restrictToolbars();

        # Make everything visible
        $self->winShowAll($self->_objClass . '->addStripObj');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->addStripObj');

        # Adding/removing widgets upsets the position of the scrollbar in each tab's textview.
        #   Make sure all the textviews are scrolled to the bottom
        $self->rescrollTextViews();

        # Redraw any visible gauges, otherwise the gauge box will be visible, but the gauges
        #   themselves will have disappeared
        $gaugeStripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::GaugeBox');
        if ($gaugeStripObj && $gaugeStripObj->visibleFlag) {

            $gaugeStripObj->updateGauges();
            # (Need to call this a second time, or the re-draw doesn't work...)
            $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->addStripObj');
        }

        return $stripObj;
    }

    sub removeStripObj {

        # Can be called by anything
        # Removes a strip object (inheriting from GA::Generic::Strip) from the list of strip objects
        #   that can be displayed in this window (whether the Gtk widget was actually drawn, or not,
        #   depends on the value of the strip object's ->visibleFlag)
        # Can't be used to remove the compulsory GA::Strip::Table object
        #
        # Expected arguments
        #   $stripObj   - The strip object to remove
        #
        # Return values
        #   'undef' on improper arguments, if $stripObj is the compulsory GA::Strip::Table object
        #       or if the object doesn't exist
        #   1 otherwise

        my ($self, $stripObj, $check) = @_;

        # Local variables
        my (
            $gaugeStripObj,
            @stripList, @resetList,
        );

        # Check for improper arguments
        if (! defined $stripObj || defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->removeStripObj', @_);
        }

        # The GA::Strip::Table object is compulsory, also check that the strip object actually
        #   exists in the window
        if (
            $stripObj->_objClass eq 'Games::Axmud::Strip::Table'
            || ! $self->ivExists('stripHash', $stripObj->number)
        ) {
            return undef
        }

        # Tell the strip object it's about to be removed, so it can do any necessary tidying up
        $stripObj->objDestroy();
        # Remove the Gtk2 widget that's contains the whole strip
        $axmud::CLIENT->desktopObj->removeWidget($self->packingBox, $stripObj->packingBox);

        # Remove the object by updating IVs
        $self->ivDelete('stripHash', $stripObj->number);

        foreach my $otherObj ($self->stripList) {

            if ($otherObj ne $stripObj) {

                push (@stripList, $otherObj);
            }
        }

        $self->ivPoke('stripList', @stripList);

        # $self->firstStripHash contains the earliest-created instance of this type of strip object.
        #   Update or reset it
        $self->ivDelete('firstStripHash', $stripObj->_objClass);
        @resetList = sort {$a->number <=> $b->number} ($self->ivValues('stripHash'));
        OUTER: foreach my $otherObj (@resetList) {

            if ($otherObj->_objClass eq $stripObj->_objClass) {

                $self->ivAdd('firstStripHash', $otherObj->_objClass, $otherObj);
                last OUTER;
            }
        }

        # Notify all other strip objects of this strip object's demise
        foreach my $otherStripObj (
            sort {$a->number <=> $b->number} ($self->ivValues('stripHash'))
        ) {
            if ($otherStripObj ne $stripObj) {

                $otherStripObj->notify_removeStripObj($stripObj);
            }
        }

        # Sensitise/desensitise widgets according to current conditions
        $self->restrictMenuBars();
        $self->restrictToolbars();

        # Make everything visible
        $self->winShowAll($self->_objClass . '->removeStripObj');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->removeStripObj');

        # Adding/removing widgets upsets the position of the scrollbar in each tab's textview.
        #   Make sure all the textviews are scrolled to the bottom
        $self->rescrollTextViews();

        # Redraw any visible gauges, otherwise the gauge box will be visible, but the gauges
        #   themselves will have disappeared
        $gaugeStripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::GaugeBox');
        if ($gaugeStripObj && $gaugeStripObj->visibleFlag) {

            $gaugeStripObj->updateGauges();
            # (Need to call this a second time, or the re-draw doesn't work...)
            $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->removeStripObj');
        }

        return 1;
    }

    sub addStrip {

        # Convenient shortcut to $self->addStripObj, which expects a package name like
        #   GA::Strip::GaugeBox as an argument
        #
        # This function accepts a string, which can be any of the following (case-insensitive):
        #   'menu' / 'menubar' / 'menu_bar'                 - adds a GA::Strip::MenuBar
        #   'tool' / 'toolbar' / 'tool_bar'                 - adds a GA::Strip::Toolbar
        #   'gauge' / 'gaugebox' / 'gauge_box'              - adds a GA::Strip::GaugeBox
        #   'entry'                                         - adds a GA::Strip::Entry
        #   'connect' / 'info' / 'connectinfo' / 'connect_info'
        #                                                   - adds a GA::Strip::ConnectInfo
        #
        # The string can also be a part of the package name itself. For example, if you create your
        #   own GA::Strip::MyObject (inheriting from GA::Strip::Custom), then this function expects
        #   the string 'MyObject' (case-sensitive)
        # NB This function is not able to check that the package actually exists, although Axmud's
        #   built-in strip objects always exist
        # NB This function can't be used to add the compulsory GA::Strip::Table which already
        #   exists
        #
        # Expected arguments
        #   $string     - The string described above
        #
        # Optional arguments
        #   $index      - The new strip object's position in the window. Strip objects are stored in
        #                   $self->stripList, in the order in which they're drawn (which could be
        #                   top to bottom, bottom to top, left to right or right to left). $index
        #                   specifies the position in that list at which the new strip object is
        #                   inserted. If $index is 0, the strip object is inserted at the beginning
        #                   of the list. If $index is 1, it's inserted second, if $index is 2, it's
        #                   inserted third, and so on. If $index is 'undef', or if its index is
        #                   outside the list, the strip object is inserted at the beginning of the
        #                   list
        #
        # Return values
        #   'undef' on improper arguments or if the strip object can't be added
        #   Otherwise returns the strip object added

        my ($self, $string, $index, $check) = @_;

        # Local variables
        my $packageName;

        # Check for improper arguments
        if (! defined $string || defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->addStrip', @_);
        }

        # Convert $string into a package name
        $packageName = $self->convertPackageName($string);
        if (! $packageName || $packageName eq 'Games::Axmud::Strip::Table') {

            return undef;

        } else {

            return $self->addStripObj($packageName, $index);
        }
    }

    sub removeStrip {

        # Convenient shortcut to $self->removeStripObj, which expects a package name like
        #   GA::Strip::GaugeBox as an argument
        #
        # This function accepts a string, which can be any of the following (case-insensitive):
        #   'menu' / 'menubar' / 'menu_bar'                 - adds a GA::Strip::MenuBar
        #   'tool' / 'toolbar' / 'tool_bar'                 - adds a GA::Strip::Toolbar
        #   'gauge' / 'gaugebox' / 'gauge_box'              - adds a GA::Strip::GaugeBox
        #   'entry'                                         - adds a GA::Strip::Entry
        #   'connect' / 'info' / 'connectinfo' / 'connect_info'
        #                                                   - adds a GA::Strip::ConnectInfo
        #
        # The string can also be a part of the package name itself. For example, if you create your
        #   own GA::Strip::MyObject (inheriting from GA::Strip::Custom), then this function expects
        #   the string 'MyObject' (case-sensitive)
        #
        # After converting the string into a package name, this function calls $self->removeStripObj
        #   to remove the earliest-created instance of that strip object, if any exists
        # NB This function is not able to check that the package actually exists, although Axmud's
        #   built-in strip objects always exist
        # NB This function can't be used to remove the compulsory GA::Strip::Table
        #
        # Expected arguments
        #   $string     - The string described above
        #
        # Return values
        #   'undef' on improper arguments or if no strip object is removed
        #   1 if a strip object is removed

        my ($self, $string, $check) = @_;

        # Local variables
        my ($packageName, $stripObj);

        # Check for improper arguments
        if (! defined $string || defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->removeStrip', @_);
        }

        # Convert $string into a package name
        $packageName = $self->convertPackageName($string);
        if (! $packageName || $packageName eq 'Games::Axmud::Strip::Table') {

            return undef;
        }

        # Find the earliest-created instance of that strip object
        $stripObj = $self->ivShow('firstStripHash', $packageName);
        if (! $stripObj) {

            return undef;

        } else {

            return $self->removeStripObj($stripObj);
        }
    }

    sub hideStripObj {

        # Can be called by anything
        # Hides a visible strip object; the strip object remains in the list of strip objects this
        #   window can display, but the Gtk widget itself is no longer drawn
        # Can't be used to hide the compulsory GA::Strip::Table object
        #
        # Expected arguments
        #   $stripObj   - The strip object to hide
        #
        # Return values
        #   'undef' on improper arguments, if $stripObj is the compulsory GA::Strip::Table object,
        #       if the object doesn't exist or if it is already hidden
        #   1 otherwise

        my ($self, $stripObj, $check) = @_;

        # Local variables
        my $gaugeStripObj;

        # Check for improper arguments
        if (! defined $stripObj || defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->hideStripObj', @_);
        }

        # The GA::Strip::Table object is compulsory, also check that the strip object actually
        #   exists in the window object's list of strip objects and is actually visible
        if (
            $stripObj->_objClass eq 'Games::Axmud::Strip::Table'
            || ! $self->ivExists('stripHash', $stripObj->number)
            || ! $stripObj->visibleFlag
        ) {
            return undef
        }

        # Remove the Gtk2 widget that contains the whole strip
        $axmud::CLIENT->desktopObj->removeWidget($self->packingBox, $stripObj->packingBox);
        # Update IVs
        $stripObj->set_visibleFlag(FALSE);

        # Sensitise/desensitise widgets according to current conditions
        $self->restrictMenuBars();
        $self->restrictToolbars();

        # Make everything visible
        $self->winShowAll($self->_objClass . '->hideStripObj');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->hideStripObj');

        # Adding/removing widgets upsets the position of the scrollbar in each tab's textview.
        #   Make sure all the textviews are scrolled to the bottom
        $self->rescrollTextViews();

        # Redraw any visible gauges, otherwise the gauge box will be visible, but the gauges
        #   themselves will have disappeared
        $gaugeStripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::GaugeBox');
        if ($gaugeStripObj && $gaugeStripObj->visibleFlag) {

            $gaugeStripObj->updateGauges();
            # (Need to call this a second time, or the re-draw doesn't work...)
            $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->hideStripObj');
        }

        return 1;
    }

    sub revealStripObj {

        # Can be called by anything
        # Reveals a hidden strip object; the strip object was  still in the list of strip objects
        #   this window can display, but the Gtk widget itself was not drawn, so draw it and add it
        #   to the window
        #
        # Expected arguments
        #   $stripObj   - The strip object to reveal
        #
        # Return values
        #   'undef' on improper arguments, if the object doesn't exist, if it is already visible or
        #       if there's an error in revealing it
        #   1 otherwise

        my ($self, $stripObj, $check) = @_;

        # Local variables
        my ($winmapObj, $count, $posn, $spacing, $gaugeStripObj);

        # Check for improper arguments
        if (! defined $stripObj || defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->revealStripObj', @_);
        }

        # Get the winmap object specified by $self->winmap, or a default winmap, if $self->winmap
        #   is 'undef'
        $winmapObj = $self->getWinmap();

        # Check that the strip object actually exists in the window object's list of strip objects
        #   and is actually hidden
        if (! $self->ivExists('stripHash', $stripObj->number) || $stripObj->visibleFlag) {

            return undef
        }

        # Find the strip object's position in the list of visible strip objects
        $count = 0;
        OUTER: foreach my $otherObj ($self->stripList) {

            if ($otherObj eq $stripObj) {

                $posn = $count;
                last OUTER;

            } elsif ($otherObj->visibleFlag) {

                $count++;
            }
        }

        if (! defined $posn) {

            # Strip object is missing (for some unlikely reason)
            return undef;
        }

        # Draw the strip object's widgets
        if (! $stripObj->objEnable($winmapObj)) {

            return undef;
        }

        # Pack the newly-visible strip, leaving a gap if it's not at the beginning or end of the
        #   list
        if (! $count || $posn == ($count - 1)) {
            $spacing = 0;
        } else {
            $spacing = $self->stripSpacingPixels;
        }

        $self->packingBox->pack_start(
            $stripObj->packingBox,
            $stripObj->expandFlag,
            $stripObj->fillFlag,
            $spacing,
        );

        if ($posn != 0) {

            $self->packingBox->reorder_child($stripObj->packingBox, $posn);
        }

        # Update IVs
        $stripObj->set_visibleFlag(TRUE);

        # Sensitise/desensitise widgets according to current conditions
        $self->restrictMenuBars();
        $self->restrictToolbars();

        # Make everything visible
        $self->winShowAll($self->_objClass . '->revealStripObj');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->revealStripObj');

        # Adding/removing widgets upsets the position of the scrollbar in each tab's textview.
        #   Make sure all the textviews are scrolled to the bottom
        $self->rescrollTextViews();

        # Redraw any visible gauges, otherwise the gauge box will be visible, but the gauges
        #   themselves will have disappeared
        $gaugeStripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::GaugeBox');
        if ($gaugeStripObj && $gaugeStripObj->visibleFlag) {

            $gaugeStripObj->updateGauges();
            # (Need to call this a second time, or the re-draw doesn't work...)
            $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->revealStripObj');
        }

        return 1;
    }

    sub replaceStripObj {

        # Can be called by anything
        # If a strip object has been redrawn for any reason, replace the old Gtk widget with the
        #   new one. The strip object's ->packingBox IV must already have been set to the new Gtk
        #   widget before calling this function
        #
        # Expected arguments
        #   $stripObj   - The strip object to replace
        #
        # Return values
        #   'undef' on improper arguments, if the object doesn't exist or if it hidden
        #   1 otherwise

        my ($self, $stripObj, $check) = @_;

        # Local variables
        my ($count, $posn, $spacing, $gaugeStripObj);

        # Check for improper arguments
        if (! defined $stripObj || defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->replaceStripObj', @_);
        }

        # Check that the strip object actually exists in the window object's list of strip objects
        #   and is actually visible
        if (! $self->ivExists('stripHash', $stripObj->number) || ! $stripObj->visibleFlag) {

            return undef
        }

        # Find the strip object's position in the list of visible strip objects
        $count = 0;
        OUTER: foreach my $otherObj ($self->stripList) {

            if ($otherObj eq $stripObj) {

                $posn = $count;
                last OUTER;

            } elsif ($otherObj->visibleFlag) {

                $count++;
            }
        }

        if (! defined $posn) {

            # Strip object is missing (for some unlikely reason)
            return undef;
        }


        # Remove the old Gtk2 widget that contains the whole strip
        $axmud::CLIENT->desktopObj->removeWidget($self->packingBox, $stripObj->packingBox);
        # Pack the newly-visible strip, leaving a gap if it's not at the beginning or end of the
        #   list
        if (! $count || $posn == ($count - 1)) {
            $spacing = 0;
        } else {
            $spacing = $self->stripSpacingPixels;
        }

        $self->packingBox->pack_start(
            $stripObj->packingBox,
            $stripObj->expandFlag,
            $stripObj->fillFlag,
            $spacing,
        );

        if ($posn != 0) {

            $self->packingBox->reorder_child($stripObj->packingBox, $posn);
        }

        # Sensitise/desensitise widgets according to current conditions
        $self->restrictMenuBars();
        $self->restrictToolbars();

        # Make everything visible
        $self->winShowAll($self->_objClass . '->replaceStripObj');
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->replaceStripObj');

        # Redraw any visible gauges, otherwise the gauge box will be visible, but the gauges
        #   themselves will have disappeared
        $gaugeStripObj = $self->ivShow('firstStripHash', 'Games::Axmud::Strip::GaugeBox');
        if ($gaugeStripObj && $gaugeStripObj->visibleFlag) {

            $gaugeStripObj->updateGauges();
            # (Need to call this a second time, or the re-draw doesn't work...)
            $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->replaceStripObj');
        }

        return 1;
    }

    sub getStrip {

        # Convenient method for getting the blessed reference of the earliest-created instance of a
        #   type of strip object
        #
        # This function accepts a string, which can be any of the following (case-insensitive):
        #   'menu' / 'menubar' / 'menu_bar'                 - converts to GA::Strip::MenuBar
        #   'tool' / 'toolbar' / 'tool_bar'                 - converts to GA::Strip::Toolbar
        #   'table'                                         - converts to GA::Strip::Table
        #   'gauge' / 'gaugebox' / 'gauge_box'              - converts to GA::Strip::GaugeBox
        #   'entry'                                         - converts to GA::Strip::Entry
        #   'connect' / 'info' / 'connectinfo' / 'connect_info'
        #                                                   - converts to GA::Strip::ConnectInfo
        #
        # The string can also be a part of the package name itself. For example, if you create your
        #   own GA::Strip::MyObject (inheriting from GA::Strip::Custom), then this function expects
        #   the string 'MyObject' (case-sensitive)
        #
        # Expected arguments
        #   $string     - The string described above
        #
        # Return values
        #   'undef' on improper arguments or if a strip object of that type doesn't exist in the
        #       window
        #   Otherwise returns the blessed reference to the earliest-created instance of the
        #       specified type of strip object

        my ($self, $string, $check) = @_;

        # Local variables
        my $packageName;

        # Check for improper arguments
        if (! defined $string || defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->getStrip', @_);
        }

        # Convert $string into a package name
        $packageName = $self->convertPackageName($string);
        if (! $packageName || $packageName eq 'Games::Axmud::Strip::Table') {

            return undef;
        }

        # Return the earliest-created instance of that strip object (return 'undef' if none exists)
        return $self->ivShow('firstStripHash', $packageName);
    }

    sub convertPackageName {

        # Called by $self->addStrip, ->getStrip and $self->removeStrip
        # Converts a simple string (e.g. 'menu' into the package name of a strip object (e.g.
        #   'Games::Axmud::Strip::MenuBar'
        #
        # This function accepts a string, which can be any of the following (case-insensitive):
        #   'menu' / 'menubar' / 'menu_bar'                 - converts to GA::Strip::MenuBar
        #   'tool' / 'toolbar' / 'tool_bar'                 - converts to GA::Strip::Toolbar
        #   'table'                                         - converts to GA::Strip::Table
        #   'gauge' / 'gaugebox' / 'gauge_box'              - converts to GA::Strip::GaugeBox
        #   'entry'                                         - converts to GA::Strip::Entry
        #   'connect' / 'info' / 'connectinfo' / 'connect_info'
        #                                                   - converts to GA::Strip::ConnectInfo
        #
        # The string can also be a part of the package name itself. For example, if you create your
        #   own GA::Strip::MyObject (inheriting from GA::Strip::Custom), then this function expects
        #   the string 'MyObject' (case-sensitive)
        #
        # Expected arguments
        #   $string     - The string to convert
        #
        # Return values
        #   'undef' on improper arguments
        #   Otherwise returns a strip object's package name

        my ($self, $string, $check) = @_;

        # Local variables
        my $packageName;

        # Check for improper arguments
        if (! defined $string || defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->convertPackageName', @_);
        }

        if (
            lc($string) eq 'menu' || lc($string) eq 'menubar' || lc($string) eq 'menu_bar'
        ) {
            $packageName = 'Games::Axmud::Strip::MenuBar';

        } elsif (
            lc($string) eq 'tool' || lc($string) eq 'toolbar' || lc($string) eq 'tool_bar'
        ) {
            $packageName = 'Games::Axmud::Strip::Toolbar';

        } elsif (
            lc($string) eq 'gauge' || lc($string) eq 'gaugebox' || lc($string) eq 'gauge_box'
        ) {
            $packageName = 'Games::Axmud::Strip::GaugeBox';

        } elsif (lc($string) eq 'entry') {

            $packageName = 'Games::Axmud::Strip::Entry';

        } elsif (
            lc($string) eq 'connect' || lc($string) eq 'info' || lc($string) eq 'connectinfo'
            || lc($string) eq 'connect_info'
        ) {
            $packageName = 'Games::Axmud::Strip::ConnectInfo';

        } else {

            $packageName = 'Games::Axmud::Strip::' . $string;
        }

        return $packageName;
    }

    ##################
    # Accessors - set

    ##################
    # Accessors - get

    sub stripHash
        { my $self = shift; return %{$self->{stripHash}}; }
    sub firstStripHash
        { my $self = shift; return %{$self->{firstStripHash}}; }
    sub stripCount
        { $_[0]->{stripCount} }
    sub stripList
        { my $self = shift; return @{$self->{stripList}}; }
    sub tableStripObj
        { $_[0]->{tableStripObj} }
    sub stripSpacingPixels
        { $_[0]->{stripSpacingPixels} }

    sub visibleSession
        { $_[0]->{visibleSession} }

    sub hostLabelText
        { $_[0]->{hostLabelText} }
    sub timeLabelText
        { $_[0]->{timeLabelText} }

    sub ctrlKeyFlag
        { $_[0]->{ctrlKeyFlag} }
    sub shiftKeyFlag
        { $_[0]->{shiftKeyFlag} }
    sub altKeyFlag
        { $_[0]->{altKeyFlag} }
    sub altGrKeyFlag
        { $_[0]->{altGrKeyFlag} }
    sub modifierKeyFlag
        { $_[0]->{modifierKeyFlag} }

    sub actualWinWidth
        { $_[0]->{actualWinWidth} }
    sub actualWinHeight
        { $_[0]->{actualWinHeight} }
    sub maximisedFlag
        { $_[0]->{maximisedFlag} }
    sub focusFlag
        { $_[0]->{focusFlag} }
}

{ package Games::Axmud::Win::Map;

    use strict;
    use warnings;
    use diagnostics;

    use Glib qw(TRUE FALSE);

    our @ISA = qw(
        Games::Axmud::Generic::MapWin Games::Axmud::Generic::GridWin Games::Axmud::Generic::Win
        Games::Axmud
    );

    ##################
    # Constructors

    sub new {

        # Called by GA::Obj::Workspace->createGridWin and ->createSimpleGridWin
        # Creates an Automapper window
        #
        # Expected arguments
        #   $number     - Unique number for this window object
        #   $winType    - The window type, must be 'map'
        #   $winName    - The window name, must be 'map'
        #   $workspaceObj
        #               - The GA::Obj::Workspace object for the workspace in which this window is
        #                   created
        #
        # Optional arguments
        #   $owner      - The owner, if known ('undef' if not). Typically it's a GA::Session or a
        #                   task (inheriting from GA::Generic::Task); could also be GA::Client. It
        #                   should not be another window object (inheriting from GA::Generic::Win).
        #                   The owner should have its own ->del_winObj function which is called when
        #                   $self->winDestroy is called
        #   $session    - The owner's session. If $owner is a GA::Session, that session. If it's
        #                   something else (like a task), the task's session. If $owner is 'undef',
        #                   so is $session
        #   $workspaceGridObj
        #               - The GA::Obj::WorkspaceGrid object into whose grid this window has been
        #                   placed. 'undef' in $workspaceObj->gridEnableFlag = FALSE
        #   $areaObj    - The GA::Obj::Area (a region of a workspace grid zone) which handles this
        #                   window. 'undef' in $workspaceObj->gridEnableFlag = FALSE
        #   $winmap     - Ignored if set
        #
        # Return values
        #   'undef' on improper arguments or if no $session was specified
        #   Blessed reference to the newly-created object on success

        my (
            $class, $number, $winType, $winName, $workspaceObj, $owner, $session, $workspaceGridObj,
            $areaObj, $winmap, $check,
        ) = @_;

        # Check for improper arguments
        if (
            ! defined $class || ! defined $number || ! defined $winType || ! defined $winName
            || ! defined $workspaceObj || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($class . '->new', @_);
        }

        # Automapper windows are unique to their session. If no $session is specified, refuse to
        #   create a window object
        if (! $session) {

            return undef;
        }

        # Check that the $winType is valid
        if ($winType ne 'map') {

            return $axmud::CLIENT->writeError(
                'Internal window error: invalid \'map\' window type \'' . $winType . '\'',
                $class . '->new',
            );
        }

        # Setup
        my $self = {
            _objName                    => 'map_win_' . $number,
            _objClass                   => $class,
            _parentFile                 => undef,       # No parent file object
            _parentWorld                => undef,       # No parent file object
            _privFlag                   => TRUE,        # All IVs are private

            # Standard window object IVs
            # --------------------------

            # Unique number for this window object
            number                      => $number,
            # The window category - 'grid' or 'free'
            winCategory                 => 'grid',
            # The window type, must be 'map'
            winType                     => $winType,
            # The window name, must be 'map'
            winName                     => $winName,
            # The GA::Obj::Workspace object for the workspace in which this window is created
            workspaceObj                => $workspaceObj,
            # The owner, if known ('undef' if not). Typically it's a GA::Session or a task
            #   (inheriting from GA::Generic::Task); could also be GA::Client. It should not be
            #   another window object (inheriting from GA::Generic::Win). The owner must have its
            #   own ->del_winObj function which is called when $self->winDestroy is called
            owner                       => $owner,
            # The owner's session ('undef' if not). If ->owner is a GA::Session, that session. If
            #   it's something else (like a task), the task's sesssion. If ->owner is 'undef', so is
            #   ->session
            session                     => $session,
            # When GA::Session->pseudoCmd is called to execute a client command, the mode in which
            #   it should be called (usually 'win_error' or 'win_only', which causes errors to be
            #   displayed in a 'dialogue' window)
            pseudoCmdMode               => 'win_only',

            # The window widget. For most window objects, the Gtk2::Window. For pseudo-windows, the
            #   parent 'main' window's Gtk2::Window
            # The code should use this IV when it wants to do something to the window itself
            #   (minimise it, make it active, etc)
            winWidget                   => undef,
            # The window container. For most window objects, the Gtk2::Window. For pseudo-windows,
            #   the parent GA::Table::PseudoWin table object
            # The code should use this IV when it wants to add, modify or remove widgets inside the
            #   window itself
            winBox                      => undef,
            # The Gnome2::Wnck::Window, if known
            wnckWin                     => undef,
            # Flag set to TRUE if the window actually exists (after a call to $self->winEnable),
            #   FALSE if not
            enabledFlag                 => FALSE,
            # Flag set to TRUE if the Gtk2 window itself is visible (after a call to
            #   $self->setVisible), FALSE if it is not visible (after a call to $self->setInvisible)
            visibleFlag                 => TRUE,
            # Registry hash of 'free' windows (excluding 'dialogue' windows) for which this window
            #   is the parent, a subset of GA::Obj::Desktop->freeWinHash. Hash in the form
            #       $childFreeWinHash{unique_number} = blessed_reference_to_window_object
            childFreeWinHash            => {},
            # When a child 'free' window (excluding 'dialogue' windows) is destroyed, this parent
            #   window is informed via a call to $self->del_childFreeWin
            # When the child is destroyed, this window might want to call some of its own functions
            #   to update various widgets and/or IVs, in which case this window adds an entry to
            #   this hash; a hash in the form
            #       $childDestroyHash{unique_number} = list_reference
            # ...where 'unique_number' is the child window's ->number, and 'list_reference' is a
            #   reference to a list in groups of 2, in the form
            #       (sub_name, argument_list_ref, sub_name, argument_list_ref...)
            childDestroyHash            => {},

            # The container widget into which all other widgets are packed (usually a Gtk2::VBox or
            #   Gtk2::HBox, but any container widget can be used; takes up the whole window client
            #   area)
            packingBox                  => undef,

            # Standard IVs for 'grid' windows

            # The GA::Obj::WorkspaceGrid object into whose grid this window has been placed. 'undef'
            #   if $workspaceObj->gridEnableFlag = FALSE
            workspaceGridObj            => $workspaceGridObj,
            # The GA::Obj::Area object for this window. An area object is a part of a zone's
            #   internal grid, handling a single window (this one). Set to 'undef' in
            #   $workspaceObj->gridEnableFlag = FALSE
            areaObj                     => $areaObj,
            # For pseudo-windows (in which a window object is created, but its widgets are drawn
            #   inside a GA::Table::PseudoWin table object), the table object created. 'undef' if
            #   this window object is a real 'grid' window
            pseudoWinTableObj           => undef,
            # The ->name of the GA::Obj::Winmap object (not used for 'map' windows)
            winmap                      => undef,

            # IVs for this kind of 'map' window

            # The parent automapper object (a GA::Obj::Map - set later)
            mapObj                      => undef,
            # The session's current world model object
            worldModelObj               => $session->worldModelObj,

            # The menu bar
            menuBar                     => undef,
            # The toolbar
            toolbar                     => undef,
            # Menu bar/toolbar items which will be sensitised or desensitised, depending on the
            #   context. Hash in the form
            #       $menuToolItemHash{'item_name'} = gtk2_widget
            # ...where:
            #   'item_name' is a descriptive scalar, e.g. 'move_up_level'
            #   'gtk2_widget' is the Gtk2::MenuItem or toolbar widget, typically Gtk2::ToolButton or
            #       Gtk2::RadioToolButton
            menuToolItemHash            => {},

            # The icons on the toolbar appear and disappear in a cycle. This IV stores which set of
            #   icons is currently visible. It is set to 0 when the toolbar isn't visible; the first
            #   set of icons is set 1
            toolbarCurrentSet           => 0,
            # The number of sets available
            toolbarSetCount             => 6,
            # The switcher icon (a Gtk2::ToolItem)
            toolbarSwitchIcon           => undef,
            # The separator which is immediately next to the switcher icon
            toolbarMainSeparator        => undef,
            # Only one set of icons is visible at a time. This hash contains all of the toolbar
            #   buttons that have been created, in the form
            #       $toolbarButtonHash{number_of_set} = [button, button, button, button...]
            #   ...where 'number_of_set' is an integer (the first set is #1), and 'button' is a
            #       Gtk2::ToolButton, Gtk2::ToggleToolButton or Gtk2::RadioToolButton
            toolbarButtonHash           => {},

            # A horizontal pane, dividing the treeview on the left from everything else on the right
            hPaned                      => undef,

            # The treeview widgets (on the left)
            treeViewModel               => undef,
            treeView                    => undef,
            treeViewScroller            => undef,
            treeViewWidthPixels         => 150,     # (Default width)
            # The currently selected line of the treeview (selected by single-clicking on it)
            treeViewSelectedLine        => undef,
            # A hash of regions in the treeview, which stores which rows containing parent regions
            #   have been expanded to reveal their child regions
            # Hash in the form
            #   $treeViewRegionHash{region_name} = flag
            # ...where 'flag' is TRUE when the row is expanded, FALSE when the row is not expanded
            treeViewRegionHash          => {},
            # A hash of pointers (iters) in the treeview, so we can look up each region's cell
            treeViewPointerHash         => {},

            # Canvas widgets (on the right)
            canvas                      => undef,
            canvasRoot                  => undef,
            canvasFrame                 => undef,
            canvasScroller              => undef,
            canvasHAdjustment           => undef,
            canvasVAdjustment           => undef,
            canvasBackground            => undef,

            # Tooltips
            # The current canvas object for which a tooltip is displayed ('undef' if no canvas
            #   object has a tooltip displayed)
            canvasTooltipObj            => undef,
            # What type of canvas object it is: 'room', 'room_tag', 'room_guild', 'exit', 'exit_tag'
            #   or 'label'
            canvasTooltipObjType        => undef,
            # When tooltips are visible, a useless 'leave-notify' event occurs. When the mouse moves
            #   over a canvas object, ->canvasTooltipObj is set and this IV is set to TRUE; if the
            #   next event is a 'leave-notify' event, it is ignored
            canvasTooltipFlag           => FALSE,

            # Blessed reference of the currently displayed GA::Obj::Regionmap ('undef' if no region
            #   is displayed; not necessarily the same region as the character's current location)
            currentRegionmap            => undef,

            # Objects on the map can be selected. There are three modes of selection:
            #   (1) There is a single room, OR a single room tag, OR a single room guild, OR a
            #           single exit, OR a single exit tag, OR a single label selected
            #   (2) Multiple objects are selected (including combinations of rooms, room tags, room
            #           guilds, exits, exit tags and labels)
            #   (3) Nothing is currently selected
            # In mode (1), one (or none) of the IVs ->selectedRoom, ->selectedRoomTag,
            #   ->selectedRoomGuild, ->selectedExit, ->selectedExitTag or ->selectedLabel is set
            #   (but the mode 2 IVs are empty)
            # In mode (2), the selected objects are in ->selectedRoomHash, ->selectedRoomTagHash,
            #   ->selectedRoomGuildHash ->selectedExitHash, ->selectedExitTagHash and
            #   ->selectedLabelHash (but the mode 1 IVs are set to 'undef')
            # In mode (3), all of the IVs below are not set
            #
            # Mode (1) IVs
            # Blessed reference of the currently selected location (a GA::ModelObj::Room), which
            #   might be the same as $self->mapObj->currentRoom or $self->mapObj->lastKnownRoom
            selectedRoom                => undef,
            # Blessed reference of the location (a GA::ModelObj::Room) whose room tag is selected
            selectedRoomTag             => undef,
            # Blessed reference of the location (a GA::ModelObj::Room) whose room guild is selected
            selectedRoomGuild           => undef,
            # Blessed reference of the currently selected exit (a GA::Obj::Exit)
            selectedExit                => undef,
            # Blessed reference of the exit (a GA::Obj::Exit) whose exit tag is selected
            selectedExitTag             => undef,
            # Blessed reference of the currently selected label (a GA::Obj::MapLabel object)
            selectedLabel               => undef,

            # Mode (2) IVs
            # Hash of selected locations, in the form
            #   $selectedRoomHash{model_number} = blessed_reference_to_room_object
            selectedRoomHash            => {},
            # Hash of locations whose room tags are selected, in the form
            #   $selectedRoomTagHash{model_number) = blessed_reference_to_room_object
            selectedRoomTagHash         => {},
            # Hash of locations whose room guilds are selected, in the form
            #   $selectedRoomGuildHash{model_number) = blessed_reference_to_room_object
            selectedRoomGuildHash       => {},
            # Hash of selected exits, in the form
            #   $selectedExitHash{exit_model_number} = blessed_reference_to_exit_object
            selectedExitHash            => {},
            # Hash of exits whose exit tags are selected, in the form
            #   $selectedExitTagHash{exit_model_number} = blessed_reference_to_exit_object
            selectedExitTagHash         => {},
            # Hash of selected labels, in the form
            #   $selectedLabelHash{label_number) = blessed_reference_to_map_label_object
            # ...where 'label_number' matches a key in GA::Obj::Regionmap->gridLabelHash
            selectedLabelHash           => {},

            # When there is a single selected exit ($self->selectedExit is set), and if it's a
            #   broken or a region exit, the twin exit (and its parent room) are drawn a different
            #   colour
            # When the broken/region exit is selected, these IVs are set...
            # The blessed reference of the twin exit
            pairedTwinExit              => undef,
            # The blessed reference of the twin exit's parent room
            pairedTwinRoom              => undef,

            # The map itself is just a collection of GA::ModelObj::Room objects, GA::Obj::Exit
            #   objects and GA::Obj::MapLabel objects. They are stored in each region's
            #   GA::Obj::Regionmap
            # The currently displayed level of the currently displayed region consists of a
            #   collection of Gnome2::Canvas::Item objects.
            # These hashes work in parallel with the regionmap's ->gridRoomHash, ->gridRoomTagHash,
            #   ->gridRoomGuildHash, ->gridExitHash, ->gridExitTagHash and ->gridLabelHash. The keys
            #   are the same for each, but the values in these hashes contain a reference to a list
            #   of Gnome2::Canvas::Items
            # Only one room is allowed per gridblock, but exits and labels can be drawn freely.
            # These hashes contain drawn objects, not necessarily every object stored in the
            #   regionmap
            # Hash of drawn rooms from this regionmap, in the form
            #   $drawnRoomHash{'x_y_z'} = [canvas_object, canvas_object...]
            drawnRoomHash               => {},
            # A subset of key-value pairs from ->drawnRoomHash, containing only those rooms which
            #   have been drawn with a double-size border (current, ghost and lost rooms, but only
            #   when GA::Obj::WorldModel->currentRoomMode is 'double'
            dummyRoomHash               => {},
            # Hash of drawn room echos from this regionmap, in the form
            #   $drawnRoomEchoHash{'x_y_z'} = [canvas_object, canvas_object...]
            drawnRoomEchoHash           => {},
            # Hash of drawn rooms with room tags from this regionmap, in the form
            #   $drawnRoomTagHash{'x_y_z'} =  [canvas_object, canvas_object...]
            drawnRoomTagHash            => {},
            # Hash of drawn rooms with room guilds from this regionmap, in the form
            #   $drawnRoomGuildHash{'x_y_z'} =  [canvas_object, canvas_object...]
            drawnRoomGuildHash          => {},
            # Hash of drawn rooms that have text drawn within their interiors, in the form
            #   $drawnRoomTextHash{'x_y_z'} = [canvas_object, canvas_object...]
            drawnRoomTextHash           => {},
            # Hash of drawn exits from this regionmap (not necessarily all the exits in all the
            #   rooms), in the form
            #       $drawnExitHash{exit_model_number} = [canvas_object, canvas_object...]
            drawnExitHash               => {},
            # Hash of drawn exits with exit tags from this regionmap, in the form
            #   $drawnExitTagHash{exit_model_number} = [canvas_object, canvas_object...]
            drawnExitTagHash            => {},
            # Hash of drawn exits from this regionmap that have exit ornaments, in the form
            #   $drawnOrnamentHash{exit_model_number} = [canvas_object, canvas_object...]
            drawnOrnamentHash           => {},
            # Hash of (all) labels that exist in this regionmap, in the form
            #   $drawnLabelHash{label_number} = [canvas_object, canvas_object...]
            drawnLabelHash              => {},
            # Flag that can be set to TRUE by any code that wants to prevent a drawing operation
            #   from starting (temporarily); the operation will be able to start when the flag is
            #   set back to FALSE
            delayDrawFlag               => FALSE,

            # When we need to draw (or redraw) objects on the canvas, sometimes we want to draw the
            #   objects right away and sometimes we want to wait until something is finished before
            #   we do the drawing
            # To draw thing right away, we call $self->doDraw. To draw things later, we call
            #   $self->markObjs to add entries to these IVs; then, at some later time, $self->doDraw
            #   is called to draw them
            # Since we're using hashes, it's safe to add the same object multiple times
            # Drawing a room will redraw any room tags/room guilds associated with that room. If
            #   the room tag/room guild has already been drawn, it is not drawn a second time during
            #   any call to $self->doDraw.
            # The same applies for exits; drawing an exit will redraw its exit tag. If the exit tag
            #   has already been drawn, it is not drawn a second time during any call to
            #   $self->doDraw
            # Hash of rooms to be drawn, in the form
            #   $markedRoomHash{model_number} = blessed_reference_to_room_object
            markedRoomHash              => {},
            # Hash of room tags to be drawn, in the form
            #   $markedRoomTagHash{model_number} = blessed_reference_to_room_object
            markedRoomTagHash           => {},
            # Hash of room guilds to be drawn, in the form
            #   $markedRoomGuildHash{model_number} = blessed_reference_to_room_object
            markedRoomGuildHash         => {},
            # Hash of exits to be drawn, in the form
            #   $markedExitHash{exit_model_number} = blessed_reference_to_exit_object
            markedExitHash              => {},
            # Hash of exit tagss to be drawn, in the form
            #   $markedExitTagHash{exit_model_number} = blessed_reference_to_exit_object
            markedExitTagHash           => {},
            # Hash of labels to be drawn, in the form
            #   $markedLabelHash{label_number} = blessed_reference_to_label_object
            markedLabelHash             => {},

            # $self->drawCycleExitHash contains a list of exits that have been drawn during the
            #   current drawing cycle. Before drawing an exit, we can check whether it has a twin
            #   exit (which occupies the same space) and, if so, we don't need to draw it a second
            #   time - thus each exit-twin exit pair is only drawn once for each call to
            #   $self->drawObjs. Hash in the form
            #       $drawCycleExitHash{exit_model_number} = blessed_reference_to_exit_object
            drawCycleExitHash           => {},
            # The (pango) size of room interior text. This value is set by $self->doDraw, at the
            #   start of every drawing cycle, to be a little bit smaller than half the width of the
            #   room (which depends on the draw exit mode in effect)
            drawRoomTextSize            => undef,
            # For room interior text, the size of the usable area (which depends on the draw exit
            #   mode in effect). The values are also set by $self->doDraw, once per drawing cycle
            drawRoomTextWidth           => undef,
            drawRoomTextHeight          => undef,
            # The (pango) sizes of text drawn on the map. Also set by $self->doDraw, based on the
            #   size of a room when exits are being drawn
            drawMapTextSize             => undef,

            # What happens when the user clicks on an empty region of the map
            #   'default' - normal operation. Any selected objects are unselected
            #   'add_room' - 'Add room at click' menu option - when the user clicks on the map, a
            #       new room is added at that location
            #   'connect_exit' - 'Connect [exit] to click' menu option - when the user clicks on a
            #       room on the map (on any level, in any region), the exit is connected to that
            #       room
            #   'add_label' - 'Add label at click' menu option - when the user clicks on the map, a
            #       new label is added at that location
            #   'move_room' - 'Move selected rooms to click' menu option - when the user clicks on
            #       the map (probably in a new region), the selected rooms (and their room tags/room
            #       guilds/exits/exit tags) and labels are move to that position on the map
            freeClickMode               => 'default',
            # When working out whether the user has clicked on an exit, how closely the angle of the
            #   drawn exit's gradient (relative to the x-axis) must match the gradient of a line
            #   from the exit's origin point, to the point on the map the user clicked (in degrees)
            exitSensitivity             => 30,
            # A value used to draw bends on bending exits. The actual size of the drawn bend (a
            #   square) is twice this value, plus 1
            exitBendSize                => 2,
            # When the user right-clicks on an exit, we need to record the position of the click, in
            #   case the user wants to add an exit bend at that point. These IVs are reset by a
            #   click on any other part of the canvas
            exitClickXPosn              => undef,
            exitClickYPosn              => undef,

            # The operating mode:
            #   'wait'      - The automapper isn't doing anything
            #   'follow'    - The automapper is following the character's position, but not
            #                   updating the world model (except for the character visit count)
            #   'update'    - The automapper is following the character's position and updating the
            #                   world model when required
            mode                        => 'wait',

            # To show visits for a different character, this IV is set to a character's name (which
            #   matches the name of a character profile). If set to 'undef', the current character
            #   profile is used
            showChar                    => undef,
            # The painter is a non-model GA::ModelObj::Room object stored in the world model. When
            #   this flag is set to TRUE, the painter's IVs are used to create (or update) new room
            #   objects; if set to FALSE, the painter is ignored
            painterFlag                 => FALSE,

            # Primary vector hash - maps Axmud's primary directions onto a vector, expressed in a
            #   list reference as (x, y, z), showing the direction that each primary direction takes
            #   us on the Axmud map
            # (In the grid, the top-left corner at the highest level has coordinates 0, 0, 0)
            constVectorHash             => {
                north                   => [0, -1, 0],
                northnortheast          => [0.5, -1, 0],
                northeast               => [1, -1, 0],
                eastnortheast           => [1, -0.5, 0],
                east                    => [1, 0, 0],
                eastsoutheast           => [1, 0.5, 0],
                southeast               => [1, 1, 0],
                southsoutheast          => [0.5, 1, 0],
                south                   => [0, 1, 0],
                southsouthwest          => [-0.5, 1, 0],
                southwest               => [-1, 1, 0],
                westsouthwest           => [-1, 0.5, 0],
                west                    => [-1, 0, 0],
                westnorthwest           => [-1, -0.5, 0],
                northwest               => [-1, -1, 0],
                northnorthwest          => [-0.5, -1, 0],
                up                      => [0, 0, 1],
                down                    => [0, 0, -1],
            },
            # A second vector hash for drawing two-way exits (which are drawn as two parallel lines)
            # Each value is expressed as a list reference (x1, y1, x2, y2)
            # (x1, y1) are simply added to the coordinates of the start and stop pixels of the first
            #   line, and (x2, y2) are added to the start and stop pixels of the second line - this
            #   moves the two lines either side of where the line is normally drawn
            # NB 'up' and 'down' are never drawn with double lines, so their values are both
            #   [0, 0, 0, 0]
            constDoubleVectorHash       => {
                north                   => [-1, 0, 1, 0],
                northnortheast          => [-1, 0, 1, 0],   # Also on top of room box, so same as N
                northeast               => [-1, 0, 0, 1],
                eastnortheast           => [0, -1, 0, 1],   # Same as E
                east                    => [0, -1, 0, 1],
                eastsoutheast           => [0, -1, 0, 1],   # Same as E
                southeast               => [-1, 0, 0, -1],
                southsoutheast          => [-1, 0, 1, 0],   # Same as S
                south                   => [-1, 0, 1, 0],
                southsouthwest          => [-1, 0, 1, 0],   # Same as S
                southwest               => [0, -1, 1, 0],
                westsouthwest           => [0, -1, 0, 1],   # Same as W
                west                    => [0, -1, 0, 1],
                westnorthwest           => [0, -1, 0, 1],   # Same as W
                northwest               => [1, 0, 0, 1],
                northnorthwest          => [-1, 0, 1, 0],   # Same as N
                up                      => [0, 0, 0, 0],
                down                    => [0, 0, 0, 0],
            },
            # A third vector hash for drawing one-way exits (which are drawn as a single line, with
            #   an arrowhead at the edge of the block, showing the exit's direction
            # Each value is expressed as a list reference (x1, y1, x2, y2)
            # (x1, y1) is a vector showing the direction of one half of the arrowhead, starting at
            #   the edge of the block. (x2, y2) is a vector showing the direction of travel of the
            #   other half
            # NB 'up' and 'down' are never drawn with single lines, so their values are both
            #   [0, 0, 0, 0]
            constArrowVectorHash        => {
                north                   => [-1, 1, 1, 1],
                northnortheast          => [-0.8, 0.5, 0.5, 0.8], # Approx. a right-angled arrowhead
                northeast               => [-1, 0, 0, 1],
                eastnortheast           => [-0.8, -0.5, -0.5, 0.8],
                east                    => [-1, -1, -1, 1],
                eastsoutheast           => [-0.5, -0.8, -0.8, 0.5],
                southeast               => [-1, 0, 0, -1],
                southsoutheast          => [-0.8, -0.5, 0.5, -0.8],
                south                   => [-1, -1, 1, -1],
                southsouthwest          => [-0.5, -0.8, 0.8, -0.5],
                southwest               => [0, -1, 1, 0],
                westsouthwest           => [0.5, -0.8, 0.8, 0.5],
                west                    => [1, -1, 1, 1],
                westnorthwest           => [0.8, -0.5, 0.5, 0.8],
                northwest               => [1, 0, 0, 1],
                northnorthwest          => [0.8, 0.5, -0.5, 0.8],
                up                      => [0, 0, 0, 0],
                down                    => [0, 0, 0, 0],
            },
            # A fourth vector hash for drawing exit ornaments (which are drawn perpendicular to the
            #   exit line)
            # Each value is expressed as a list reference (x1, y1, x2, y2)
            # (x1, y1) is a vector showing the direction of one half of the ornament, generally
            #   starting in the middle of the exit line (and perpendicular to it). (x2, y2) is a
            #   vector showing the direction of the other half
            # NB 'up' and 'down' are never drawn with single lines, so their values are both
            #   [0, 0, 0, 0]
            constPerpVectorHash         => {    # 'perp' for 'perpendicular'
                north                   => [-1, 0, 1, 0],
                northnortheast          => [-0.8, -0.5, 0.8, 0.5],  # Approx perpendicular line
                northeast               => [-1, -1, 1, 1],
                eastnortheast           => [-0.5, -0.8, 0.5, 0.8],
                east                    => [0, -1, 0, 1],
                eastsoutheast           => [0.5, -0.8, -0.5, 0.8],
                southeast               => [-1, 1, 1, -1],
                southsoutheast          => [-0.8, 0.5, 0.8, -0.5],
                south                   => [-1, 0, 1, 0],
                southsouthwest          => [-0.8, -0.5, 0.8, 0.5],
                southwest               => [-1, -1, 1, 1],
                westsouthwest           => [-0.5, -0.8, 0.5, 0.8],
                west                    => [0, -1, 0, 1],
                westnorthwest           => [-0.5, 0,8, 0.5, -0.8],
                northwest               => [-1, 1, 1, -1],
                northnorthwest          => [-0.8, 0.5, 0.8, -0.5],
                up                      => [0, 0, 0, 0],
                down                    => [0, 0, 0, 0],
            },
            # A fifth vector hash, a slightly modified version of ->constVectorHash, used by
            #   GA::Obj::Map->moveKnownDirSeen for placing new rooms on the map.
            # Moves in the north-south west-east southwest-northeast and southeast-northwest
            #   directions are placed in adjacent gridblocks, but moves in the northnortheast (etc)
            #   direction have to be placed about 2 gridblocks away
            constSpecialVectorHash      => {
                north                   => [0, -1, 0],  # Same values used in ->constVectorHash
                northnortheast          => [1, -2, 0],  # Double values used in ->constVectorHash
                northeast               => [1, -1, 0],
                eastnortheast           => [2, -1, 0],
                east                    => [1, 0, 0],
                eastsoutheast           => [2, 1, 0],
                southeast               => [1, 1, 0],
                southsoutheast          => [1, 2, 0],
                south                   => [0, 1, 0],
                southsouthwest          => [-1, 2, 0],
                southwest               => [-1, 1, 0],
                westsouthwest           => [-2, 1, 0],
                west                    => [-1, 0, 0],
                westnorthwest           => [-2, -1, 0],
                northwest               => [-1, -1, 0],
                northnorthwest          => [-1, -2, 0],
                up                      => [0, 0, 1],
                down                    => [0, 0, -1],
            },
            # A hash for drawing triangles in a return exit. One of the triangle's points is at the
            #   pixel where an incomplete exit would start, touching the room box. The other two
            #   points are corners of the square used to draw broken/region exits
            # $self->preDrawnSquareExitHash describes the positions of opposite corners of this
            #   square as:
            #       (top_left_x, top_left_y, bottom_right_x, bottom_right_y)
            # This hash tells gives us four of these values, referred to as 0-3
            #       (0,          1,          2,              3)
            # The first pair describes the second corner of the triangle; the second pair describes
            #   the third corner of the triangle
            constTriangleCornerHash     => {
                north                   => [0, 1, 2, 1],
                northnortheast          => [0, 1, 2, 1],    # Same as N
                northeast               => [0, 1, 2, 3],
                eastnortheast           => [2, 1, 2, 3],    # Same as E
                east                    => [2, 1, 2, 3],
                eastsoutheast           => [2, 1, 2, 3],    # Same as E
                southeast               => [2, 1, 0, 3],
                southsoutheast          => [0, 3, 2, 3],    # Same as S
                south                   => [0, 3, 2, 3],
                southsouthwest          => [0, 3, 2, 3],    # Same as S
                southwest               => [0, 1, 2, 3],
                westsouthwest           => [0, 1, 0, 3],    # Same as W
                west                    => [0, 1, 0, 3],
                westnorthwest           => [0, 1, 0, 3],    # Same as W
                northwest               => [2, 1, 0, 3],
                northnorthwest          => [0, 1, 2, 1],    # Same as N
                up                      => [0, 0],
                down                    => [0, 0],
            },
            # Anchor hashes - converts a standard primary direction into a Gtk2 anchor constant, so
            #   that exit tags can be drawn in the right position
            constGtkAnchorHash          => {
                north                   => 'GTK_ANCHOR_S',
                northnortheast          => 'GTK_ANCHOR_S',  # Same as N/no GTK constant for NNE, etc
                northeast               => 'GTK_ANCHOR_SW',
                eastnortheast           => 'GTK_ANCHOR_W',  # Same as E
                east                    => 'GTK_ANCHOR_W',
                eastsoutheast           => 'GTK_ANCHOR_W',  # Same as E
                southeast               => 'GTK_ANCHOR_NW',
                southsoutheast          => 'GTK_ANCHOR_N',  # Same as S
                south                   => 'GTK_ANCHOR_N',
                southsouthwest          => 'GTK_ANCHOR_N',  # Same as S
                southwest               => 'GTK_ANCHOR_NE',
                westsouthwest           => 'GTK_ANCHOR_E',  # Same as W
                west                    => 'GTK_ANCHOR_E',
                westnorthwest           => 'GTK_ANCHOR_E',  # Same as w
                northwest               => 'GTK_ANCHOR_SE',
                northnorthwest          => 'GTK_ANCHOR_S',  # Same as N
            },

            # Hashes set at the beginning of every draw cycle (i.e. every call to $self->drawObjs)
            #   by $self->preDrawPositions and $self->preDrawExits (see the comments in these
            #   functions for a longer explanation)
            # Calculates the position of each type of exit, and of a few room components, relative
            #   to their gridblocks, to make the drawing of rooms and exits much quicker
            blockCornerXPosPixels       => undef,
            blockCornerYPosPixels       => undef,
            blockCentreXPosPixels       => undef,
            blockCentreYPosPixels       => undef,
            borderCornerXPosPixels      => undef,
            borderCornerYPosPixels      => undef,
            preDrawnIncompleteExitHash  => {},
            preDrawnUncertainExitHash   => {},
            preDrawnLongExitHash        => {},
            preDrawnSquareExitHash      => {},

            # Magnfication list. A list of standard magnification factors used for zooming in or out
            #   from the map
            # Each GA::Obj::Regionmap object has its own ->magnification IV, so zooming on one
            #   region doesn't affect the magnification of others
            # When the user zooms in or out, ->magnification is set to one of the values in this
            #   list, and various IVs in GA::Obj::Regionmap (such as ->blockWidthPixels and
            #   ->roomHeightPixels) are changed. When the map is redrawn, everything in it is bigger
            #   (or smaller)
            constMagnifyList            => [
                0.01, 0.02, 0.04, 0.06, 0.08, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9,
                1,
                1.1, 1.2, 1.35, 1.5, 2, 3, 5, 7, 10,
            ],
            # A subset of these magnifications, used as menu items
            constShortMagnifyList       => [
                0.5, 0.8, 1, 1.2, 1.5, 1.75, 2
            ],
            # When some menu items are selected (e.g. View > Room filters > Release markers filter),
            #   a call is made to this session's GA::Obj::WorldModel, which in turn calls every
            #   Automapper window using the model, in order to update its menu. When this happens,
            #   the following flag is set to TRUE, so that updating the menu item doesn't cause
            #   further calls to GA::Obj::WorldModel
            ignoreMenuUpdateFlag        => FALSE,

            # IVs used during a drag operation
            # Flag set to TRUE during drag mode (set from the menu or the toolbar). Normally, it's
            #   necessary to hold down the Alt-Gr key to drag canvas objects; when drag mode is on,
            #   clicks on canvas objects are treated as the start of a drag, rather than a
            #   select/unselect operation)
            # NB During a drag operation initiated with the Alt-Gr key, ->dragModeFlag's value
            #   doesn't change
            dragModeFlag                => FALSE,
            # Flag set to TRUE when a dragging operation starts
            dragFlag                    => FALSE,
            # The canvas object being dragged
            dragCanvasObj               => undef,
            # The GA::ModelObj::Room / GA::Obj::Exit / GA::Obj::MapLabel being dragged
            dragModelObj                => undef,
            # The type of object being dragged - 'room', 'room_tag', 'room_guild', 'exit',
            #   'exit_tag' or 'label'
            dragModelObjType            => undef,
            # The canvas object's initial coordinates on the canvas
            dragInitXPos                => undef,
            dragInitYPos                => undef,
            # The canvas object's current coordinates on the canvas
            dragCurrentXPos             => undef,
            dragCurrentYPos             => undef,
            # When dragging a room, the fake room drawn at the original location (so that the exits
            #   don't look messy)
            dragFakeRoomObj             => undef,
            # When dragging an exit bend, the bend's index in the exit's list of bends (the bend
            #   closest to the start of the exit has the index 0)
            dragBendNum                 => undef,
            # When dragging an exit bend, the initial position of the bend, relative to the start of
            #   the bending section of the exit
            dragBendInitXPos            => undef,
            dragBendInitYPos            => undef,
            # The corresponding IVs for the twin exit, when dragging an exit bend
            dragBendTwinNum             => undef,
            dragBendTwinInitXPos        => undef,
            dragBendTwinInitYPos        => undef,
            # When dragging an exit bend, the exit drawing mode (corresponds to
            #   GA::Obj::WorldModel->drawExitMode)
            dragExitDrawMode            => undef,
        };

        # Bless the object into existence
        bless $self, $class;

        return $self;
    }

    ##################
    # Methods

    # Standard window object functions

    sub winSetup {

        # Called by GA::Obj::Workspace->createGridWin or ->createSimpleGridWin
        # Creates the Gtk2::Window itself
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $title      - The window title; ignored if specified ($self->setWinTitle sets the
        #                   window title)
        #   $listRef    - Reference to a list of functions to call, just after the Gtk2::Window is
        #                   created (can be used to set up further ->signal_connects, if this
        #                   window needs them)
        #
        # Return values
        #   'undef' on improper arguments or if the window can't be opened
        #   1 on success

        my ($self, $title, $listRef, $check) = @_;

        # Local variables
        my $iv;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->winSetup', @_);
        }

        # Don't create a new window, if it already exists
        if ($self->enabledFlag) {

            return undef;
        }

        # Create the Gtk2::Window
        my $winWidget = Gtk2::Window->new('toplevel');
        if (! $winWidget) {

            return undef;

        } else {

            # Store the IV now, as subsequent code needs it
            $self->ivPoke('winWidget', $winWidget);
            $self->ivPoke('winBox', $winWidget);
        }

        # Set up ->signal_connects (other ->signal_connects are set up in the call to
        #   $self->winEnable() )
        $self->setDeleteEvent();            # 'delete-event'
        $self->setFocusOutEvent();          # 'focus-out-event'
        # Set up ->signal_connects specified by the calling function, if any
        if ($listRef) {

            foreach my $func (@$listRef) {

                $self->$func();
            }
        }

        # Set the window title. If $title wasn't specified, use a suitable default title
        $self->setWinTitle();

        # Set the window's default size and position (this will almost certainly be changed before
        #   the call to $self->winEnable() )
        $winWidget->set_default_size(
            $axmud::CLIENT->customGridWinWidth,
            $axmud::CLIENT->customGridWinHeight,
        );

        $winWidget->set_border_width($axmud::CLIENT->constGridBorderPixels);

        # Set the icon list for this window
        $iv = $self->winType . 'WinIconList';
        $winWidget->set_icon_list($axmud::CLIENT->desktopObj->$iv);

        # Draw the widgets used by this window
        if (! $self->drawWidgets()) {

            return undef;
        }

        # The calling function can now move the window into position, before calling
        #   $self->winEnable to make it visible, and to set up any more ->signal_connects()
        return 1;
    }

    sub winEnable {

        # Called by GA::Obj::Workspace->createGridWin or ->createSimpleGridWin
        # After the Gtk2::Window has been setup and moved into position, makes it visible and calls
        #   any further ->signal_connects that must be not be setup until the window is visible
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $listRef    - Reference to a list of functions to call, just after the Gtk2::Window is
        #                   created (can be used to set up further ->signal_connects, if this
        #                   window needs them)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 on success

        my ($self, $listRef, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->winEnable', @_);
        }

        # Make the window appear on the desktop
        $self->winShowAll($self->_objClass . '->winEnable');
        $self->ivPoke('enabledFlag', TRUE);

        # For windows about to be placed on a grid, briefly minimise the window so it doesn't
        #   appear in the centre of the desktop before being moved to its correct workspace, size
        #   and position
        if ($self->workspaceGridObj && $self->winWidget eq $self->winBox) {

            $self->winWidget->iconify();
        }

        # This type of window is unique to its GA::Session (only one can be open at any time, per
        #   session); inform the session it has opened
        $self->session->set_mapWin($self);

        # Set up ->signal_connects specified by the calling function, if any
        if ($listRef) {

            foreach my $func (@$listRef) {

                $self->$func();
            }
        }

        # If the automapper object is in 'track alone' mode, disable the mode
        $self->session->mapObj->set_trackAloneFlag(FALSE);

        return 1;
    }

    sub winDisengage {

        # Should not be called, in general (provides compatibility with other types of window,
        #   whose window objects can be destroyed without closing the windows themselves)
        # If called, this function just calls $self->winDestroy and returns the result
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the window can't be disengaged
        #   1 on success

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->winDisengage', @_);
        }

        return $self->winDestroy();
    }

    sub winDestroy {

        # Called by GA::Obj::WorkspaceGrid->stop or by any other function
        # Updates the automapper object (GA::Obj::Map), informs the parent workspace grid (if this
        #   'grid' window is on a workspace grid) and the desktop object, and then destroys the
        #   Gtk2::Window (if it is open)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the window can't be destroyed or if it has already
        #       been destroyed
        #   1 on success

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->winDestroy', @_);
        }

        if (! $self->winBox) {

            # Window already destroyed in a previous call to this function
            return undef;
        }

        # If the pause window is visible, destroy it
        if ($axmud::CLIENT->busyWin) {

            $self->hidePauseWin();
        }

        # If the automapper object knows the current world model room, and if the Locator task is
        #   running and knows about the current location, and if the world model flag that permits
        #   it is set, and if this Automapper window isn't currently in 'wait' mode, let the
        #   automapper go into 'track alone' mode
        if (
            $self->mapObj->currentRoom
            && $self->session->locatorTask
            && $self->session->locatorTask->roomObj
            && $self->worldModelObj->allowTrackAloneFlag
            && $self->mode ne 'wait'
        ) {
            # Go into 'track alone' mode
            $self->mapObj->set_trackAloneFlag(TRUE);
        }

        # Update the parent GA::Obj::Map in all cases
        $self->mapObj->set_mapWin();

        # Close any 'free' windows for which this window is a parent
        foreach my $winObj ($self->ivValues('childFreeWinHash')) {

            $winObj->winDestroy();
        }

        # Inform the parent workspace grid object (if any)
        if ($self->workspaceGridObj) {

            $self->workspaceGridObj->del_gridWin($self);
        }

        # Inform the desktop object
        $axmud::CLIENT->desktopObj->del_gridWin($self);

        # Destroy the Gtk2::Window
        eval { $self->winBox->destroy(); };
        if ($@) {

            # Window can't be destroyed
            return undef;

        } else {

            $self->ivUndef('winWidget');
            $self->ivUndef('winBox');
        }

        # Inform the ->owner, if there is one
        if ($self->owner) {

            $self->owner->del_winObj($self);
        }

        # This type of window is unique to its GA::Session (only one can be open at any time, per
        #   session); inform the session it has closed
        $self->session->set_mapWin();

        return 1;
    }

#   sub winShowAll {}           # Inherited from GA::Win::Generic

    sub drawWidgets {

        # Called by $self->winSetup
        # Sets up the Gtk2::Window by drawing its widgets
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 on success

        my ($self, $check) = @_;

        # Local variables
        my ($menuBar, $toolbar, $hPaned, $treeViewScroller, $canvasFrame);

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->drawWidgets', @_);
        }

        # Create a packing box
        my $packingBox = Gtk2::VBox->new(FALSE, 0);
        $self->winBox->add($packingBox);
        $packingBox->set_border_width(0);
        # Update IVs immediately
        $self->ivPoke('packingBox', $packingBox);

        # Create a menu (if allowed)
        if ($self->worldModelObj->showMenuBarFlag) {

            $menuBar = $self->enableMenu();
            if ($menuBar) {

                # Pack the widget
                $packingBox->pack_start($menuBar, FALSE, FALSE, 0);
            }
        }

        # Create a toolbar at the top of the window (if allowed)
        if ($self->worldModelObj->showToolbarFlag) {

            $toolbar = $self->enableToolbar();
            if ($toolbar) {

                # Pack the widget
                $packingBox->pack_start($toolbar, FALSE, FALSE, 0);
            }
        }

        # Create a horizontal pane to divide everything under the menu into two, with the treeview
        #   on the left, and everything else on the right (only if both the treeview and the canvas
        #   are shown)
        if ($self->worldModelObj->showTreeViewFlag && $self->worldModelObj->showCanvasFlag) {

            $hPaned = Gtk2::HPaned->new();
            if ($hPaned) {

                # Set the width of the space about to be filled with the treeview
                $hPaned->set_position($self->treeViewWidthPixels);

                # Pack the widget
                $packingBox->pack_start($hPaned, TRUE, TRUE, 0);
                $self->ivPoke('hPaned', $hPaned);
            }
        }

        # Create a treeview (if allowed)
        if ($self->worldModelObj->showTreeViewFlag) {

            $treeViewScroller = $self->enableTreeView();
            if ($treeViewScroller) {

                # Pack the widget
                if ($hPaned) {

                    # Add the treeview's scroller to the left pane
                    $hPaned->add1($treeViewScroller);

                } else {

                    # Pack the treeview directly into the packing box
                    $packingBox->pack_start($treeViewScroller, TRUE, TRUE, 0);
                }

            }
        }

        # Create a canvas (if allowed)
        if ($self->worldModelObj->showCanvasFlag) {

            $canvasFrame = $self->enableCanvas();
            if ($canvasFrame) {

                # Pack the widget
                if ($hPaned) {

                    # Add the frame to the right pane
                    $hPaned->add2($canvasFrame);

                } else {

                    # Pack the frame directly into the packing box
                    $packingBox->pack_start($canvasFrame, TRUE, TRUE, 0);
                }
            }
        }

        return 1;
    }

    sub redrawWidgets {

        # Can be called by any function
        # Redraws some or all of the menu bar, toolbar, treeview and canvas
        # The widgets redrawn are specified by the calling function, but are not redrawn if the
        #   right flags aren't set (e.g. the menu bar isn't redrawn if
        #   GA::Obj::WorldModel->showMenuBarFlag isn't set)
        #
        # Expected arguments
        #   @widgetList - A list of widget names. One or all of the following strings, in any order:
        #                   'menu_bar', 'toolbar', 'treeview', 'canvas'
        #
        # Return values
        #   'undef' on improper arguments or if any of the widgets in @widgetList are unrecognised
        #   1 otherwise

        my ($self, @widgetList) = @_;

        # Local variables
        my (
            $menuBar, $toolbar, $hPaned, $treeViewScroller, $canvasFrame,
            %widgetHash,
        );

        # Check for improper arguments
        if (! @widgetList) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->redrawWidgets', @_);
        }

        # Check that the strings in @widgetList are valid, and add each string into a hash so that
        #   no widget is drawn more than once
        # Initialise the hash of allowed widgets
        %widgetHash = (
            'menu_bar'  => FALSE,
            'toolbar'   => FALSE,
            'treeview'  => FALSE,
            'canvas'    => FALSE,
        );

        # Check everything in @widgetList
        foreach my $name (@widgetList) {

            if (! exists $widgetHash{$name}) {

                return $self->session->writeError(
                    'Unrecognised widget \'' . $name . '\'',
                    $self->_objClass . '->redrawWidgets',
                );

            } else {

                # If the same string appears more than once in @widgetList, we only draw the widget
                #   once
                $widgetHash{$name} = TRUE;
            }
        }

        # Remove the old widgets from the vertical packing box
        if ($self->menuBar) {

            $axmud::CLIENT->desktopObj->removeWidget($self->packingBox, $self->menuBar);
        }

        if ($self->toolbar) {

            $axmud::CLIENT->desktopObj->removeWidget($self->packingBox, $self->toolbar);
        }

        if ($self->hPaned) {

            foreach my $child ($self->hPaned->get_children()) {

                $self->hPaned->remove($child);
            }

            $axmud::CLIENT->desktopObj->removeWidget($self->packingBox, $self->hPaned);

        } else {

            if ($self->treeViewScroller) {

                $axmud::CLIENT->desktopObj->removeWidget(
                    $self->packingBox,
                    $self->treeViewScroller,
                );
            }

            if ($self->canvasFrame) {

                $axmud::CLIENT->desktopObj->removeWidget($self->packingBox, $self->canvasFrame);
            }
        }

        # Redraw the menu bar, if specified (and if allowed)
        if ($self->worldModelObj->showMenuBarFlag) {

            if ($widgetHash{'menu_bar'}) {

                $self->resetMenuBarIVs();

                my $menuBar = $self->enableMenu();
                if ($menuBar) {

                    # Pack the new widget
                    $self->packingBox->pack_start($menuBar,FALSE,FALSE,0);

                } else {

                    # After the error, stop trying to draw menu bars
                    $self->worldModelObj->set_showMenuBarFlag(FALSE);
                }

            # Otherwise, repack the old menu bar
            } elsif ($self->menuBar) {

                $self->packingBox->pack_start($self->menuBar,FALSE,FALSE,0);
            }
        }

        # Redraw the toolbar, if specified (and if allowed)
        if ($self->worldModelObj->showToolbarFlag) {

            if ($widgetHash{'toolbar'}) {

                $self->resetToolbarIVs();

                $toolbar = $self->enableToolbar();
                if ($toolbar) {

                    # Pack the new widget
                    $self->packingBox->pack_start($toolbar, FALSE, FALSE, 0);

                } else {

                    # After the error, stop trying to draw toolbars
                    $self->worldModelObj->set_showToolbarFlag(FALSE);
                }

            # Otherwise, repack the old toolbar
            } elsif ($self->toolbar) {

                $self->packingBox->pack_start($self->toolbar, FALSE, FALSE, 0);
            }
        }

        # Create a new horizontal pane (only if both the treeview and the canvas are allowed)
        if ($self->worldModelObj->showTreeViewFlag && $self->worldModelObj->showCanvasFlag) {

            $hPaned = Gtk2::HPaned->new();
            if ($hPaned) {

                # Set the width of the space about to be filled with the treeview
                $hPaned->set_position($self->treeViewWidthPixels);

                # Pack the widget
                $self->packingBox->pack_start($hPaned, TRUE, TRUE, 0);
                $self->ivPoke('hPaned', $hPaned);

            } else {

                # After the error, stop trying to draw either the treeview or the canvas
                $self->worldModelObj->set_showTreeViewFlag(FALSE);
                $self->worldModelObj->set_showCanvasFlag(FALSE);
            }

        } else {

            # Horizontal pane no longer required
            $self->ivUndef('hPaned');
        }

        # Redraw the treeview, if specified (and if allowed)
        if ($self->worldModelObj->showTreeViewFlag) {

            if ($widgetHash{'treeview'}) {

                $self->resetTreeViewIVs();

                $treeViewScroller = $self->enableTreeView();
                if ($treeViewScroller) {

                    # Pack the new widget
                    if ($hPaned) {

                        # Add the treeview's scroller to the left pane
                        $hPaned->add1($treeViewScroller);

                    } else {

                        # Pack the treeview directly into the packing box
                        $self->packingBox->pack_start($treeViewScroller, TRUE, TRUE, 0);
                    }

                } else {

                    # After the error, stop trying to draw treeviews
                    $self->worldModelObj->set_showTreeViewFlag(FALSE);
                }

            # Otherwise, repack the old treeview
            } elsif ($self->treeViewScroller) {

                if ($hPaned) {

                    # Add the treeview's scroller to the left-hand pane
                    $hPaned->add1($self->treeViewScroller);

                } else {

                    # Pack the treeview directly into the packing box
                    $self->packingBox->pack_start($self->treeViewScroller, TRUE, TRUE, 0);
                }
            }
        }

        # Redraw the canvas, if specified (and if allowed)
        if ($self->worldModelObj->showCanvasFlag) {

            if ($widgetHash{'canvas'}) {

                $self->resetCanvasIVs();

                $canvasFrame = $self->enableCanvas();
                if ($canvasFrame) {

                    # Pack the new widget
                    if ($hPaned) {

                        # Add the frame to the right pane
                        $hPaned->add2($canvasFrame);

                    } else {

                        # Pack the frame directly into the packing box
                        $self->packingBox->pack_start($canvasFrame, TRUE, TRUE, 0);
                    }

                } else {

                    # After the error, stop trying to draw canvases
                    $self->worldModelObj->set_showCanvasFlag(FALSE);
                }

            # Otherwise, repack the old canvas
            } elsif ($self->canvasFrame) {

                if ($hPaned) {

                    # Add the frame to the right-hand pane
                    $hPaned->add2($self->canvasFrame);

                } else {

                    # Pack the frame directly into the packing box
                    $self->packingBox->pack_start($self->canvasFrame, TRUE, TRUE, 0);
                }
            }
        }

        # Now, for each widget that is no longer drawn, set default IVs
        if (! $self->worldModelObj->showMenuBarFlag) {

            $self->resetMenuBarIVs();
        }

        if (! $self->worldModelObj->showToolbarFlag) {

            $self->resetToolbarIVs();
        }

        if (! $self->worldModelObj->showTreeViewFlag || ! $self->worldModelObj->showCanvasFlag) {

            $self->ivUndef('hPaned');
        }

        if (! $self->worldModelObj->showTreeViewFlag) {

            $self->resetTreeViewIVs();
        }

        if (! $self->worldModelObj->showCanvasFlag) {

            $self->resetCanvasIVs();
        }

        # Repack complete
        $self->winShowAll($self->_objClass . '->redrawWidgets');

        return 1;
    }

    # Standard 'map' window object functions

    sub winReset {

        # Called by GA::Obj::Map->openWin to reset an existing Automapper window
        #
        # Expected arguments
        #   $mapObj     - The calling GA::Obj::Map object
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $mapObj, $check) = @_;

        # Check for improper arguments
        if (! defined $mapObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->winReset', @_);
        }

        # Set new Perl object component IVs
        $self->ivPoke('mapObj', $mapObj);
        $self->ivPoke('worldModelObj', $self->session->worldModelObj);

        # Reset the map, which destroys all existing canvas objects. The FALSE argument sets the
        #   background colour to (default) white, to show there's no current region
        $self->resetMap(FALSE);

        # Reset the current region
        $self->ivUndef('currentRegionmap');

        # Reset selected objects
        $self->ivUndef('selectedRoom');
        $self->ivUndef('selectedExit');
        $self->ivUndef('selectedRoomTag');
        $self->ivUndef('selectedRoomGuild');
        $self->ivUndef('selectedLabel');
        $self->ivEmpty('selectedRoomHash');
        $self->ivEmpty('selectedExitHash');
        $self->ivEmpty('selectedRoomTagHash');
        $self->ivEmpty('selectedRoomGuildHash');
        $self->ivEmpty('selectedLabelHash');

        # Reset other IVs to their default values
        $self->ivPoke('freeClickMode', 'default');
        $self->ivPoke('mode', 'wait');
        $self->ivUndef('showChar');     # Show character visits for the current character

        # Reset the title bar
        $self->setWinTitle();
        # Reset window components
        $self->redrawWidgets('menu_bar', 'toolbar', 'treeview', 'canvas');

        return 1;
    }

    sub winUpdate {

        # Called by GA::Session->spinMaintainLoop or by any other code
        # Lets the Automapper window do anything it needs to do, in order to update itself. At the
        #   moment, we only draw any objects which have been marked to be drawn, but other code
        #   might be added here later
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->winUpdate', @_);
        }

        # If any objects have been marked to be drawn, draw them
        $self->doDraw();

        return 1;
    }

    # ->signal_connects

    sub setDeleteEvent {

        # Called by $self->winSetup
        # Set up a ->signal_connect to watch out for the user manually closing the 'map' window
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->setDeleteEvent', @_);
        }

        $self->winBox->signal_connect('delete-event' => sub {

            # Prevent Gtk2 from taking action directly. Instead redirect the request to
            #   $self->winDestroy, which does things like resetting a portion of the workspace
            #   grid, as well as actually destroying the window
            return $self->winDestroy();
        });

        return 1;
    }

    sub setFocusOutEvent {

        # Called by $self->winSetup
        # Set up a ->signal_connect to watch out for the 'map' window losing the focus
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

             return $axmud::CLIENT->writeImproper($self->_objClass . '->setFocusInEvent', @_);
        }

        $self->winBox->signal_connect('focus-out-event' => sub {

            my ($widget, $event) = @_;

            # If the tooltips are visible, hide them
            if ($event->type eq 'focus-change' && $self->canvasTooltipFlag) {

               $self->hideTooltips();
            }
        });

        return 1;
    }

    # Other functions

    sub resetMenuBarIVs {

        # Called by $self->redrawWidgets at certain points, to reset the IVs storing details about
        #   the menu bar back to their defaults
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->resetMenuBarIVs', @_);
        }

        $self->ivUndef('menuBar');
        $self->ivEmpty('menuToolItemHash');

        return 1;
    }

    sub resetToolbarIVs {

        # Called by $self->redrawWidgets at certain points, to reset the IVs storing details about
        #   the toolbar back to their defaults
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->resetToolbarIVs', @_);
        }

        $self->ivUndef('toolbar');
        $self->ivPoke('toolbarCurrentSet', 0);
        $self->ivUndef('toolbarSwitchIcon');
        $self->ivUndef('toolbarMainSeparator');
        $self->ivEmpty('toolbarButtonHash');

        return 1;
    }

    sub resetTreeViewIVs {

        # Called by $self->redrawWidgets at certain points, to reset the IVs storing details about
        #   the treeview back to their defaults
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->resetTreeViewIVs', @_);
        }

        $self->ivUndef('treeViewModel');
        $self->ivUndef('treeView');
        $self->ivUndef('treeViewScroller');
        $self->ivUndef('treeViewSelectedLine');
        $self->ivEmpty('treeViewRegionHash');
        $self->ivEmpty('treeViewPointerHash');

        return 1;
    }

    sub resetCanvasIVs {

        # Called by $self->redrawWidgets at certain points, to reset the IVs storing details about
        #   the canvas back to their defaults
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->resetCanvasIVs', @_);
        }

        $self->ivUndef('canvas');
        $self->ivUndef('canvasRoot');
        $self->ivUndef('canvasFrame');
        $self->ivUndef('canvasScroller');
        $self->ivUndef('canvasHAdjustment');
        $self->ivUndef('canvasVAdjustment');
        $self->ivUndef('canvasBackground');
        $self->ivUndef('canvasTooltipObj');
        $self->ivUndef('canvasTooltipObjType');
        $self->ivUndef('canvasTooltipFlag');

        return 1;
    }

    # Menu widget methods

    sub enableMenu {

        # Called by $self->drawWidgets
        # Sets up the Automapper window's Gtk2::MenuBar widget
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::MenuBar created

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableMenu', @_);
        }

        # Create the menu bar
        my $menuBar = Gtk2::MenuBar->new();
        if (! $menuBar) {

            return undef;
        }

        # 'File' column
        my $column_file = $self->enableFileColumn();
        my $item_file = Gtk2::MenuItem->new('_File');
        $item_file->set_submenu($column_file);
        $menuBar->append($item_file);

        # 'Edit' column
        my $column_edit = $self->enableEditColumn();
        my $item_edit = Gtk2::MenuItem->new('_Edit');
        $item_edit->set_submenu($column_edit);
        $menuBar->append($item_edit);

        # 'View' column
        my $column_view = $self->enableViewColumn();
        my $item_view = Gtk2::MenuItem->new('_View');
        $item_view->set_submenu($column_view);
        $menuBar->append($item_view);

        # 'Mode' column
        my $column_mode = $self->enableModeColumn();
        my $item_mode = Gtk2::MenuItem->new('_Mode');
        $item_mode->set_submenu($column_mode);
        $menuBar->append($item_mode);

        # 'Regions' column
        my $column_regions = $self->enableRegionsColumn();
        my $item_regions = Gtk2::MenuItem->new('_Regions');
        $item_regions->set_submenu($column_regions);
        $menuBar->append($item_regions);

        # 'Rooms' column
        my $column_rooms = $self->enableRoomsColumn();
        my $item_rooms = Gtk2::MenuItem->new('R_ooms');
        $item_rooms->set_submenu($column_rooms);
        $menuBar->append($item_rooms);

        # 'Exits' column
        my $column_exits = $self->enableExitsColumn();
        my $item_exits = Gtk2::MenuItem->new('E_xits');
        $item_exits->set_submenu($column_exits);
        $menuBar->append($item_exits);

        # 'Labels' column
        my $column_labels = $self->enableLabelsColumn();
        my $item_labels = Gtk2::MenuItem->new('_Labels');
        $item_labels->set_submenu($column_labels);
        $menuBar->append($item_labels);

        # Store the widget
        $self->ivPoke('menuBar', $menuBar);

        # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
        $self->restrictWidgets();

        # Setup complete
        return $menuBar;
    }

    sub enableFileColumn {

        # Called by $self->enableMenu
        # Sets up the 'File' column of the Automapper window's menu bar
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableFileColumn', @_);
        }

        # Set up column
        my $column_file = Gtk2::Menu->new();
        if (! $column_file) {

            return undef;
        }

        my $item_loadModel = Gtk2::MenuItem->new('_Load world model');
        $item_loadModel->signal_connect('activate' => sub {

            # $self->winReset will be called by $self->set_worldModelObj when the ';load' command
            #   has finished its work
            # NB Force pseudo command mode 'win_error' in this menu column (success system messages
            #   in the 'main' window; errors/improper arguments messages shown in a 'dialogue'
            #   window)
            $self->session->pseudoCmd('load -m', 'win_error');
        });
        $column_file->append($item_loadModel);

        my $item_loadAll = Gtk2::ImageMenuItem->new('L_oad all files');
        $item_loadAll->signal_connect('activate' => sub {

            # The ';load' command will  $self->winReset when finished

            $self->session->pseudoCmd('load', 'win_error');
        });
        my $img_loadAll = Gtk2::Image->new_from_stock('gtk-open', 'menu');
        $item_loadAll->set_image($img_loadAll);
        $column_file->append($item_loadAll);

        $column_file->append(Gtk2::SeparatorMenuItem->new());   # Separator

        my $item_saveModel = Gtk2::MenuItem->new('_Save world model');
        $item_saveModel->signal_connect('activate' => sub {

            # Do a forced save. The ';save' command sets $self->freeClickMode back to 'default'
            $self->session->pseudoCmd('save -m -f', 'win_error');
        });
        $column_file->append($item_saveModel);

        my $item_saveAll = Gtk2::ImageMenuItem->new('S_ave all files');
        $item_saveAll->signal_connect('activate' => sub {

            # Do a forced save. The ';save' command sets $self->freeClickMode back to 'default'
            $self->session->pseudoCmd('save -f', 'win_error');
        });
        my $img_saveAll = Gtk2::Image->new_from_stock('gtk-save', 'menu');
        $item_saveAll->set_image($img_saveAll);
        $column_file->append($item_saveAll);

        $column_file->append(Gtk2::SeparatorMenuItem->new());   # Separator

        my $item_importModel = Gtk2::MenuItem->new('_Import/load world model...');
        $item_importModel->signal_connect('activate' => sub {

            $self->importModelCallback();
        });
        $column_file->append($item_importModel);

        my $item_exportModel = Gtk2::MenuItem->new('Save/_export world model...');
        $item_exportModel->signal_connect('activate' => sub {

            $self->exportModelCallback();
        });
        $column_file->append($item_exportModel);

        $column_file->append(Gtk2::SeparatorMenuItem->new());   # Separator

        my $item_closeWindow = Gtk2::ImageMenuItem->new('_Close window');
        $item_closeWindow->signal_connect('activate' => sub {

            $self->winDestroy();
        });
        my $img_closeWindow = Gtk2::Image->new_from_stock('gtk-quit', 'menu');
        $item_closeWindow->set_image($img_closeWindow);
        $column_file->append($item_closeWindow);

        # Setup complete
        return $column_file;
    }

    sub enableEditColumn {

        # Called by $self->enableMenu
        # Sets up the 'Edit' column of the Automapper window's menu bar
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Local variables
        my $winObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableEditColumn', @_);
        }

        # Set up column
        my $column_edit = Gtk2::Menu->new();
        if (! $column_edit) {

            return undef;
        }

        my $item_selectAll = Gtk2::MenuItem->new('_Select all');
        $item_selectAll->signal_connect('activate' => sub {

            $self->selectAllCallback();
        });
        $column_edit->append($item_selectAll);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'select_all', $item_selectAll);

            # 'Select' submenu
            my $subMenu_select = Gtk2::Menu->new();

            my $item_selectAllRooms = Gtk2::MenuItem->new('Select all _rooms');
            $item_selectAllRooms->signal_connect('activate' => sub {

                $self->selectAllCallback('room');
            });
            $subMenu_select->append($item_selectAllRooms);

            my $item_selectAllExits = Gtk2::MenuItem->new('Select all _exits');
            $item_selectAllExits->signal_connect('activate' => sub {

                $self->selectAllCallback('exit');
            });
            $subMenu_select->append($item_selectAllExits);

            my $item_selectAllRoomTags = Gtk2::MenuItem->new('Select all room _tags');
            $item_selectAllRoomTags->signal_connect('activate' => sub {

                $self->selectAllCallback('room_tag');
            });
            $subMenu_select->append($item_selectAllRoomTags);

            my $item_selectAllRoomGuilds = Gtk2::MenuItem->new('Select all room g_uilds');
            $item_selectAllRoomGuilds->signal_connect('activate' => sub {

                $self->selectAllCallback('room_guild');
            });
            $subMenu_select->append($item_selectAllRoomGuilds);

            my $item_selectAllLabels = Gtk2::MenuItem->new('Select all _labels');
            $item_selectAllLabels->signal_connect('activate' => sub {

                $self->selectAllCallback('label');
            });
            $subMenu_select->append($item_selectAllLabels);

        my $item_select = Gtk2::MenuItem->new('S_elect');
        $item_select->set_submenu($subMenu_select);
        $column_edit->append($item_select);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'select', $item_select);

        my $item_unselectAll = Gtk2::MenuItem->new('_Unselect all');
        $item_unselectAll->signal_connect('activate' => sub {

            $self->setSelectedObj();
        });
        $column_edit->append($item_unselectAll);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'unselect_all', $item_unselectAll);

        $column_edit->append(Gtk2::SeparatorMenuItem->new());   # Separator

        my $item_moveSelected = Gtk2::MenuItem->new('_Move selected rooms...');
        $item_moveSelected->signal_connect('activate' => sub {

            $self->moveSelectedRoomsCallback();
        });
        $column_edit->append($item_moveSelected);
        # (Requires $self->currentRegionmap and one or more selected rooms)
        $self->ivAdd('menuToolItemHash', 'move_selected', $item_moveSelected);

        my $item_moveSelectedToClick = Gtk2::MenuItem->new('Move selected rooms to _click');
        $item_moveSelectedToClick->signal_connect('activate' => sub {

            # Set the free clicking mode: $self->mouseClickEvent will move the objects  when the
            #   user next clicks on an empty part of the map
            $self->ivPoke('freeClickMode', 'move_room');
        });
        $column_edit->append($item_moveSelectedToClick);
        # (Requires $self->currentRegionmap and one or more selected rooms)
        $self->ivAdd('menuToolItemHash', 'move_to_click', $item_moveSelectedToClick);

        $column_edit->append(Gtk2::SeparatorMenuItem->new());   # Separator

            # 'Search' submenu
            my $subMenu_search = Gtk2::Menu->new();

            my $item_searchModel = Gtk2::MenuItem->new('Search _world model...');
            $item_searchModel->signal_connect('activate' => sub {

                # Open a 'pref' window to conduct the search
                $self->createFreeWin(
                    'Games::Axmud::PrefWin::Search',
                    $self,
                    $self->session,
                    'World model search',
                );
            });
            $subMenu_search->append($item_searchModel);

            $subMenu_search->append(Gtk2::SeparatorMenuItem->new());   # Separator

            my $item_findRoom = Gtk2::MenuItem->new('Find _room...');
            $item_findRoom->signal_connect('activate' => sub {

                $self->findRoomCallback();
            });
            $subMenu_search->append($item_findRoom);

            my $item_findExit = Gtk2::MenuItem->new('Find _exit...');
            $item_findExit->signal_connect('activate' => sub {

                $self->findExitCallback();
            });
            $subMenu_search->append($item_findExit);

        my $item_search = Gtk2::ImageMenuItem->new('Se_arch');
        my $img_search = Gtk2::Image->new_from_stock('gtk-find', 'menu');
        $item_search->set_image($img_search);
        $item_search->set_submenu($subMenu_search);
        $column_edit->append($item_search);

        $column_edit->append(Gtk2::SeparatorMenuItem->new());   # Separator

            # 'Generate reports' submenu
            my $subMenu_reports = Gtk2::Menu->new();

            my $item_showSummary = Gtk2::MenuItem->new('_Show general report');
            $item_showSummary->signal_connect('activate' => sub {

                # (Don't use $self->pseudoCmdMode - we want to see the footer messages)
                $self->session->pseudoCmd('modelreport', 'show_all');
            });
            $subMenu_reports->append($item_showSummary);

            my $item_showCurrentRegion = Gtk2::MenuItem->new('S_how current region');
            $item_showCurrentRegion->signal_connect('activate' => sub {

                $self->session->pseudoCmd(
                    'modelreport -r <' . $self->currentRegionmap->name . '>',
                    'show_all',
                );
            });
            $subMenu_reports->append($item_showCurrentRegion);
            # (Requires $self->currentRegionmap)
            $self->ivAdd('menuToolItemHash', 'report_region', $item_showCurrentRegion);

            $subMenu_reports->append(Gtk2::SeparatorMenuItem->new());  # Separator

                # 'Character visits' sub-submenu
                my $subSubMenu_visits = Gtk2::Menu->new();

                my $item_visits1 = Gtk2::MenuItem->new('_All regions/characters');
                $item_visits1->signal_connect('activate' => sub {

                    $self->session->pseudoCmd('modelreport -v', 'show_all');
                });
                $subSubMenu_visits->append($item_visits1);

                my $item_visits2 = Gtk2::MenuItem->new('Current _region');
                $item_visits2->signal_connect('activate' => sub {

                    $self->session->pseudoCmd(
                        'modelreport -v -r <' . $self->currentRegionmap->name . '>',
                        'show_all',
                    );
                });
                $subSubMenu_visits->append($item_visits2);
                # (Requires $self->currentRegionmap)
                $self->ivAdd('menuToolItemHash', 'report_visits_2', $item_visits2);

                my $item_visits3 = Gtk2::MenuItem->new('Current c_haracter');
                $item_visits3->signal_connect('activate' => sub {

                    $self->session->pseudoCmd(
                        'modelreport -v -c <' . $self->session->currentChar->name . '>',
                        'show_all',
                    );
                });
                $subSubMenu_visits->append($item_visits3);
                # (Requires current character profile)
                $self->ivAdd('menuToolItemHash', 'report_visits_3', $item_visits3);

                my $item_visits4 = Gtk2::MenuItem->new('_Current region/character');
                $item_visits4->signal_connect('activate' => sub {

                    $self->session->pseudoCmd(
                        'modelreport -v -r <' . $self->currentRegionmap->name . '>' . ' -c <'
                        . $self->session->currentChar->name . '>',
                        'show_all',
                    );
                });
                $subSubMenu_visits->append($item_visits4);
                # (Requires $self->currentRegionmap and current character profile)
                $self->ivAdd('menuToolItemHash', 'report_visits_4', $item_visits4);

            my $item_visits = Gtk2::MenuItem->new('_Character visits');
            $item_visits->set_submenu($subSubMenu_visits);
            $subMenu_reports->append($item_visits);

                # 'Room guilds' sub-submenu
                my $subSubMenu_guilds = Gtk2::Menu->new();

                my $item_guilds1 = Gtk2::MenuItem->new('_All regions/guilds');
                $item_guilds1->signal_connect('activate' => sub {

                    $self->session->pseudoCmd('modelreport -g', 'show_all');
                });
                $subSubMenu_guilds->append($item_guilds1);

                my $item_guilds2 = Gtk2::MenuItem->new('Current _region');
                $item_guilds2->signal_connect('activate' => sub {

                    $self->session->pseudoCmd(
                        'modelreport -g -r <' . $self->currentRegionmap->name . '>',
                        'show_all',
                    );
                });
                $subSubMenu_guilds->append($item_guilds2);
                # (Requires $self->currentRegionmap)
                $self->ivAdd('menuToolItemHash', 'report_guilds_2', $item_guilds2);

                my $item_guilds3 = Gtk2::MenuItem->new('C_urrent guild');
                $item_guilds3->signal_connect('activate' => sub {

                    $self->session->pseudoCmd(
                        'modelreport -g -n <' . $self->session->currentGuild->name . '>',
                        'show_all',
                    );
                });
                $subSubMenu_guilds->append($item_guilds3);
                # (Requires current guild profile)
                $self->ivAdd('menuToolItemHash', 'report_guilds_3', $item_guilds3);

                my $item_guilds4 = Gtk2::MenuItem->new('_Current region/guild');
                $item_guilds4->signal_connect('activate' => sub {

                    $self->session->pseudoCmd(
                        'modelreport -g -r <' . $self->currentRegionmap->name . '>' . ' -n <'
                        . $self->session->currentGuild->name . '>',
                        'show_all',
                    );
                });
                $subSubMenu_guilds->append($item_guilds4);
                # (Requires $self->currentRegionmap and current guild profile)
                $self->ivAdd('menuToolItemHash', 'report_guilds_4', $item_guilds4);

            my $item_guilds = Gtk2::MenuItem->new('_Room guilds');
            $item_guilds->set_submenu($subSubMenu_guilds);
            $subMenu_reports->append($item_guilds);

                # 'Room flags' sub-submenu
                my $subSubMenu_roomFlags = Gtk2::Menu->new();

                my $item_roomFlags1 = Gtk2::MenuItem->new('_All regions/flags');
                $item_roomFlags1->signal_connect('activate' => sub {

                    $self->session->pseudoCmd('modelreport -f', 'show_all');
                });
                $subSubMenu_roomFlags->append($item_roomFlags1);

                my $item_roomFlags2 = Gtk2::MenuItem->new('Current _region');
                $item_roomFlags2->signal_connect('activate' => sub {

                    $self->session->pseudoCmd(
                        'modelreport -f -r <' . $self->currentRegionmap->name . '>',
                        'show_all',
                    );
                });
                $subSubMenu_roomFlags->append($item_roomFlags2);
                # (Requires $self->currentRegionmap)
                $self->ivAdd('menuToolItemHash', 'report_flags_2', $item_roomFlags2);

                my $item_roomFlags3 = Gtk2::MenuItem->new('_Specify flag...');
                $item_roomFlags3->signal_connect('activate' => sub {

                    my (
                        $choice,
                        @list,
                    );

                    @list = sort {lc($a) cmp lc($b)}
                                ($self->worldModelObj->ivKeys('roomFlagTextHash'));

                    $choice = $self->showComboDialogue(
                        'Select room flag',
                        'Select one of the world model\'s room flags',
                        FALSE,
                        \@list,
                    );

                    if ($choice) {

                        $self->session->pseudoCmd(
                            'modelreport -f -l <' . $choice . '>',
                            'show_all',
                        );
                    }
                });
                $subSubMenu_roomFlags->append($item_roomFlags3);

                my $item_roomFlags4 = Gtk2::MenuItem->new('_Current region/specify flag...');
                $item_roomFlags4->signal_connect('activate' => sub {

                    my (
                        $choice,
                        @list,
                    );

                    @list = sort {lc($a) cmp lc($b)}
                                ($self->worldModelObj->ivKeys('roomFlagTextHash')),

                    $choice = $self->showComboDialogue(
                        'Select room flag',
                        'Select one of the world model\'s room flags',
                        FALSE,
                        \@list,
                    );

                    if ($choice) {

                        $self->session->pseudoCmd(
                            'modelreport -f -r <' . $self->currentRegionmap->name . '>' . ' -l <'
                            . $choice . '>',
                            'show_all',
                        );
                    }
                });
                $subSubMenu_roomFlags->append($item_roomFlags4);
                # (Requires $self->currentRegionmap)
                $self->ivAdd('menuToolItemHash', 'report_flags_4', $item_roomFlags4);

            my $item_roomFlags = Gtk2::MenuItem->new('Room _flags');
            $item_roomFlags->set_submenu($subSubMenu_roomFlags);
            $subMenu_reports->append($item_roomFlags);

                 # 'Rooms' sub-submenu
                my $subSubMenu_rooms = Gtk2::Menu->new();

                my $item_rooms1 = Gtk2::MenuItem->new('_All regions');
                $item_rooms1->signal_connect('activate' => sub {

                    $self->session->pseudoCmd('modelreport -m', 'show_all');
                });
                $subSubMenu_rooms->append($item_rooms1);

                my $item_rooms2 = Gtk2::MenuItem->new('_Current region');
                $item_rooms2->signal_connect('activate' => sub {

                    $self->session->pseudoCmd(
                        'modelreport -m -r <' . $self->currentRegionmap->name . '>',
                        'show_all',
                    );
                });
                $subSubMenu_rooms->append($item_rooms2);
                # (Requires $self->currentRegionmap)
                $self->ivAdd('menuToolItemHash', 'report_rooms_2', $item_rooms2);

            my $item_rooms = Gtk2::MenuItem->new('_Rooms');
            $item_rooms->set_submenu($subSubMenu_rooms);
            $subMenu_reports->append($item_rooms);

                 # 'Exits' sub-submenu
                my $subSubMenu_exits = Gtk2::Menu->new();

                my $item_exits1 = Gtk2::MenuItem->new('_All regions');
                $item_exits1->signal_connect('activate' => sub {

                    $self->session->pseudoCmd('modelreport -x', 'show_all');
                });
                $subSubMenu_exits->append($item_exits1);

                my $item_exits2 = Gtk2::MenuItem->new('_Current region');
                $item_exits2->signal_connect('activate' => sub {

                    $self->session->pseudoCmd(
                        'modelreport -x -r <' . $self->currentRegionmap->name . '>',
                        'show_all',
                    );
                });
                $subSubMenu_exits->append($item_exits2);
                # (Requires $self->currentRegionmap)
                $self->ivAdd('menuToolItemHash', 'report_exits_2', $item_exits2);

            my $item_exits = Gtk2::MenuItem->new('_Exits');
            $item_exits->set_submenu($subSubMenu_exits);
            $subMenu_reports->append($item_exits);

        my $item_reports = Gtk2::MenuItem->new('_Generate reports');
        $item_reports->set_submenu($subMenu_reports);
        $column_edit->append($item_reports);

        my $item_resetVisits = Gtk2::MenuItem->new('Reset c_haracter visits...');
        $item_resetVisits->signal_connect('activate' => sub {

            $self->resetVisitsCallback();
        });
        $column_edit->append($item_resetVisits);

        my $item_resetRemote = Gtk2::MenuItem->new('Reset remo_te data...');
        $item_resetRemote->signal_connect('activate' => sub {

            $self->resetRemoteCallback();
        });
        $column_edit->append($item_resetRemote);

        $column_edit->append(Gtk2::SeparatorMenuItem->new());   # Separator

        my $item_editDict = Gtk2::ImageMenuItem->new('Edit current _dictionary...');
        my $img_editDict = Gtk2::Image->new_from_stock('gtk-edit', 'menu');
        $item_editDict->set_image($img_editDict);
        $item_editDict->signal_connect('activate' => sub {

            # Open an 'edit' window for the current dictionary
            $self->createFreeWin(
                'Games::Axmud::EditWin::Dict',
                $self,
                $self->session,
                'Edit dictionary \'' . $self->session->currentDict->name . '\'',
                $self->session->currentDict,
                FALSE,          # Not temporary
            );
        });
        $column_edit->append($item_editDict);

        my $item_addWords = Gtk2::MenuItem->new('Add dictionary _words...');
        $item_addWords->signal_connect('activate' => sub {

            $self->createFreeWin(
                'Games::Axmud::OtherWin::QuickWord',
                $self,
                $self->session,
                'Quick word adder',
            );
        });
        $column_edit->append($item_addWords);

        my $item_updateModel = Gtk2::MenuItem->new('Update mode_l words');
        $item_updateModel->signal_connect('activate' => sub {

            # Use pseudo-command mode 'win_error' - show success messages in the 'main' window,
            #   error messages in 'dialogue' window
            $self->session->pseudoCmd('updatemodel -t', 'win_error');
        });
        $column_edit->append($item_updateModel);

        $column_edit->append(Gtk2::SeparatorMenuItem->new());   # Separator

        my $item_setupWizard = Gtk2::ImageMenuItem->new('Run Locator wi_zard...');
        my $img_setupWizard = Gtk2::Image->new_from_stock('gtk-page-setup', 'menu');
        $item_setupWizard->set_image($img_setupWizard);
        $item_setupWizard->signal_connect('activate' => sub {

            if ($self->session->wizWin) {

                # Some kind of 'wiz' window is already open
                $self->session->wizWin->restoreFocus();

            } else {

                # Open the Locator wizard window
                $self->session->pseudoCmd('locatorwizard', $self->pseudoCmdMode);
            }
        });
        $column_edit->append($item_setupWizard);

        my $item_editModel = Gtk2::ImageMenuItem->new('Edit w_orld model...');
        my $img_editModel = Gtk2::Image->new_from_stock('gtk-edit', 'menu');
        $item_editModel->set_image($img_editModel);
        $item_editModel->signal_connect('activate' => sub {

            # Open an 'edit' window for the world model
            $self->createFreeWin(
                'Games::Axmud::EditWin::WorldModel',
                $self,
                $self->session,
                'Edit world model',
                $self->session->worldModelObj,
                FALSE,                          # Not temporary
            );
        });
        $column_edit->append($item_editModel);

        # Setup complete
        return $column_edit;
    }

    sub enableViewColumn {

        # Sets up the 'View' column of the Automapper window's menu bar
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Local variables
        my (
            $item_group,
            @magList, @shortMagList, @initList, @interiorList,
            %interiorHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableViewColumn', @_);
        }

        # Set up column
        my $column_view = Gtk2::Menu->new();
        if (! $column_view) {

            return undef;
        }

            # 'Window components' submenu
            my $subMenu_winComponents = Gtk2::Menu->new();

            my $item_showMenuBar = Gtk2::CheckMenuItem->new('Show menu_bar');
            $item_showMenuBar->set_active($self->worldModelObj->showMenuBarFlag);
            $item_showMenuBar->signal_connect('toggled' => sub {

                $self->worldModelObj->toggleWinComponents(
                    'showMenuBarFlag',
                    $item_showMenuBar->get_active(),
                );
            });
            $subMenu_winComponents->append($item_showMenuBar);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'show_menu_bar', $item_showMenuBar);

            my $item_showToolbar = Gtk2::CheckMenuItem->new('Show _toolbar');
            $item_showToolbar->set_active($self->worldModelObj->showToolbarFlag);
            $item_showToolbar->signal_connect('toggled' => sub {

                $self->worldModelObj->toggleWinComponents(
                    'showToolbarFlag',
                    $item_showToolbar->get_active(),
                );
            });
            $subMenu_winComponents->append($item_showToolbar);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'show_toolbar', $item_showToolbar);

            my $item_showTreeView = Gtk2::CheckMenuItem->new('Show _regions');
            $item_showTreeView->set_active($self->worldModelObj->showTreeViewFlag);
            $item_showTreeView->signal_connect('toggled' => sub {

                $self->worldModelObj->toggleWinComponents(
                    'showTreeViewFlag',
                    $item_showTreeView->get_active(),
                );
            });
            $subMenu_winComponents->append($item_showTreeView);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'show_treeview', $item_showTreeView);

            my $item_showCanvas = Gtk2::CheckMenuItem->new('Show _map');
            $item_showCanvas->set_active($self->worldModelObj->showCanvasFlag);
            $item_showCanvas->signal_connect('toggled' => sub {

                $self->worldModelObj->toggleWinComponents(
                    'showCanvasFlag',
                    $item_showCanvas->get_active(),
                );
            });
            $subMenu_winComponents->append($item_showCanvas);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'show_canvas', $item_showCanvas);

            $subMenu_winComponents->append(Gtk2::SeparatorMenuItem->new());  # Separator

            my $item_redrawWindow = Gtk2::MenuItem->new('Re_draw window');
            $item_redrawWindow->signal_connect('activate' => sub {

                $self->redrawWidgets('menu_bar', 'toolbar', 'treeview', 'canvas');
            });
            $subMenu_winComponents->append($item_redrawWindow);

            $subMenu_winComponents->append(Gtk2::SeparatorMenuItem->new());  # Separator

            my $item_resetList = Gtk2::MenuItem->new('Re_set region list');
            $item_resetList->signal_connect('activate' => sub {

                $self->worldModelObj->resetRegionList();
            });
            $subMenu_winComponents->append($item_resetList);

            my $item_reverseList = Gtk2::MenuItem->new('Re_verse region list');
            $item_reverseList->signal_connect('activate' => sub {

                $self->worldModelObj->reverseRegionList();
            });
            $subMenu_winComponents->append($item_reverseList);

            my $item_moveCurrentRegion = Gtk2::MenuItem->new('Move _current region to top');
            $item_moveCurrentRegion->signal_connect('activate' => sub {

                $self->worldModelObj->moveRegionToTop($self->currentRegionmap);
            });
            $subMenu_winComponents->append($item_moveCurrentRegion);
            # (Requires $self->currentRegionmap for a region that doesn't have a parent region)
            $self->ivAdd('menuToolItemHash', 'move_region_top', $item_moveCurrentRegion);

        my $item_windowComponents = Gtk2::MenuItem->new('_Window components');
        $item_windowComponents->set_submenu($subMenu_winComponents);
        $column_view->append($item_windowComponents);

            # 'Current room' submenu
            my $subMenu_currentRoom = Gtk2::Menu->new();

            my $item_radio1 = Gtk2::RadioMenuItem->new(undef, 'Draw _normal room');
            $item_radio1->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio1->get_active()) {

                    $self->worldModelObj->switchMode(
                        'currentRoomMode',
                        'single',           # New value of ->currentRoomMode
                        FALSE,              # No call to ->drawRegion; the current room is redrawn
                        'normal_current_mode',
                    );
                }
            });
            my $item_group0 = $item_radio1->get_group();
            $subMenu_currentRoom->append($item_radio1);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'normal_current_mode', $item_radio1);

            my $item_radio2 = Gtk2::RadioMenuItem->new($item_group0, 'Draw _emphasised room');
            if ($self->worldModelObj->currentRoomMode eq 'double') {

                $item_radio2->set_active(TRUE);
            }
            $item_radio2->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio2->get_active()) {

                    $self->worldModelObj->switchMode(
                        'currentRoomMode',
                        'double',           # New value of ->currentRoomMode
                        FALSE,              # No call to ->drawRegion; the current room is redrawn
                        'empahsise_current_room',
                    );
                }
            });
            $subMenu_currentRoom->append($item_radio2);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'empahsise_current_room', $item_radio2);

            my $item_radio3 = Gtk2::RadioMenuItem->new($item_group0, 'Draw _filled-in room');
            if ($self->worldModelObj->currentRoomMode eq 'interior') {

                $item_radio3->set_active(TRUE);
            }
            $item_radio3->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio3->get_active()) {

                    $self->worldModelObj->switchMode(
                        'currentRoomMode',
                        'interior',         # New value of ->currentRoomMode
                        FALSE,              # No call to ->drawRegion; the current room is redrawn
                        'fill_in_current_room',
                    );
                }
            });
            $subMenu_currentRoom->append($item_radio3);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'fill_in_current_room', $item_radio3);

        my $item_currentRoom = Gtk2::MenuItem->new('_Draw current room');
        $item_currentRoom->set_submenu($subMenu_currentRoom);
        $column_view->append($item_currentRoom);

            # 'Room filters' submenu
            my $subMenu_roomFilters = Gtk2::Menu->new();

            my $item_releaseAllFilters = Gtk2::CheckMenuItem->new('Release _all filters');
            $item_releaseAllFilters->set_active($self->worldModelObj->allRoomFiltersFlag);
            $item_releaseAllFilters->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'allRoomFiltersFlag',
                        $item_releaseAllFilters->get_active(),
                        TRUE,      # Do call $self->drawRegion
                        'release_all_filters',
                        'icon_release_all_filters',
                    );
                }
            });
            $subMenu_roomFilters->append($item_releaseAllFilters);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'release_all_filters', $item_releaseAllFilters);

            $subMenu_roomFilters->append(Gtk2::SeparatorMenuItem->new()); # Separator

            foreach my $filter ($self->worldModelObj->roomFilterList) {

                my $iconItem;

                my $menuItem = Gtk2::CheckMenuItem->new('Release _' . $filter . ' filter');
                $menuItem->set_active($self->worldModelObj->ivShow('roomFilterHash', $filter));
                $menuItem->signal_connect('toggled' => sub {

                    if (! $self->ignoreMenuUpdateFlag) {

                        $self->worldModelObj->toggleFilter(
                            $filter,
                            $menuItem->get_active(),
                        );
                    }
                });
                $subMenu_roomFilters->append($menuItem);
                # (Never desensitised)
                $self->ivAdd('menuToolItemHash', $filter . '_filter', $menuItem);
            }

        my $item_roomFilters = Gtk2::MenuItem->new('Room _filters');
        $item_roomFilters->set_submenu($subMenu_roomFilters);
        $column_view->append($item_roomFilters);

            # 'Room interiors' submenu
            my $subMenu_roomInteriors = Gtk2::Menu->new();

            @initList = (
                'none' => '_Don\'t draw counts',
                'shadow_count' => 'Draw _unallocated/shadow exits',
                'region_count' => 'Draw reg_ion/super region exits',
                'room_content' => 'Draw _room contents',
                'hidden_count' => 'Draw _hidden contents',
                'temp_count' => 'Draw _temporary contents',
                'word_count' => 'Draw r_ecognised words',
                'room_flag' => 'Draw r_oom flag text',
                'visit_count' => 'Draw _character visits',
                'profile_count' => 'Draw e_xclusive profiles',
                'title_descrip' => 'Draw titles/descriptions',
                'exit_pattern' => 'Draw exit patterns',
                'source_code' => 'Draw room _source code',
                'vnum' => 'Draw world\'s room _vnum',
            );

            do {

                my ($mode, $descrip);

                $mode = shift @initList;
                $descrip = shift @initList;

                push (@interiorList, $mode);
                $interiorHash{$mode} = $descrip;

            } until (! @initList);

            for (my $count = 0; $count < (scalar @interiorList); $count++) {

                my ($icon, $mode);

                $mode = $interiorList[$count];

                # (For $count = 0, $item_group is 'undef')
                my $item_radio = Gtk2::RadioMenuItem->new($item_group, $interiorHash{$mode});

                if ($self->worldModelObj->roomInteriorMode eq $mode) {

                    $item_radio->set_active(TRUE);
                }

                $item_radio->signal_connect('toggled' => sub {

                    if (! $self->ignoreMenuUpdateFlag && $item_radio->get_active()) {

                        $self->worldModelObj->switchRoomInteriorMode($mode);
                    }
                });
                $item_group = $item_radio->get_group();
                $subMenu_roomInteriors->append($item_radio);
                # (Never desensitised)
                $self->ivAdd('menuToolItemHash', 'interior_mode_' . $mode, $item_radio);
            }

            $subMenu_roomInteriors->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_changeCharDrawn = Gtk2::MenuItem->new('Ch_ange character drawn...');
            $item_changeCharDrawn->signal_connect('activate' => sub {

                # (Callback func has no dependencies)
                $self->changeCharDrawnCallback();
            });
            $subMenu_roomInteriors->append($item_changeCharDrawn);

        my $item_roomInteriors = Gtk2::MenuItem->new('Room _interiors');
        $item_roomInteriors->set_submenu($subMenu_roomInteriors);
        $column_view->append($item_roomInteriors);

            # 'All exits' submenu
            my $subMenu_allExits = Gtk2::Menu->new();

            my $item_radio11 = Gtk2::RadioMenuItem->new(undef, '_Use region exit settings');
            $item_radio11->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio11->get_active()) {

                    $self->worldModelObj->switchMode(
                        'drawExitMode',
                        'ask_regionmap',    # New value of ->drawExitMode
                        TRUE,               # Do call $self->drawRegion
                        'draw_defer_exits',
                        'icon_draw_defer_exits',
                    );
                }
            });
            my $item_group1 = $item_radio11->get_group();
            $subMenu_allExits->append($item_radio11);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'draw_defer_exits', $item_radio11);

            my $item_radio12 = Gtk2::RadioMenuItem->new($item_group1, 'Draw _no exits');
            if ($self->worldModelObj->drawExitMode eq 'no_exit') {

                $item_radio12->set_active(TRUE);
            }
            $item_radio12->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio12->get_active()) {

                    $self->worldModelObj->switchMode(
                        'drawExitMode',
                        'no_exit',          # New value of ->drawExitMode
                        TRUE,               # Do call $self->drawRegion
                        'draw_no_exits',
                        'icon_draw_no_exits',
                    );
                }
            });
            $subMenu_allExits->append($item_radio12);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'draw_no_exits', $item_radio12);

            my $item_radio13 = Gtk2::RadioMenuItem->new($item_group1, 'Draw _simple exits');
            if ($self->worldModelObj->drawExitMode eq 'simple_exit') {

                $item_radio13->set_active(TRUE);
            }
            $item_radio13->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio13->get_active()) {

                    $self->worldModelObj->switchMode(
                        'drawExitMode',
                        'simple_exit',      # New value of ->drawExitMode
                        TRUE,               # Do call $self->drawRegion
                        'draw_simple_exits',
                        'icon_draw_simple_exits',
                    );
                }
            });
            $subMenu_allExits->append($item_radio13);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'draw_simple_exits', $item_radio13);

            my $item_radio14 = Gtk2::RadioMenuItem->new(
                $item_group1,
                'Draw _complex exits',
            );
            if ($self->worldModelObj->drawExitMode eq 'complex_exit') {

                $item_radio14->set_active(TRUE);
            }
            $item_radio14->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio14->get_active()) {

                    $self->worldModelObj->switchMode(
                        'drawExitMode',
                        'complex_exit',     # New value of ->drawExitMode
                        TRUE,               # Do call $self->drawRegion
                        'draw_complex_exits',
                        'icon_draw_complex_exits',
                    );
                }
            });
            $subMenu_allExits->append($item_radio14);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'draw_complex_exits', $item_radio14);

            $subMenu_allExits->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_drawOrnaments = Gtk2::CheckMenuItem->new('Draw exit _ornaments');
            $item_drawOrnaments->set_active($self->worldModelObj->drawOrnamentsFlag);
            $item_drawOrnaments->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'drawOrnamentsFlag',
                        $item_drawOrnaments->get_active(),
                        TRUE,      # Do call $self->drawRegion
                        'draw_ornaments',
                        'icon_draw_ornaments',
                    );
                }
            });
            $subMenu_allExits->append($item_drawOrnaments);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'draw_ornaments', $item_drawOrnaments);

        my $item_allExits = Gtk2::MenuItem->new('Exits (_all regions)');
        $item_allExits->set_submenu($subMenu_allExits);
        $column_view->append($item_allExits);

            # 'Region exits' submenu
            my $subMenu_regionExits = Gtk2::Menu->new();

            my $item_radio21 = Gtk2::RadioMenuItem->new(undef, 'Draw _no exits');
            $item_radio21->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio21->get_active()) {

                    $self->worldModelObj->switchRegionDrawExitMode(
                        $self->currentRegionmap,
                        'no_exit',
                    );
                }
            });
            my $item_group2 = $item_radio21->get_group();
            $subMenu_regionExits->append($item_radio21);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'region_draw_no_exits', $item_radio21);

            my $item_radio22 = Gtk2::RadioMenuItem->new($item_group2, 'Draw _simple exits');
            if ($self->currentRegionmap && $self->currentRegionmap->drawExitMode eq 'simple_exit') {

                $item_radio22->set_active(TRUE);
            }
            $item_radio22->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio22->get_active()) {

                    $self->worldModelObj->switchRegionDrawExitMode(
                        $self->currentRegionmap,
                        'simple_exit',
                    );
                }
            });
            $subMenu_regionExits->append($item_radio22);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'region_draw_simple_exits', $item_radio22);

            my $item_radio23 = Gtk2::RadioMenuItem->new($item_group2, 'Draw _complex exits');
            if (
                $self->currentRegionmap
                && $self->currentRegionmap->drawExitMode eq 'complex_exit'
            ) {
                $item_radio23->set_active(TRUE);
            }
            $item_radio23->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio23->get_active()) {

                    $self->worldModelObj->switchRegionDrawExitMode(
                        $self->currentRegionmap,
                        'complex_exit',
                    );
                }
            });
            $subMenu_regionExits->append($item_radio23);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'region_draw_complex_exits', $item_radio23);

        my $item_regionExits = Gtk2::MenuItem->new('Exits (_current region)');
        $item_regionExits->set_submenu($subMenu_regionExits);
        $column_view->append($item_regionExits);
        # (Requires $self->currentRegionmap and $self->worldModelObj->drawExitMode is
        #   'ask_regionmap')
        $self->ivAdd('menuToolItemHash', 'draw_region_exits', $item_regionExits);

        $column_view->append(Gtk2::SeparatorMenuItem->new());   # Separator

        my $item_zoomIn = Gtk2::ImageMenuItem->new('Zoom i_n');
        my $img_zoomIn = Gtk2::Image->new_from_stock('gtk-zoom-in', 'menu');
        $item_zoomIn->set_image($img_zoomIn);
        $item_zoomIn->signal_connect('activate' => sub {

            $self->zoomCallback('in');
        });
        $column_view->append($item_zoomIn);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'zoom_in', $item_zoomIn);

        my $item_zoomOut = Gtk2::ImageMenuItem->new('Zoom _out');
        my $img_zoomOut = Gtk2::Image->new_from_stock('gtk-zoom-out', 'menu');
        $item_zoomOut->set_image($img_zoomOut);
        $item_zoomOut->signal_connect('activate' => sub {

            $self->zoomCallback('out');
        });
        $column_view->append($item_zoomOut);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'zoom_out', $item_zoomOut);

            # 'Zoom' submenu
            my $subMenu_zoom = Gtk2::Menu->new();

            # Import the list of magnifications
            @magList = $self->constMagnifyList;
            # Use a subset of magnifications from $self->constMagnifyList (and in reverse order to
            #   that found in $self->constMagnifyList)
            @shortMagList = reverse $self->constShortMagnifyList;

            foreach my $mag (@shortMagList) {

                my $menuItem = Gtk2::MenuItem->new('Zoom ' . $mag * 100 . '%');
                $menuItem->signal_connect('activate' => sub {

                    # No argument causes the called function to prompt the user
                    $self->zoomCallback($mag);
                });
                $subMenu_zoom->append($menuItem);
            }

            $subMenu_zoom->append(Gtk2::SeparatorMenuItem->new());  # Separator

            my $item_zoomMax = Gtk2::MenuItem->new('Zoom _in max');
            $item_zoomMax->signal_connect('activate' => sub {

                $self->zoomCallback($magList[-1]);
            });
            $subMenu_zoom->append($item_zoomMax);

            my $item_zoomMin = Gtk2::MenuItem->new('Zoom _out max');
            $item_zoomMin->signal_connect('activate' => sub {

                $self->zoomCallback($magList[0]);
            });
            $subMenu_zoom->append($item_zoomMin);

            $subMenu_zoom->append(Gtk2::SeparatorMenuItem->new());  # Separator

            my $item_zoomPrompt = Gtk2::MenuItem->new('O_ther...');
            $item_zoomPrompt->signal_connect('activate' => sub {

                # No argument causes the called function to prompt the user
                $self->zoomCallback();
            });
            $subMenu_zoom->append($item_zoomPrompt);

        my $item_zoom = Gtk2::ImageMenuItem->new('_Zoom');
        my $img_zoom = Gtk2::Image->new_from_stock('gtk-zoom-fit', 'menu');
        $item_zoom->set_image($img_zoom);
        $item_zoom->set_submenu($subMenu_zoom);
        $column_view->append($item_zoom);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'zoom_sub', $item_zoom);

        $column_view->append(Gtk2::SeparatorMenuItem->new());   # Separator

            # 'Level' submenu
            my $subMenu_level = Gtk2::Menu->new();

            my $item_moveUpLevel = Gtk2::MenuItem->new('Move _up level');
            $item_moveUpLevel->signal_connect('activate' => sub {

                $self->setCurrentLevel($self->currentRegionmap->currentLevel + 1);

                # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
                $self->restrictWidgets();
            });
            $subMenu_level->append($item_moveUpLevel);
            # (Requires $self->currentRegionmap)
            $self->ivAdd('menuToolItemHash', 'move_up_level', $item_moveUpLevel);

            my $item_moveDownLevel = Gtk2::MenuItem->new('Move _down level');
            $item_moveDownLevel->signal_connect('activate' => sub {

                $self->setCurrentLevel($self->currentRegionmap->currentLevel - 1);

                # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
                $self->restrictWidgets();
            });
            $subMenu_level->append($item_moveDownLevel);
            # (Requires $self->currentRegionmap)
            $self->ivAdd('menuToolItemHash', 'move_down_level', $item_moveDownLevel);

            $subMenu_level->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_changeLevel = Gtk2::MenuItem->new('_Change level...');
            $item_changeLevel->signal_connect('activate' => sub {

                $self->changeLevelCallback();
            });
            $subMenu_level->append($item_changeLevel);

        my $item_level = Gtk2::MenuItem->new('_Level');
        $item_level->set_submenu($subMenu_level);
        $column_view->append($item_level);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'level_sub', $item_level);

        $column_view->append(Gtk2::SeparatorMenuItem->new());   # Separator

            # 'Centre map' submenu
            my $subMenu_centreMap = Gtk2::Menu->new();

            my $item_centreMap_currentRoom = Gtk2::MenuItem->new('_Current room');
            $item_centreMap_currentRoom->signal_connect('activate' => sub {

                $self->centreMapOverRoom($self->mapObj->currentRoom);
            });
            $subMenu_centreMap->append($item_centreMap_currentRoom);
            # (Requires $self->currentRegionmap & $self->mapObj->currentRoom)
            $self->ivAdd(
                'menuToolItemHash',
                'centre_map_current_room',
                $item_centreMap_currentRoom,
            );

            my $item_centreMap_selectRoom = Gtk2::MenuItem->new('_Selected room');
            $item_centreMap_selectRoom->signal_connect('activate' => sub {

                $self->centreMapOverRoom($self->selectedRoom);
            });
            $subMenu_centreMap->append($item_centreMap_selectRoom);
            # (Requires $self->currentRegionmap & $self->selectedRoom)
            $self->ivAdd(
                'menuToolItemHash',
                'centre_map_select_room',
                $item_centreMap_selectRoom,
            );

            my $item_centreMap_lastKnownRoom = Gtk2::MenuItem->new('_Last known room');
            $item_centreMap_lastKnownRoom->signal_connect('activate' => sub {

                $self->centreMapOverRoom($self->mapObj->lastKnownRoom);
            });
            $subMenu_centreMap->append($item_centreMap_lastKnownRoom);
            # (Requires $self->currentRegionmap & $self->mapObj->lastknownRoom)
            $self->ivAdd(
                'menuToolItemHash',
                'centre_map_last_known_room',
                $item_centreMap_lastKnownRoom,
            );

            $subMenu_centreMap->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_centreMap_middleGrid = Gtk2::MenuItem->new('_Middle of grid');
            $item_centreMap_middleGrid->signal_connect('activate' => sub {

                $self->setMapPosn(0.5, 0.5);
            });
            $subMenu_centreMap->append($item_centreMap_middleGrid);
            # (Requires $self->currentRegionmap)
            $self->ivAdd('menuToolItemHash', 'centre_map_middle_grid', $item_centreMap_middleGrid);

        my $item_centreMap = Gtk2::MenuItem->new('Centre _map');
        $item_centreMap->set_submenu($subMenu_centreMap);
        $column_view->append($item_centreMap);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'centre_map_sub', $item_centreMap);

        my $item_repositionAllMaps = Gtk2::MenuItem->new('R_eposition all maps');
        $item_repositionAllMaps->signal_connect('activate' => sub {

            $self->worldModelObj->repositionMaps();
        });
        $column_view->append($item_repositionAllMaps);

            # 'Tracking' submenu
            my $subMenu_tracking = Gtk2::Menu->new();

            my $item_trackCurrentRoom = Gtk2::CheckMenuItem->new('_Track current room');
            $item_trackCurrentRoom->set_active($self->worldModelObj->trackPosnFlag);
            $item_trackCurrentRoom->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'trackPosnFlag',
                        $item_trackCurrentRoom->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'track_current_room',
                        'icon_track_current_room',
                    );
                }
            });
            $subMenu_tracking->append($item_trackCurrentRoom);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'track_current_room', $item_trackCurrentRoom);

            $subMenu_tracking->append(Gtk2::SeparatorMenuItem->new());  # Separator

            my $item_radio31 = Gtk2::RadioMenuItem->new(undef, '_Always track');
            if (
                $self->worldModelObj->trackingSensitivity != 0.33
                && $self->worldModelObj->trackingSensitivity != 0.66
                && $self->worldModelObj->trackingSensitivity != 1
            ) {
                # Only the sensitivity values 0, 0.33, 0.66 and 1 are curently allowed; act as
                #   though the IV was set to 0
                $item_radio31->set_active(TRUE);
            }
            $item_radio31->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio31->get_active()) {

                    $self->worldModelObj->setTrackingSensitivity(0);
                }
            });
            my $item_group3 = $item_radio31->get_group();
            $subMenu_tracking->append($item_radio31);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'track_always', $item_radio31);

            my $item_radio32 = Gtk2::RadioMenuItem->new($item_group3, 'Track near _centre');
            if ($self->worldModelObj->trackingSensitivity == 0.33) {

                $item_radio32->set_active(TRUE);
            }
            $item_radio32->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio32->get_active()) {

                    $self->worldModelObj->setTrackingSensitivity(0.33);
                }
            });
            $subMenu_tracking->append($item_radio32);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'track_near_centre', $item_radio32);

            my $item_radio33 = Gtk2::RadioMenuItem->new($item_group3, 'Track near _edge');
            if ($self->worldModelObj->trackingSensitivity == 0.66) {

                $item_radio33->set_active(TRUE);
            }
            $item_radio33->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio33->get_active()) {

                    $self->worldModelObj->setTrackingSensitivity(0.66);
                }
            });
            $subMenu_tracking->append($item_radio33);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'track_near_edge', $item_radio33);

            my $item_radio34 = Gtk2::RadioMenuItem->new(
                $item_group3,
                'Track if not _visible',
            );
            if ($self->worldModelObj->trackingSensitivity == 1) {

                $item_radio34->set_active(TRUE);
            }
            $item_radio34->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $item_radio34->get_active()) {

                    $self->worldModelObj->setTrackingSensitivity(1);
                }
            });
            $subMenu_tracking->append($item_radio34);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'track_not_visible', $item_radio34);

        my $item_tracking = Gtk2::MenuItem->new('_Tracking');
        $item_tracking->set_submenu($subMenu_tracking);
        $column_view->append($item_tracking);

        # Setup complete
        return $column_view;
    }

    sub enableModeColumn {

        # Called by $self->enableMenu
        # Sets up the 'Mode' column of the Automapper window's menu bar
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableModeColumn', @_);
        }

        # Set up column
        my $column_mode = Gtk2::Menu->new();
        if (! $column_mode) {

            return undef;
        }

        # (Save each radio menu item in a hash IV, so that when $self->setMode is called, the radio
        #   group can be toggled)
        my $item_radio1 = Gtk2::RadioMenuItem->new(undef,'_Wait mode');
        $item_radio1->signal_connect('toggled' => sub {

            # (To stop the equivalent toolbar icon from being toggled by the call to ->setMode,
            #   make use of $self->ignoreMenuUpdateFlag)
            if ($item_radio1->get_active && ! $self->ignoreMenuUpdateFlag) {

                $self->setMode('wait');
            }
        });
        my $item_group = $item_radio1->get_group();
        $column_mode->append($item_radio1);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'set_wait_mode', $item_radio1);

        my $item_radio2 = Gtk2::RadioMenuItem->new($item_group,'_Follow mode');
        $item_radio2->signal_connect('toggled' => sub {

            if ($item_radio2->get_active && ! $self->ignoreMenuUpdateFlag) {

                $self->setMode('follow');
            }
        });
        $column_mode->append($item_radio2);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'set_follow_mode', $item_radio2);

        my $item_radio3 = Gtk2::RadioMenuItem->new($item_group,'_Update mode');
        $item_radio3->signal_connect('toggled' => sub {

            if ($item_radio3->get_active && ! $self->ignoreMenuUpdateFlag) {

                $self->setMode('update');
            }
        });
        $column_mode->append($item_radio3);
        # (Requires $self->currentRegionmap, GA::Obj::WorldModel->disableUpdateModeFlag set to
        #   FALSE and a session not in 'connect offline' mode
        $self->ivAdd('menuToolItemHash', 'set_update_mode', $item_radio3);

        $column_mode->append(Gtk2::SeparatorMenuItem->new());   # Separator

        my $item_dragMode = Gtk2::CheckMenuItem->new('_Drag mode');
        $item_dragMode->set_active($self->dragModeFlag);
        $item_dragMode->signal_connect('toggled' => sub {

            if ($item_dragMode->get_active()) {
                $self->ivPoke('dragModeFlag', TRUE);
            } else {
                $self->ivPoke('dragModeFlag', FALSE);
            }

            # Set the equivalent toolbar button
            if ($self->ivExists('menuToolItemHash', 'icon_drag_mode')) {

                my $menuItem = $self->ivShow('menuToolItemHash', 'icon_drag_mode');
                $menuItem->set_active($item_dragMode->get_active());
            }
        });
        $column_mode->append($item_dragMode);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'drag_mode', $item_dragMode);

        $column_mode->append(Gtk2::SeparatorMenuItem->new());   # Separator

            # 'Match rooms' submenu
            my $subMenu_matchRooms = Gtk2::Menu->new();

            my $item_matchTitle = Gtk2::CheckMenuItem->new('Match room _titles');
            $item_matchTitle->set_active($self->worldModelObj->matchTitleFlag);
            $item_matchTitle->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'matchTitleFlag',
                        $item_matchTitle->get_active(),
                        FALSE,          # Do call $self->drawRegion
                        'match_title',
                    );
                }
            });
            $subMenu_matchRooms->append($item_matchTitle);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'match_title', $item_matchTitle);

            my $item_matchDescrip = Gtk2::CheckMenuItem->new('_Match room descriptions');
            $item_matchDescrip->set_active($self->worldModelObj->matchDescripFlag);
            $item_matchDescrip->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'matchDescripFlag',
                        $item_matchDescrip->get_active(),
                        FALSE,          # Do call $self->drawRegion
                        'match_descrip',
                    );
                }
            });
            $subMenu_matchRooms->append($item_matchDescrip);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'match_descrip', $item_matchDescrip);

            my $item_matchExit = Gtk2::CheckMenuItem->new('Match _exits');
            $item_matchExit->set_active($self->worldModelObj->matchExitFlag);
            $item_matchExit->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'matchExitFlag',
                        $item_matchExit->get_active(),
                        FALSE,          # Do call $self->drawRegion
                        'match_exit',
                    );
                }
            });
            $subMenu_matchRooms->append($item_matchExit);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'match_exit', $item_matchExit);

            my $item_matchSource = Gtk2::CheckMenuItem->new('Match _source code');
            $item_matchSource->set_active($self->worldModelObj->matchSourceFlag);
            $item_matchSource->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'matchSourceFlag',
                        $item_matchSource->get_active(),
                        FALSE,          # Do call $self->drawRegion
                        'match_source',
                    );
                }
            });
            $subMenu_matchRooms->append($item_matchSource);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'match_source', $item_matchSource);

            my $item_matchVNum = Gtk2::CheckMenuItem->new('Match room _vnum');
            $item_matchVNum->set_active($self->worldModelObj->matchVNumFlag);
            $item_matchVNum->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'matchVNumFlag',
                        $item_matchVNum->get_active(),
                        FALSE,          # Do call $self->drawRegion
                        'match_vnum',
                    );
                }
            });
            $subMenu_matchRooms->append($item_matchVNum);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'match_vnum', $item_matchVNum);

        my $item_matchRooms = Gtk2::MenuItem->new('_Match rooms');
        $item_matchRooms->set_submenu($subMenu_matchRooms);
        $column_mode->append($item_matchRooms);

            # 'Update rooms' submenu
            my $subMenu_updateRooms = Gtk2::Menu->new();

            my $item_updateTitle = Gtk2::CheckMenuItem->new('Update room _titles');
            $item_updateTitle->set_active($self->worldModelObj->updateTitleFlag);
            $item_updateTitle->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'updateTitleFlag',
                        $item_updateTitle->get_active(),
                        FALSE,          # Do call $self->drawRegion
                        'update_title',
                    );
                }
            });
            $subMenu_updateRooms->append($item_updateTitle);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'update_title', $item_updateTitle);

            my $item_updateDescrip = Gtk2::CheckMenuItem->new('Update room _descriptions');
            $item_updateDescrip->set_active($self->worldModelObj->updateDescripFlag);
            $item_updateDescrip->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'updateDescripFlag',
                        $item_updateDescrip->get_active(),
                        FALSE,          # Do call $self->drawRegion
                        'update_descrip',
                    );
                }
            });
            $subMenu_updateRooms->append($item_updateDescrip);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'update_descrip', $item_updateDescrip);

            my $item_updateExit = Gtk2::CheckMenuItem->new('Update _exits');
            $item_updateExit->set_active($self->worldModelObj->updateExitFlag);
            $item_updateExit->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'updateExitFlag',
                        $item_updateExit->get_active(),
                        FALSE,          # Do call $self->drawRegion
                        'update_exit',
                    );
                }
            });
            $subMenu_updateRooms->append($item_updateExit);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'update_exit', $item_updateExit);

            my $item_updateOrnament
                = Gtk2::CheckMenuItem->new('Update _ornaments from exit state');
            $item_updateOrnament->set_active($self->worldModelObj->updateOrnamentFlag);
            $item_updateOrnament->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'updateOrnamentFlag',
                        $item_updateOrnament->get_active(),
                        FALSE,          # Do call $self->drawRegion
                        'update_ornament',
                    );
                }
            });
            $subMenu_updateRooms->append($item_updateOrnament);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'update_ornament', $item_updateOrnament);

            my $item_updateSource = Gtk2::CheckMenuItem->new('Update _source code');
            $item_updateSource->set_active($self->worldModelObj->updateSourceFlag);
            $item_updateSource->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'updateSourceFlag',
                        $item_updateSource->get_active(),
                        FALSE,          # Do call $self->drawRegion
                        'update_source',
                    );
                }
            });
            $subMenu_updateRooms->append($item_updateSource);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'update_source', $item_updateSource);

            my $item_updateVNum = Gtk2::CheckMenuItem->new('Update room _vnum, etc');
            $item_updateVNum->set_active($self->worldModelObj->updateVNumFlag);
            $item_updateVNum->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'updateVNumFlag',
                        $item_updateVNum->get_active(),
                        FALSE,          # Do call $self->drawRegion
                        'update_vnum',
                    );
                }
            });
            $subMenu_updateRooms->append($item_updateVNum);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'update_vnum', $item_updateVNum);

            my $item_updateRoomCmd = Gtk2::CheckMenuItem->new('Update room _commands');
            $item_updateRoomCmd->set_active($self->worldModelObj->updateRoomCmdFlag);
            $item_updateRoomCmd->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'updateRoomCmdFlag',
                        $item_updateRoomCmd->get_active(),
                        FALSE,          # Do call $self->drawRegion
                        'update_room_cmd',
                    );
                }
            });
            $subMenu_updateRooms->append($item_updateRoomCmd);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'update_room_cmd', $item_updateRoomCmd);

        my $item_updateRooms = Gtk2::MenuItem->new('Update _rooms');
        $item_updateRooms->set_submenu($subMenu_updateRooms);
        $column_mode->append($item_updateRooms);

        my $item_verboseChars = Gtk2::MenuItem->new('Verbose _characters...');
        $item_verboseChars->signal_connect('activate' => sub {

            $self->verboseCharsCallback();
        });
        $column_mode->append($item_verboseChars);

        my $item_analyseDescrip = Gtk2::CheckMenuItem->new('A_nalyse room descrips');
        $item_analyseDescrip->set_active($self->worldModelObj->analyseDescripFlag);
        $item_analyseDescrip->signal_connect('toggled' => sub {

            if (! $self->ignoreMenuUpdateFlag) {

                $self->worldModelObj->toggleFlag(
                    'analyseDescripFlag',
                    $item_analyseDescrip->get_active(),
                    FALSE,      # Don't call $self->drawRegion
                    'analyse_descrip',
                );
            }
        });
        $column_mode->append($item_analyseDescrip);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'analyse_descrip', $item_analyseDescrip);

        $column_mode->append(Gtk2::SeparatorMenuItem->new());   # Separator

            # 'Painter' submenu
            my $subMenu_painter = Gtk2::Menu->new();

            my $item_painterEnabled = Gtk2::CheckMenuItem->new('_Painter enabled');
            $item_painterEnabled->set_active($self->painterFlag);
            $item_painterEnabled->signal_connect('toggled' => sub {

                my $item;

                # Toggle the flag
                if ($item_painterEnabled->get_active()) {
                    $self->ivPoke('painterFlag', TRUE);
                } else {
                    $self->ivPoke('painterFlag', FALSE);
                }

                # Update the corresponding toolbar icon
                $item = $self->ivShow('menuToolItemHash', 'icon_enable_painter');
                if ($item) {

                    $item->set_active($self->painterFlag);
                }
            });
            $subMenu_painter->append($item_painterEnabled);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'enable_painter', $item_painterEnabled);

            $subMenu_painter->append(Gtk2::SeparatorMenuItem->new());   # Separator

            my $item_radio4 = Gtk2::RadioMenuItem->new(undef, 'Paint _all rooms');
            $item_radio4->signal_connect('toggled' => sub {

                if ($item_radio4->get_active) {

                    $self->worldModelObj->set_paintAllRoomsFlag(TRUE);

                    # Set the equivalent toolbar button
                    if ($self->ivExists('menuToolItemHash', 'icon_paint_all')) {

                        $self->ivShow('menuToolItemHash', 'icon_paint_all')->set_active(TRUE);
                    }
                }
            });
            my $item_group2 = $item_radio4->get_group();
            $subMenu_painter->append($item_radio4);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'paint_all', $item_radio4);

            my $item_radio5 = Gtk2::RadioMenuItem->new(
                $item_group2,
                'Paint only _new rooms',
            );
            if (! $self->worldModelObj->paintAllRoomsFlag) {

                $item_radio5->set_active(TRUE);
            }
            $item_radio5->signal_connect('toggled' => sub {

                if ($item_radio5->get_active) {

                    $self->worldModelObj->set_paintAllRoomsFlag(FALSE);

                    # Set the equivalent toolbar button
                    if ($self->ivExists('menuToolItemHash', 'icon_paint_new')) {

                        $self->ivShow('menuToolItemHash', 'icon_paint_new')->set_active(TRUE);
                    }
                }
            });
            $subMenu_painter->append($item_radio5);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'paint_new', $item_radio5);

            $subMenu_painter->append(Gtk2::SeparatorMenuItem->new());   # Separator

            my $item_repaintCurrentRoom = Gtk2::MenuItem->new('_Repaint current room');
            $item_repaintCurrentRoom->signal_connect('activate' => sub {

                if ($self->mapObj->currentRoom) {

                    # Repaint the current room. The TRUE argument instructs the function to tell
                    #   the world model to redraw the room in every Automapper window
                    $self->paintRoom($self->mapObj->currentRoom, TRUE);
                }
            });
            $subMenu_painter->append($item_repaintCurrentRoom);
            # (Requires $self->currentRegionmap and $self->mapObj->currentRoom)
            $self->ivAdd('menuToolItemHash', 'repaint_current', $item_repaintCurrentRoom);

            my $item_repaintSelectedRooms = Gtk2::MenuItem->new('Repaint _selected rooms');
            $item_repaintSelectedRooms->signal_connect('activate' => sub {

                $self->repaintSelectedRoomsCallback();
            });
            $subMenu_painter->append($item_repaintSelectedRooms);
            # (Requires $self->currentRegionmap and either $self->selectedRoom or
            #   $self->selectedRoomHash)
            $self->ivAdd('menuToolItemHash', 'repaint_selected', $item_repaintSelectedRooms);

            $subMenu_painter->append(Gtk2::SeparatorMenuItem->new());   # Separator

            my $item_useNewPainter = Gtk2::MenuItem->new('_Use new painter');
            $item_useNewPainter->signal_connect('activate' => sub {

                $self->worldModelObj->resetPainter($self->session);

                $self->showMsgDialogue(
                    'Painter',
                    'info',
                    'The painter object has been reset',
                    'ok',
                );
            });
            $subMenu_painter->append($item_useNewPainter);

            my $item_editPainter = Gtk2::ImageMenuItem->new('_Edit painter');
            my $img_editPainter = Gtk2::Image->new_from_stock('gtk-edit', 'menu');
            $item_editPainter->set_image($img_editPainter);
            $item_editPainter->signal_connect('activate' => sub {

                # Open an 'edit' window for the painter object
                $self->createFreeWin(
                    'Games::Axmud::EditWin::Painter',
                    $self,
                    $self->session,
                    'Edit world model painter',
                    $self->worldModelObj->painterObj,
                    FALSE,          # Not temporary
                );
            });
            $subMenu_painter->append($item_editPainter);

        my $item_painter = Gtk2::ImageMenuItem->new('_Painter');
        my $img_painter = Gtk2::Image->new_from_stock('gtk-select-color', 'menu');
        $item_painter->set_image($img_painter);
        $item_painter->set_submenu($subMenu_painter);
        $column_mode->append($item_painter);

        $column_mode->append(Gtk2::SeparatorMenuItem->new());   # Separator

            # 'Assisted moves' submenu
            my $subMenu_assistedMoves = Gtk2::Menu->new();

            my $item_allowAssisted = Gtk2::CheckMenuItem->new('_Allow assisted moves');
            $item_allowAssisted->set_active($self->worldModelObj->assistedMovesFlag);
            $item_allowAssisted->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'assistedMovesFlag',
                        $item_allowAssisted->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'allow_assisted_moves',
                    );

                    # The menu items below which set ->protectedMovesFlag and
                    #   ->superProtectedMovesFlag are desensitised if ->assistedMovesFlag is false
                    # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
                    $self->restrictWidgets();
                }
            });
            $subMenu_assistedMoves->append($item_allowAssisted);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'allow_assisted_moves', $item_allowAssisted);

            $subMenu_assistedMoves->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_assistedBreak = Gtk2::CheckMenuItem->new('_Break doors before move');
            $item_assistedBreak->set_active($self->worldModelObj->assistedBreakFlag);
            $item_assistedBreak->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'assistedBreakFlag',
                        $item_assistedBreak->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'break_before_move',
                    );
                }
            });
            $subMenu_assistedMoves->append($item_assistedBreak);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'break_before_move', $item_assistedBreak);

            my $item_assistedPick = Gtk2::CheckMenuItem->new('_Pick doors before move');
            $item_assistedPick->set_active($self->worldModelObj->assistedPickFlag);
            $item_assistedPick->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'assistedPickFlag',
                        $item_assistedPick->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'pick_before_move',
                    );
                }
            });
            $subMenu_assistedMoves->append($item_assistedPick);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'pick_before_move', $item_assistedPick);

            my $item_assistedUnlock = Gtk2::CheckMenuItem->new('_Unlock doors before move');
            $item_assistedUnlock->set_active($self->worldModelObj->assistedUnlockFlag);
            $item_assistedUnlock->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'assistedUnlockFlag',
                        $item_assistedUnlock->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'unlock_before_move',
                    );
                }
            });
            $subMenu_assistedMoves->append($item_assistedUnlock);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'unlock_before_move', $item_assistedUnlock);

            my $item_assistedOpen = Gtk2::CheckMenuItem->new('_Open doors before move');
            $item_assistedOpen->set_active($self->worldModelObj->assistedOpenFlag);
            $item_assistedOpen->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'assistedOpenFlag',
                        $item_assistedOpen->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'open_before_move',
                    );
                }
            });
            $subMenu_assistedMoves->append($item_assistedOpen);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'open_before_move', $item_assistedOpen);

            my $item_assistedClose = Gtk2::CheckMenuItem->new('_Close doors after move');
            $item_assistedClose->set_active($self->worldModelObj->assistedCloseFlag);
            $item_assistedClose->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'assistedCloseFlag',
                        $item_assistedClose->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'close_after_move',
                    );
                }
            });
            $subMenu_assistedMoves->append($item_assistedClose);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'close_after_move', $item_assistedClose);

            my $item_assistedLock = Gtk2::CheckMenuItem->new('_Lock doors after move');
            $item_assistedLock->set_active($self->worldModelObj->assistedLockFlag);
            $item_assistedLock->signal_connect('toggled' => sub {

                if (! $self->assistedLockFlag) {

                    $self->worldModelObj->toggleFlag(
                        'assistedLockFlag',
                        $item_assistedLock->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'lock_after_move',
                    );
                }
            });
            $subMenu_assistedMoves->append($item_assistedLock);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'lock_after_move', $item_assistedLock);

            $subMenu_assistedMoves->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_allowProtected = Gtk2::CheckMenuItem->new('Allow p_rotected moves');
            $item_allowProtected->set_active($self->worldModelObj->protectedMovesFlag);
            $item_allowProtected->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'protectedMovesFlag',
                        $item_allowProtected->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'allow_protected_moves',
                    );
                }
            });
            $subMenu_assistedMoves->append($item_allowProtected);
            # (Requires $self->worldModelObj->assistedMovesFlag)
            $self->ivAdd('menuToolItemHash', 'allow_protected_moves', $item_allowProtected);

            my $item_allowSuper = Gtk2::CheckMenuItem->new('Ca_ncel commands when overruled');
            $item_allowSuper->set_active($self->worldModelObj->superProtectedMovesFlag);
            $item_allowSuper->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'superProtectedMovesFlag',
                        $item_allowSuper->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'allow_super_protected_moves',
                    );
                }
            });
            $subMenu_assistedMoves->append($item_allowSuper);
            # (Requires $self->worldModelObj->assistedMovesFlag)
            $self->ivAdd('menuToolItemHash', 'allow_super_protected_moves', $item_allowSuper);

        my $item_assistedMoves = Gtk2::MenuItem->new('_Assisted moves');
        $item_assistedMoves->set_submenu($subMenu_assistedMoves);
        $column_mode->append($item_assistedMoves);

            # 'Start-up flags' submenu
            my $subMenu_startUpFlags = Gtk2::Menu->new();

            my $item_autoOpenWindow = Gtk2::CheckMenuItem->new('_Open automapper on startup');
            $item_autoOpenWindow->set_active($self->worldModelObj->autoOpenWinFlag);
            $item_autoOpenWindow->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'autoOpenWinFlag',
                        $item_autoOpenWindow->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'auto_open_win',
                    );
                }
            });
            $subMenu_startUpFlags->append($item_autoOpenWindow);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'auto_open_win', $item_autoOpenWindow);

            my $item_pseudoWin = Gtk2::CheckMenuItem->new('Open as _pseudo-window');
            $item_pseudoWin->set_active($self->worldModelObj->pseudoWinFlag);
            $item_pseudoWin->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'pseudoWinFlag',
                        $item_pseudoWin->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'pseudo_win',
                    );
                }
            });
            $subMenu_startUpFlags->append($item_pseudoWin);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'pseudo_win', $item_pseudoWin);

            my $item_allowTrackAlone = Gtk2::CheckMenuItem->new('_Follow character after closing');
            $item_allowTrackAlone->set_active($self->worldModelObj->allowTrackAloneFlag);
            $item_allowTrackAlone->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'allowTrackAloneFlag',
                        $item_allowTrackAlone->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'keep_following',
                    );
                }
            });
            $subMenu_startUpFlags->append($item_allowTrackAlone);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'keep_following', $item_allowTrackAlone);

        my $item_startUpFlags = Gtk2::MenuItem->new('_Start-up flags');
        $item_startUpFlags->set_submenu($subMenu_startUpFlags);
        $column_mode->append($item_startUpFlags);

            # 'Other flags' submenu
            my $subMenu_otherFlags = Gtk2::Menu->new();

            my $item_allowModelScripts = Gtk2::CheckMenuItem->new('_Allow model-wide scripts');
            $item_allowModelScripts->set_active($self->worldModelObj->allowModelScriptFlag);
            $item_allowModelScripts->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'allowModelScriptFlag',
                        $item_allowModelScripts->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'allow_model_scripts',
                    );
                }
            });
            $subMenu_otherFlags->append($item_allowModelScripts);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'allow_model_scripts', $item_autoOpenWindow);

            my $item_allowRoomScripts = Gtk2::CheckMenuItem->new(
                'Allow ' . $axmud::BASIC_NAME . ' _scripts',
            );
            $item_allowRoomScripts->set_active($self->worldModelObj->allowRoomScriptFlag);
            $item_allowRoomScripts->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'allowRoomScriptFlag',
                        $item_allowRoomScripts->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'allow_room_scripts',
                    );
                }
            });
            $subMenu_otherFlags->append($item_allowRoomScripts);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'allow_room_scripts', $item_autoOpenWindow);

            my $item_autoCheckRooms = Gtk2::CheckMenuItem->new('A_uto-compare new rooms');
            $item_autoCheckRooms->set_active($self->worldModelObj->autoCompareFlag);
            $item_autoCheckRooms->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'autoCompareFlag',
                        $item_autoCheckRooms->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'auto_compare_rooms',
                    );
                }
            });
            $subMenu_otherFlags->append($item_autoCheckRooms);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'auto_compare_rooms', $item_autoCheckRooms);

            my $item_roomTagsInCaps = Gtk2::CheckMenuItem->new('Capitalise room _tags');
            $item_roomTagsInCaps->set_active($self->worldModelObj->capitalisedRoomTagFlag);
            $item_roomTagsInCaps->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'capitalisedRoomTagFlag',
                        $item_roomTagsInCaps->get_active(),
                        TRUE,      # Do call $self->drawRegion
                        'room_tags_capitalised',
                    );
                }
            });
            $subMenu_otherFlags->append($item_roomTagsInCaps);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'room_tags_capitalised', $item_roomTagsInCaps);

            my $item_countVisits = Gtk2::CheckMenuItem->new('_Count character visits');
            $item_countVisits->set_active($self->worldModelObj->countVisitsFlag);
            $item_countVisits->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'countVisitsFlag',
                        $item_countVisits->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'count_char_visits',
                    );
                }
            });
            $subMenu_otherFlags->append($item_countVisits);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'count_char_visits', $item_autoOpenWindow);

            my $item_disableUpdate = Gtk2::CheckMenuItem->new('_Disable update mode');
            $item_disableUpdate->set_active($self->worldModelObj->disableUpdateModeFlag);
            $item_disableUpdate->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleDisableUpdateModeFlag(
                        $item_disableUpdate->get_active(),
                    );
                }
            });
            $subMenu_otherFlags->append($item_disableUpdate);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'disable_update_mode', $item_disableUpdate);

            my $item_drawBentExits = Gtk2::CheckMenuItem->new('Draw _bent broken exits');
            $item_drawBentExits->set_active($self->worldModelObj->drawBentExitsFlag);
            $item_drawBentExits->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'drawBentExitsFlag',
                        $item_drawBentExits->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'draw_bent_exits',
                    );
                }
            });
            $subMenu_otherFlags->append($item_drawBentExits);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'draw_bent_exits', $item_drawBentExits);

            my $item_drawRoomEcho = Gtk2::CheckMenuItem->new('Draw _room echos');
            $item_drawRoomEcho->set_active($self->worldModelObj->drawRoomEchoFlag);
            $item_drawRoomEcho->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'drawRoomEchoFlag',
                        $item_drawRoomEcho->get_active(),
                        TRUE,      # Do call $self->drawRegion
                        'draw_room_echo',
                    );
                }
            });
            $subMenu_otherFlags->append($item_drawRoomEcho);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'draw_room_echo', $item_drawRoomEcho);

            my $item_explainGetLost = Gtk2::CheckMenuItem->new('_Explain when getting lost');
            $item_explainGetLost->set_active($self->worldModelObj->explainGetLostFlag);
            $item_explainGetLost->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'explainGetLostFlag',
                        $item_explainGetLost->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'explain_get_lost',
                    );
                }
            });
            $subMenu_otherFlags->append($item_explainGetLost);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'explain_get_lost', $item_explainGetLost);

            my $item_followAnchor = Gtk2::CheckMenuItem->new('New exits for _follow anchors');
            $item_followAnchor->set_active($self->worldModelObj->followAnchorFlag);
            $item_followAnchor->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'followAnchorFlag',
                        $item_followAnchor->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'follow_anchor',
                    );
                }
            });
            $subMenu_otherFlags->append($item_followAnchor);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'follow_anchor', $item_followAnchor);

            my $item_showTooltips = Gtk2::CheckMenuItem->new('_Show tooltips');
            $item_showTooltips->set_active($self->worldModelObj->showTooltipsFlag);
            $item_showTooltips->signal_connect('toggled' => sub {

                $self->worldModelObj->toggleShowTooltipsFlag(
                    $item_showTooltips->get_active(),
                );
            });
            $subMenu_otherFlags->append($item_showTooltips);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'show_tooltips', $item_showTooltips);

            my $item_showAllPrimary = Gtk2::CheckMenuItem->new('_Show all directions in dialogues');
            $item_showAllPrimary->set_active($self->worldModelObj->showAllPrimaryFlag);
            $item_showAllPrimary->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'showAllPrimaryFlag',
                        $item_showAllPrimary->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'show_all_primary',
                    );
                }
            });
            $subMenu_otherFlags->append($item_showAllPrimary);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'show_all_primary', $item_showAllPrimary);

        my $item_otherFlags = Gtk2::MenuItem->new('_Other flags');
        $item_otherFlags->set_submenu($subMenu_otherFlags);
        $column_mode->append($item_otherFlags);

        # Setup complete
        return $column_mode;
    }

    sub enableRegionsColumn {

        # Called by $self->enableMenu
        # Sets up the 'Regions' column of the Automapper window's menu bar
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableRegionsColumn', @_);
        }

        # Set up column
        my $column_regions = Gtk2::Menu->new();
        if (! $column_regions) {

            return undef;
        }

        my $item_newRegion = Gtk2::ImageMenuItem->new('_New region...');
        my $img_newRegion = Gtk2::Image->new_from_stock('gtk-add', 'menu');
        $item_newRegion->set_image($img_newRegion);
        $item_newRegion->signal_connect('activate' => sub {

            $self->newRegionCallback(FALSE);
        });
        $column_regions->append($item_newRegion);

        my $item_newTempRegion = Gtk2::ImageMenuItem->new('New _temporary region...');
        my $img_newTempRegion = Gtk2::Image->new_from_stock('gtk-add', 'menu');
        $item_newTempRegion->set_image($img_newTempRegion);
        $item_newTempRegion->signal_connect('activate' => sub {

            $self->newRegionCallback(TRUE);
        });
        $column_regions->append($item_newTempRegion);

        $column_regions->append(Gtk2::SeparatorMenuItem->new());    # Separator

        my $item_editRegion = Gtk2::ImageMenuItem->new('_Edit region...');
        my $img_editRegion = Gtk2::Image->new_from_stock('gtk-edit', 'menu');
        $item_editRegion->set_image($img_editRegion);
        $item_editRegion->signal_connect('activate' => sub {

            $self->editRegionCallback();
        });
        $column_regions->append($item_editRegion);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'edit_region', $item_editRegion);

        my $item_editRegionmap = Gtk2::ImageMenuItem->new('_E_dit regionmap...');
        my $img_editRegionmap = Gtk2::Image->new_from_stock('gtk-edit', 'menu');
        $item_editRegionmap->set_image($img_editRegionmap);
        $item_editRegionmap->signal_connect('activate' => sub {

            # Open an 'edit' window for the regionmap
            $self->createFreeWin(
                'Games::Axmud::EditWin::Regionmap',
                $self,
                $self->session,
                'Edit \'' . $self->currentRegionmap->name . '\' regionmap',
                $self->currentRegionmap,
                FALSE,          # Not temporary
            );
        });
        $column_regions->append($item_editRegionmap);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'edit_regionmap', $item_editRegionmap);

            # 'Edit region features' submenu
            my $subMenu_editFeatures = Gtk2::Menu->new();

            my $item_renameRegion = Gtk2::MenuItem->new('_Rename region...');
            $item_renameRegion->signal_connect('activate' => sub {

                $self->renameRegionCallback();
            });
            $subMenu_editFeatures->append($item_renameRegion);
            # (Requires $self->currentRegionmap)
            $self->ivAdd('menuToolItemHash', 'rename_region', $item_renameRegion);

            my $item_changeParent = Gtk2::MenuItem->new('_Change parent...');
            $item_changeParent->signal_connect('activate' => sub {

                $self->changeRegionParentCallback();
            });
            $subMenu_editFeatures->append($item_changeParent);

            $subMenu_editFeatures->append(Gtk2::SeparatorMenuItem->new());    # Separator

            my $item_resetObjectCounts = Gtk2::MenuItem->new('Reset _object counts');
            $item_resetObjectCounts->signal_connect('activate' => sub {

                 # Empty the hashes which store temporary object counts and redraw the region
                 $self->worldModelObj->resetRegionCounts($self->currentRegionmap);
            });
            $subMenu_editFeatures->append($item_resetObjectCounts);

            my $item_removeRoomFlags = Gtk2::MenuItem->new('Remove room _flags...');
            $item_removeRoomFlags->signal_connect('activate' => sub {

                $self->removeRoomFlagsCallback();
            });
            $subMenu_editFeatures->append($item_removeRoomFlags);
            # (Requires $self->currentRegionmap)
            $self->ivAdd('menuToolItemHash', 'remove_room_flags', $item_removeRoomFlags);

        my $item_editFeatures = Gtk2::MenuItem->new('_Edit region features');
        $item_editFeatures->set_submenu($subMenu_editFeatures);
        $column_regions->append($item_editFeatures);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'edit_region_features', $item_editFeatures);

            # 'Recalculate paths' submenu
            my $subMenu_recalculatePaths = Gtk2::Menu->new();

            my $item_recalculateInCurrentRegion = Gtk2::MenuItem->new('In _current region');
            $item_recalculateInCurrentRegion->signal_connect('activate' => sub {

                $self->recalculatePathsCallback('current');
            });
            $subMenu_recalculatePaths->append($item_recalculateInCurrentRegion);
            # (Requires $self->currentRegionmap and a non-empty
            #   self->currentRegionmap->gridRoomHash)
            $self->ivAdd(
                'menuToolItemHash',
                'recalculate_in_region',
                $item_recalculateInCurrentRegion,
            );

            my $item_recalculateSelectRegion = Gtk2::MenuItem->new('In _region...');
            $item_recalculateSelectRegion->signal_connect('activate' => sub {

                $self->recalculatePathsCallback('select');
            });
            $subMenu_recalculatePaths->append($item_recalculateSelectRegion);

            my $item_recalculateAllRegions = Gtk2::MenuItem->new('In _all regions');
            $item_recalculateAllRegions->signal_connect('activate' => sub {

                $self->recalculatePathsCallback('all');
            });
            $subMenu_recalculatePaths->append($item_recalculateAllRegions);

            $subMenu_recalculatePaths->append(Gtk2::SeparatorMenuItem->new());    # Separator

            my $item_recalculateFromExit = Gtk2::MenuItem->new('For selected _exit');
            $item_recalculateFromExit->signal_connect('activate' => sub {

                $self->recalculatePathsCallback('exit');
            });
            $subMenu_recalculatePaths->append($item_recalculateFromExit);
            # (Requires $self->currentRegionmap and a $self->selectedExit which is a super-region
            #   exit)
            $self->ivAdd('menuToolItemHash', 'recalculate_from_exit', $item_recalculateFromExit);

        my $item_recalculatePaths = Gtk2::MenuItem->new('Rec_alculate region paths');
        $item_recalculatePaths->set_submenu($subMenu_recalculatePaths);
        $column_regions->append($item_recalculatePaths);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'recalculate_paths', $item_recalculatePaths);

        $column_regions->append(Gtk2::SeparatorMenuItem->new());    # Separator

        my $item_identifyRegion = Gtk2::MenuItem->new('_Identify highlighted region');
        $item_identifyRegion->signal_connect('activate' => sub {

            $self->identifyRegionCallback();
        });
        $column_regions->append($item_identifyRegion);
        # (Requires $self->treeViewSelectedLine)
        $self->ivAdd('menuToolItemHash', 'identify_region', $item_identifyRegion);

            # 'Screenshots' submenu
            my $subMenu_screenshots = Gtk2::Menu->new();

            my $item_visibleScreenshot = Gtk2::MenuItem->new('_Visible map');
            $item_visibleScreenshot->signal_connect('activate' => sub {

                $self->visibleScreenshotCallback();
            });
            $subMenu_screenshots->append($item_visibleScreenshot);

            my $item_occupiedScreenshot = Gtk2::MenuItem->new('_Occupied portion');
            $item_occupiedScreenshot->signal_connect('activate' => sub {

                $self->regionScreenshotCallback(FALSE);
            });
            $subMenu_screenshots->append($item_occupiedScreenshot);

            my $item_regionScreenshot = Gtk2::MenuItem->new('_Whole region');
            $item_regionScreenshot->signal_connect('activate' => sub {

                $self->regionScreenshotCallback(TRUE);
            });
            $subMenu_screenshots->append($item_regionScreenshot);

        my $item_screenshots = Gtk2::MenuItem->new('Take _screenshot');
        $item_screenshots->set_submenu($subMenu_screenshots);
        $column_regions->append($item_screenshots);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'screenshots', $item_screenshots);

            # 'Locate current room' submenu
            my $subMenu_locateCurrentRoom = Gtk2::Menu->new();

            my $item_locateInCurrentRegion = Gtk2::MenuItem->new('In _current region');
            $item_locateInCurrentRegion->signal_connect('activate' => sub {

                $self->locateCurrentRoomCallback('current');
            });
            $subMenu_locateCurrentRoom->append($item_locateInCurrentRegion);
            # (Requires $self->currentRegionmap and a non-empty GA::Obj::Regionmap->gridRoomHash)
            $self->ivAdd('menuToolItemHash', 'locate_room_in_current', $item_locateInCurrentRegion);

            my $item_locateInSelectRegion = Gtk2::MenuItem->new('In _region...');
            $item_locateInSelectRegion->signal_connect('activate' => sub {

                $self->locateCurrentRoomCallback('select');
            });
            $subMenu_locateCurrentRoom->append($item_locateInSelectRegion);

            my $item_locateInAllRegions = Gtk2::MenuItem->new('In _all regions');
            $item_locateInAllRegions->signal_connect('activate' => sub {

                $self->locateCurrentRoomCallback('all');
            });
            $subMenu_locateCurrentRoom->append($item_locateInAllRegions);

        my $item_locateCurrentRoom = Gtk2::ImageMenuItem->new('Locate c_urrent room');
        my $img_locateCurrentRoom = Gtk2::Image->new_from_stock('gtk-find', 'menu');
        $item_locateCurrentRoom->set_image($img_locateCurrentRoom);
        $item_locateCurrentRoom->set_submenu($subMenu_locateCurrentRoom);
        $column_regions->append($item_locateCurrentRoom);

        $column_regions->append(Gtk2::SeparatorMenuItem->new());    # Separator

        my $item_emptyRegion = Gtk2::MenuItem->new('Empt_y region');
        $item_emptyRegion->signal_connect('activate' => sub {

            $self->emptyRegionCallback();
        });
        $column_regions->append($item_emptyRegion);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'empty_region', $item_emptyRegion);

        my $item_deleteRegion = Gtk2::ImageMenuItem->new('De_lete region');
        my $img_deleteRegion = Gtk2::Image->new_from_stock('gtk-delete', 'menu');
        $item_deleteRegion->set_image($img_deleteRegion);
        $item_deleteRegion->signal_connect('activate' => sub {

            $self->deleteRegionCallback();
        });
        $column_regions->append($item_deleteRegion);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'delete_region', $item_deleteRegion);

        my $item_deleteTempRegion = Gtk2::ImageMenuItem->new('Delete te_mporary regions');
        my $img_deleteTempRegion = Gtk2::Image->new_from_stock('gtk-delete', 'menu');
        $item_deleteTempRegion->set_image($img_deleteTempRegion);
        $item_deleteTempRegion->signal_connect('activate' => sub {

            $self->deleteTempRegionsCallback();
        });
        $column_regions->append($item_deleteTempRegion);

        # Setup complete
        return $column_regions;
    }

    sub enableRoomsColumn {

        # Called by $self->enableMenu
        # Sets up the 'Rooms' column of the Automapper window's menu bar
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableRoomsColumn', @_);
        }

        # Set up column
        my $column_rooms = Gtk2::Menu->new();
        if (! $column_rooms) {

            return undef;
        }

        my $item_setCurrentRoom = Gtk2::MenuItem->new('_Set current room');
        $item_setCurrentRoom->signal_connect('activate' => sub {

            $self->mapObj->setCurrentRoom($self->selectedRoom);
        });
        $column_rooms->append($item_setCurrentRoom);
        # (Requires $self->currentRegionmap & $self->selectedRoom)
        $self->ivAdd('menuToolItemHash', 'set_current_room', $item_setCurrentRoom);

        my $item_unsetCurrentRoom = Gtk2::MenuItem->new('_Unset current room');
        $item_unsetCurrentRoom->signal_connect('activate' => sub {

            # This function automatically redraws the room
            $self->mapObj->setCurrentRoom();
        });
        $column_rooms->append($item_unsetCurrentRoom);
        # (Requires $self->currentRegionmap & $self->mapObj->currentRoom)
        $self->ivAdd('menuToolItemHash', 'unset_current_room', $item_unsetCurrentRoom);

        $column_rooms->append(Gtk2::SeparatorMenuItem->new());  # Separator

            # 'Locator task' submenu
            my $subMenu_locatorTask = Gtk2::Menu->new();

            my $item_resetLocator = Gtk2::MenuItem->new('_Reset Locator');
            $item_resetLocator->signal_connect('activate' => sub {

                $self->resetLocatorCallback();
            });
            $subMenu_locatorTask->append($item_resetLocator);
            # (Requires $self->currentRegionmap)
            $self->ivAdd('menuToolItemHash', 'reset_locator', $item_resetLocator);

            my $item_updateLocator = Gtk2::MenuItem->new('_Update Locator');
            $item_updateLocator->signal_connect('activate' => sub {

                # Update the Locator task
                $self->mapObj->updateLocator();
            });
            $subMenu_locatorTask->append($item_updateLocator);
            # (Requires $self->currentRegionmap & $self->mapObj->currentRoom)
            $self->ivAdd('menuToolItemHash', 'update_locator', $item_updateLocator);

            my $item_editLocatorRoom = Gtk2::MenuItem->new('_Edit Locator room...');
            $item_editLocatorRoom->signal_connect('activate' => sub {

                $self->editLocatorRoomCallback();
            });
            $subMenu_locatorTask->append($item_editLocatorRoom);

        my $item_locatorTask = Gtk2::MenuItem->new('_Locator task');
        $item_locatorTask->set_submenu($subMenu_locatorTask);
        $column_rooms->append($item_locatorTask);

            # 'Pathfinding' submenu
            my $subMenu_pathFinding = Gtk2::Menu->new();

            my $item_highlightPath = Gtk2::MenuItem->new('_Highlight path');
            $item_highlightPath->signal_connect('activate' => sub {

                $self->processPathCallback('select_room');
            });
            $subMenu_pathFinding->append($item_highlightPath);
            # (Requires $self->currentRegionmap, $self->mapObj->currentRoom and $self->selectedRoom)
            $self->ivAdd('menuToolItemHash', 'path_finding_highlight', $item_highlightPath);

            my $item_displayPath = Gtk2::MenuItem->new('_Edit path');
            $item_displayPath->signal_connect('activate' => sub {

                $self->processPathCallback('pref_win');
            });
            $subMenu_pathFinding->append($item_displayPath);
            # (Requires $self->currentRegionmap, $self->mapObj->currentRoom and $self->selectedRoom)
            $self->ivAdd('menuToolItemHash', 'path_finding_edit', $item_displayPath);

            my $item_goToRoom = Gtk2::MenuItem->new('_Go to room');
            $item_goToRoom->signal_connect('activate' => sub {

                $self->processPathCallback('send_char');
            });
            $subMenu_pathFinding->append($item_goToRoom);
            # (Requires $self->currentRegionmap, $self->mapObj->currentRoom and $self->selectedRoom)
            $self->ivAdd('menuToolItemHash', 'path_finding_go', $item_goToRoom);

            $subMenu_pathFinding->append(Gtk2::SeparatorMenuItem->new());   # Separator

            my $item_allowPostProcessing = Gtk2::CheckMenuItem->new('_Allow post-processing');
            $item_allowPostProcessing->set_active($self->worldModelObj->postProcessingFlag);
            $item_allowPostProcessing->signal_connect('toggled' => sub {

                $self->worldModelObj->toggleFlag(
                    'postProcessingFlag',
                    $item_allowPostProcessing->get_active(),
                    FALSE,      # Don't call $self->drawRegion
                    'allow_post_process',
                );
            });
            $subMenu_pathFinding->append($item_allowPostProcessing);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'allow_post_process', $item_allowPostProcessing);

            my $item_avoidHazardousRooms = Gtk2::CheckMenuItem->new('Avoid ha_zardous rooms');
            $item_avoidHazardousRooms->set_active($self->worldModelObj->avoidHazardsFlag);
            $item_avoidHazardousRooms->signal_connect('toggled' => sub {

                $self->worldModelObj->toggleFlag(
                    'avoidHazardsFlag',
                    $item_avoidHazardousRooms->get_active(),
                    FALSE,      # Don't call $self->drawRegion
                    'allow_hazard_rooms',
                );
            });
            $subMenu_pathFinding->append($item_avoidHazardousRooms);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'allow_hazard_rooms', $item_avoidHazardousRooms);

            my $item_doubleClickPathFind = Gtk2::CheckMenuItem->new(
                'Allow _double-click moves',
            );
            $item_doubleClickPathFind->set_active($self->worldModelObj->quickPathFindFlag);
            $item_doubleClickPathFind->signal_connect('toggled' => sub {

                $self->worldModelObj->toggleFlag(
                    'quickPathFindFlag',
                    $item_doubleClickPathFind->get_active(),
                    FALSE,      # Don't call $self->drawRegion
                    'allow_quick_path_find',
                );
            });
            $subMenu_pathFinding->append($item_doubleClickPathFind);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'allow_quick_path_find', $item_doubleClickPathFind);

        my $item_pathFinding = Gtk2::MenuItem->new('_Pathfinding');
        $item_pathFinding->set_submenu($subMenu_pathFinding);
        $column_rooms->append($item_pathFinding);

        $column_rooms->append(Gtk2::SeparatorMenuItem->new());  # Separator

            # 'Add room' submenu
            my $subMenu_addRoom = Gtk2::Menu->new();

            my $item_addFirstRoom = Gtk2::MenuItem->new('Add _first room');
            $item_addFirstRoom->signal_connect('activate' => sub {

                $self->addFirstRoomCallback();
            });
            $subMenu_addRoom->append($item_addFirstRoom);
            # (Requires $self->currentRegionmap & an empty $self->currentRegionmap->gridRoomHash)
            $self->ivAdd('menuToolItemHash', 'add_first_room', $item_addFirstRoom);

            my $item_addRoomAtClick = Gtk2::MenuItem->new('Add room at _click');
            $item_addRoomAtClick->signal_connect('activate' => sub {

                # Set the free clicking mode: $self->mouseClickEvent will create the new room when
                #   the user next clicks on an empty part of the map
                if ($self->currentRegionmap) {

                    $self->ivPoke('freeClickMode', 'add_room');
                }
            });
            $subMenu_addRoom->append($item_addRoomAtClick);

            my $item_addRoomAtBlock = Gtk2::MenuItem->new('Add room at _block...');
            $item_addRoomAtBlock->signal_connect('activate' => sub {

                $self->addRoomAtBlockCallback();
            });
            $subMenu_addRoom->append($item_addRoomAtBlock);

        my $item_addRoom = Gtk2::ImageMenuItem->new('Add _room');
        my $img_addRoom = Gtk2::Image->new_from_stock('gtk-add', 'menu');
        $item_addRoom->set_image($img_addRoom);
        $item_addRoom->set_submenu($subMenu_addRoom);
        $column_rooms->append($item_addRoom);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'add_room', $item_addRoom);

            # 'Add exit' submenu
            my $subMenu_addExit = Gtk2::Menu->new();

            my $item_addNormal = Gtk2::MenuItem->new('Add _normal exit...');
            $item_addNormal->signal_connect('activate' => sub {

                $self->addExitCallback(FALSE);  # FALSE - not a hidden exit
            });
            $subMenu_addExit->append($item_addNormal);

            my $item_addMultiple = Gtk2::MenuItem->new('Add _multiple exits...');
            $item_addMultiple->signal_connect('activate' => sub {

                $self->addMultipleExitsCallback();
            });
            $subMenu_addExit->append($item_addMultiple);

            $subMenu_addExit->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_addHiddenExit = Gtk2::MenuItem->new('Add _hidden exit...');
            $item_addHiddenExit->signal_connect('activate' => sub {

                $self->addExitCallback(TRUE);   # TRUE - a hidden exit
            });
            $subMenu_addExit->append($item_addHiddenExit);

        my $item_addExit = Gtk2::ImageMenuItem->new('_Add exit to room');
        my $img_addExit = Gtk2::Image->new_from_stock('gtk-add', 'menu');
        $item_addExit->set_image($img_addExit);
        $item_addExit->set_submenu($subMenu_addExit);
        $column_rooms->append($item_addExit);
        # (Requires $self->currentRegionmap & $self->selectedRoom)
        $self->ivAdd('menuToolItemHash', 'room_exits', $item_addExit);

            # 'Add patterns' submenu
            my $subMenu_exitPatterns = Gtk2::Menu->new();

            my $item_addFailedExitWorld = Gtk2::MenuItem->new('Add failed exit to _world...');
            $item_addFailedExitWorld->signal_connect('activate' => sub {

                $self->addFailedExitCallback(TRUE);
            });
            $subMenu_exitPatterns->append($item_addFailedExitWorld);

            my $item_addFailedExitRoom = Gtk2::MenuItem->new('Add failed exit to _room...');
            $item_addFailedExitRoom->signal_connect('activate' => sub {

                $self->addFailedExitCallback(FALSE, $self->mapObj->currentRoom);
            });
            $subMenu_exitPatterns->append($item_addFailedExitRoom);
            # (Requires $self->currentRegionmap & $self->mapObj->currentRoom)
            $self->ivAdd('menuToolItemHash', 'add_failed_room', $item_addFailedExitRoom);

            $subMenu_exitPatterns->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_addInvoluntaryExitRoom = Gtk2::MenuItem->new(
                'Add _involuntary exit to room...',
            );
            $item_addInvoluntaryExitRoom->signal_connect('activate' => sub {

                $self->addInvoluntaryExitCallback($self->mapObj->currentRoom);
            });
            $subMenu_exitPatterns->append($item_addInvoluntaryExitRoom);
            # (Requires $self->currentRegionmap & $self->mapObj->currentRoom)
            $self->ivAdd('menuToolItemHash', 'add_involuntary_exit', $item_addInvoluntaryExitRoom);

            my $item_addRepulseExitRoom = Gtk2::MenuItem->new(
                'Add r_epulse exit to room...',
            );
            $item_addRepulseExitRoom->signal_connect('activate' => sub {

                $self->addRepulseExitCallback($self->mapObj->currentRoom);
            });
            $subMenu_exitPatterns->append($item_addRepulseExitRoom);
            # (Requires $self->currentRegionmap & $self->mapObj->currentRoom)
            $self->ivAdd('menuToolItemHash', 'add_repulse_exit', $item_addRepulseExitRoom);

            my $item_addSpecialDepartRoom = Gtk2::MenuItem->new(
                'Add _special departure to room...',
            );
            $item_addSpecialDepartRoom->signal_connect('activate' => sub {

                $self->addSpecialDepartureCallback($self->mapObj->currentRoom);
            });
            $subMenu_exitPatterns->append($item_addSpecialDepartRoom);
            # (Requires $self->currentRegionmap & $self->mapObj->currentRoom)
            $self->ivAdd('menuToolItemHash', 'add_special_depart', $item_addSpecialDepartRoom);

        my $item_exitPatterns = Gtk2::MenuItem->new('Add patter_n');
        $item_exitPatterns->set_submenu($subMenu_exitPatterns);
        $column_rooms->append($item_exitPatterns);

            # 'Add to model' submenu
            my $subMenu_addToModel = Gtk2::Menu->new();

            my $item_addRoomContents = Gtk2::MenuItem->new('Add _contents...');
            $item_addRoomContents->signal_connect('activate' => sub {

                $self->addContentsCallback(FALSE);
            });
            $subMenu_addToModel->append($item_addRoomContents);
            # Requires $self->currentRegionmap, $self->mapObj->currentRoom
            $self->ivAdd('menuToolItemHash', 'add_room_contents', $item_addRoomContents);

            my $item_addContentsString = Gtk2::MenuItem->new('Add contents from _string...');
            $item_addContentsString->signal_connect('activate' => sub {

                $self->addContentsCallback(TRUE);
            });
            $subMenu_addToModel->append($item_addContentsString);
            # Requires $self->currentRegionmap, $self->selectedRoom
            $self->ivAdd('menuToolItemHash', 'add_contents_string', $item_addContentsString);

            $subMenu_addToModel->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_addHiddenObj = Gtk2::MenuItem->new('Add _hidden object...');
            $item_addHiddenObj->signal_connect('activate' => sub {

                $self->addHiddenObjCallback(FALSE);
            });
            $subMenu_addToModel->append($item_addHiddenObj);
            # Requires $self->currentRegionmap, $self->mapObj->currentRoom
            $self->ivAdd('menuToolItemHash', 'add_hidden_object', $item_addHiddenObj);

            my $item_addHiddenString = Gtk2::MenuItem->new('Add hidden object _from string...');
            $item_addHiddenString->signal_connect('activate' => sub {

                $self->addHiddenObjCallback(TRUE);
            });
            $subMenu_addToModel->append($item_addHiddenString);
            # Requires $self->currentRegionmap, $self->selectedRoom
            $self->ivAdd('menuToolItemHash', 'add_hidden_string', $item_addHiddenString);

            $subMenu_addToModel->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_addSearchResult = Gtk2::MenuItem->new('Add search _result...');
            $item_addSearchResult->signal_connect('activate' => sub {

                $self->addSearchResultCallback();
            });
            $subMenu_addToModel->append($item_addSearchResult);
            # Requires $self->currentRegionmap and $self->mapObj->currentRoom
            $self->ivAdd('menuToolItemHash', 'add_search_result', $item_addSearchResult);

        my $item_addToModel = Gtk2::MenuItem->new('Add to _model');
        $item_addToModel->set_submenu($subMenu_addToModel);
        $column_rooms->append($item_addToModel);
        # Requires $self->currentRegionmap and either $self->mapObj->currentRoom or
        #   $self->selectedRoom
        $self->ivAdd('menuToolItemHash', 'add_to_model', $item_addToModel);

        $column_rooms->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_selectExit = Gtk2::MenuItem->new('Select e_xit in room...');
        $item_selectExit->signal_connect('activate' => sub {

            $self->selectExitCallback();
        });
        $column_rooms->append($item_selectExit);
        # (Requires $self->currentRegionmap & $self->selectedRoom)
        $self->ivAdd('menuToolItemHash', 'select_exit', $item_selectExit);

        $column_rooms->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_identifyRoom = Gtk2::MenuItem->new('_Identify room(s)');
        $item_identifyRoom->signal_connect('activate' => sub {

            $self->identifyRoomsCallback();
        });
        $column_rooms->append($item_identifyRoom);
        # (Requires $self->currentRegionmap and EITHER $self->selectedRoom or
        #   $self->selectedRoomHash or $self->mapObj->currentRoom)
        $self->ivAdd('menuToolItemHash', 'identify_room', $item_identifyRoom);

        my $item_editRoom = Gtk2::ImageMenuItem->new('_Edit room...');
        my $img_editRoom = Gtk2::Image->new_from_stock('gtk-edit', 'menu');
        $item_editRoom->set_image($img_editRoom);
        $item_editRoom->signal_connect('activate' => sub {

            # Open the room's 'edit' window
            $self->createFreeWin(
                'Games::Axmud::EditWin::ModelObj::Room',
                $self,
                $self->session,
                'Edit ' . $self->selectedRoom->category . ' model object',
                $self->selectedRoom,
                FALSE,                          # Not temporary
            );
        });
        $column_rooms->append($item_editRoom);
        # (Requires $self->currentRegionmap & $self->selectedRoom)
        $self->ivAdd('menuToolItemHash', 'edit_room', $item_editRoom);

            # 'Source code' submenu
            my $subMenu_sourceCode = Gtk2::Menu->new();

            my $item_setFilePath = Gtk2::MenuItem->new('_Set file path...');
            $item_setFilePath->signal_connect('activate' => sub {

                $self->setFilePathCallback();
            });
            $subMenu_sourceCode->append($item_setFilePath);
            # (Requires $self->currentRegionmap and $self->selectedRoom)
            $self->ivAdd('menuToolItemHash', 'set_file_path', $item_setFilePath);

            my $item_setVirtualArea = Gtk2::MenuItem->new('Set virtual _area...');
            $item_setVirtualArea->signal_connect('activate' => sub {

                $self->setVirtualAreaCallback(TRUE);
            });
            $subMenu_sourceCode->append($item_setVirtualArea);
            # (Requires $self->currentRegionmap & either $self->selectedRoom or
            #   $self->selectedRoomHash)
            $self->ivAdd('menuToolItemHash', 'set_virtual_area', $item_setVirtualArea);

            my $item_resetVirtualArea = Gtk2::MenuItem->new('_Reset virtual area...');
            $item_resetVirtualArea->signal_connect('activate' => sub {

                $self->setVirtualAreaCallback(FALSE);
            });
            $subMenu_sourceCode->append($item_resetVirtualArea);
            # (Requires $self->currentRegionmap & either $self->selectedRoom or
            #   $self->selectedRoomHash)
            $self->ivAdd('menuToolItemHash', 'reset_virtual_area', $item_resetVirtualArea);

            my $item_showSourceCode = Gtk2::MenuItem->new('S_how file paths');
            $item_showSourceCode->signal_connect('activate' => sub {

                # (Don't use $self->pseudoCmdMode - we want to see the footer messages)
                $self->session->pseudoCmd('listsourcecode', 'show_all');
            });
            $subMenu_sourceCode->append($item_showSourceCode);

            $subMenu_sourceCode->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_viewSourceCode = Gtk2::MenuItem->new('_View file...');
            $item_viewSourceCode->signal_connect('activate' => sub {

                $self->quickFreeWin(
                    'Games::Axmud::OtherWin::SourceCode',
                    $self->session,
                    # Config
                    'model_obj' => $self->selectedRoom,
                );
            });
            $subMenu_sourceCode->append($item_viewSourceCode);
            # (Requires $self->currentRegionmap, $self->selectedRoom &
            #   $self->selectedRoom->sourceCodePath & empty $self->selectedRoom->virtualAreaPath)
            $self->ivAdd('menuToolItemHash', 'view_source_code', $item_viewSourceCode);

            my $item_editSourceCode = Gtk2::MenuItem->new('_Edit file...');
            $item_editSourceCode->signal_connect('activate' => sub {

                $self->editFileCallback();
            });
            $subMenu_sourceCode->append($item_editSourceCode);
            # (Requires $self->currentRegionmap, $self->selectedRoom &
            #   $self->selectedRoom->sourceCodePath & empty $self->selectedRoom->virtualAreaPath)
            $self->ivAdd('menuToolItemHash', 'edit_source_code', $item_editSourceCode);

            $subMenu_sourceCode->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_viewVirtualArea = Gtk2::MenuItem->new('View virtual area _file...');
            $item_viewVirtualArea->signal_connect('activate' => sub {

                $self->quickFreeWin(
                    'Games::Axmud::OtherWin::SourceCode',
                    $self->session,
                    # Config
                    'model_obj' => $self->selectedRoom,
                    'virtual_flag' => TRUE,
                );
            });
            $subMenu_sourceCode->append($item_viewVirtualArea);
            # (Requires $self->currentRegionmap, $self->selectedRoom &
            #   $self->selectedRoom->virtualAreaPath
            $self->ivAdd('menuToolItemHash', 'view_virtual_area', $item_viewVirtualArea);

            my $item_editVirtualArea = Gtk2::MenuItem->new('E_dit virtual area file...');
            $item_editVirtualArea->signal_connect('activate' => sub {

                # Use TRUE to specify that the virtual area file should be opened
                $self->editFileCallback(TRUE);
            });
            $subMenu_sourceCode->append($item_editVirtualArea);
            # (Requires $self->currentRegionmap, $self->selectedRoom &
            #   $self->selectedRoom->virtualAreaPath
            $self->ivAdd('menuToolItemHash', 'edit_virtual_area', $item_editVirtualArea);

        my $item_sourceCode = Gtk2::MenuItem->new('Source _code');
        $item_sourceCode->set_submenu($subMenu_sourceCode);
        $column_rooms->append($item_sourceCode);

        $column_rooms->append(Gtk2::SeparatorMenuItem->new());  # Separator

            # 'Toggle room flag' submenu
            my $subMenu_toggleRoomFlag = Gtk2::Menu->new();

            foreach my $filter ($self->worldModelObj->roomFilterList) {

                my $flagListRef;

                # A sub-sub menu for $filter
                my $subMenu = Gtk2::Menu->new();

                $flagListRef = $self->worldModelObj->ivShow('roomFlagReverseHash', $filter);
                foreach my $flag (@$flagListRef) {

                    my $menuItem = Gtk2::MenuItem->new(
                        $self->worldModelObj->ivShow('roomFlagDescripHash', $flag),
                    );
                    $menuItem->signal_connect('activate' => sub {

                        # Toggle the flags for all selected rooms, redraw them and (if the flag is
                        #   one of the hazardous room flags) recalculate the regionmap's paths. The
                        #   TRUE argument tells the world model to redraw the rooms
                        $self->worldModelObj->toggleRoomFlags(
                            $self->session,
                            TRUE,
                            $flag,
                            $self->compileSelectedRooms(),
                        );
                    });
                    $subMenu->append($menuItem);
                }

                my $menuItem = Gtk2::MenuItem->new(ucfirst($filter));
                $menuItem->set_submenu($subMenu);
                $subMenu_toggleRoomFlag->append($menuItem);
            }

        my $item_toggleRoomFlag = Gtk2::MenuItem->new('_Toggle room flags');
        $item_toggleRoomFlag->set_submenu($subMenu_toggleRoomFlag);
        $column_rooms->append($item_toggleRoomFlag);
        # (Requires $self->currentRegionmap & either $self->selectedRoom or
        #   $self->selectedRoomHash)
        $self->ivAdd('menuToolItemHash', 'toggle_room_flag_sub', $item_toggleRoomFlag);

            # 'Room text' submenu
            my $subMenu_roomText = Gtk2::Menu->new();

            my $item_setRoomTag = Gtk2::MenuItem->new('Set room _tag...');
            $item_setRoomTag->signal_connect('activate' => sub {

                $self->setRoomTagCallback();
            });
            $subMenu_roomText->append($item_setRoomTag);
            # (Requires $self->currentRegionmap and either $self->selectedRoom or
            #   $self->selectedRoomTag)
            $self->ivAdd('menuToolItemHash', 'set_room_tag', $item_setRoomTag);

            my $item_setGuild = Gtk2::MenuItem->new('Set room _guild...');
            $item_setGuild->signal_connect('activate' => sub {

                $self->setRoomGuildCallback();
            });
            $subMenu_roomText->append($item_setGuild);
            # (Requires $self->currentRegionmap and one or more of $self->selectedRoom,
            #   $self->selectedRoomHash, $self->selectedRoomGuild, $self->selectedRoomGuildHash)
            $self->ivAdd('menuToolItemHash', 'set_room_guild', $item_setGuild);

            $subMenu_roomText->append(Gtk2::SeparatorMenuItem->new());  # Separator

            my $item_resetPositions = Gtk2::MenuItem->new('_Reset text positions');
            $item_resetPositions->signal_connect('activate' => sub {

                $self->resetRoomOffsetsCallback();
            });
            $subMenu_roomText->append($item_resetPositions);
            # (Requires $self->currentRegionmap & $self->selectedRoom)
            $self->ivAdd('menuToolItemHash', 'reset_positions', $item_resetPositions);

        my $item_roomText = Gtk2::MenuItem->new('Set r_oom text');
        $item_roomText->set_submenu($subMenu_roomText);
        $column_rooms->append($item_roomText);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'room_text', $item_roomText);

            # 'Room exclusivity' submenu
            my $subMenu_exclusivity = Gtk2::Menu->new();

            my $item_toggleExclusivity = Gtk2::MenuItem->new('_Toggle exclusivity');
            $item_toggleExclusivity->signal_connect('activate' => sub {

                $self->toggleExclusiveProfileCallback();
            });
            $subMenu_exclusivity->append($item_toggleExclusivity);
            # (Requires $self->currentRegionmap & either $self->selectedRoom or
            #   $self->selectedRoomHash)
            $self->ivAdd('menuToolItemHash', 'toggle_exclusivity', $item_toggleExclusivity);

            $subMenu_exclusivity->append(Gtk2::SeparatorMenuItem->new());  # Separator

            my $item_addExclusiveProf = Gtk2::MenuItem->new('_Add exclusive profile...');
            $item_addExclusiveProf->signal_connect('activate' => sub {

                $self->addExclusiveProfileCallback();
            });
            $subMenu_exclusivity->append($item_addExclusiveProf);
            # (Requires $self->currentRegionmap & $self->selectedRoom)
            $self->ivAdd('menuToolItemHash', 'add_exclusive_prof', $item_addExclusiveProf);

            my $item_clearExclusiveProf = Gtk2::MenuItem->new('_Clear exclusive profiles');
            $item_clearExclusiveProf->signal_connect('activate' => sub {

                $self->resetExclusiveProfileCallback();
            });
            $subMenu_exclusivity->append($item_clearExclusiveProf);
            # (Requires $self->currentRegionmap & either $self->selectedRoom or
            #   $self->selectedRoomHash)
            $self->ivAdd('menuToolItemHash', 'clear_exclusive_profs', $item_clearExclusiveProf);

        my $item_exclusivity = Gtk2::MenuItem->new('Room exclusi_vity');
        $item_exclusivity->set_submenu($subMenu_exclusivity);
        $column_rooms->append($item_exclusivity);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'room_exclusivity', $item_exclusivity);

        $column_rooms->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_deleteRoom = Gtk2::ImageMenuItem->new('_Delete rooms');
        my $img_deleteRoom = Gtk2::Image->new_from_stock('gtk-delete', 'menu');
        $item_deleteRoom->set_image($img_deleteRoom);
        $item_deleteRoom->signal_connect('activate' => sub {

            $self->worldModelObj->deleteRooms(
                $self->session,
                TRUE,           # Update Automapper windows now
                $self->compileSelectedRooms(),
            );
        });
        $column_rooms->append($item_deleteRoom);
        # (Requires $self->currentRegionmap & either $self->selectedRoom or
        #   $self->selectedRoomHash)
        $self->ivAdd('menuToolItemHash', 'delete_room', $item_deleteRoom);

        # Setup complete
        return $column_rooms;
    }

    sub enableExitsColumn {

        # Called by $self->enableMenu
        # Sets up the 'Exits' column of the Automapper window's menu bar
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Local variables
        my @titleList;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableExitsColumn', @_);
        }

        # Set up column
        my $column_exits = Gtk2::Menu->new();
        if (! $column_exits) {

            return undef;
        }

        my $item_changeDirection = Gtk2::MenuItem->new('C_hange direction...');
        $item_changeDirection->signal_connect('activate' => sub {

            $self->changeDirCallback();
        });
        $column_exits->append($item_changeDirection);
        # (Requires $self->currentRegionmap and $self->selectedExit and
        #   $self->selectedExit->drawMode is 'primary' or 'perm_alloc')
        $self->ivAdd('menuToolItemHash', 'change_direction', $item_changeDirection);

        my $item_setAssisted = Gtk2::MenuItem->new('Set assisted _move...');
        $item_setAssisted->signal_connect('activate' => sub {

            $self->setAssistedMoveCallback();
        });
        $column_exits->append($item_setAssisted);
        # (Requires $self->currentRegionmap and $self->selectedExit and
        #   $self->selectedExit->drawMode is 'primary', 'temp_unalloc' or 'perm_alloc')
        $self->ivAdd('menuToolItemHash', 'set_assisted_move', $item_setAssisted);

            # 'Allocate map direction' submenu
            my $subMenu_allocateMapDir = Gtk2::Menu->new();

            my $item_allocatePrimary = Gtk2::MenuItem->new('Choose _direction...');
            $item_allocatePrimary->signal_connect('activate' => sub {

                $self->allocateMapDirCallback();
            });
            $subMenu_allocateMapDir->append($item_allocatePrimary);

            my $item_confirmTwoWay = Gtk2::MenuItem->new('Confirm _two-way exit...');
            $item_confirmTwoWay->signal_connect('activate' => sub {

                $self->confirmTwoWayCallback();
            });
            $subMenu_allocateMapDir->append($item_confirmTwoWay);

        my $item_allocateMapDir = Gtk2::MenuItem->new('_Allocate map direction');
        $item_allocateMapDir->set_submenu($subMenu_allocateMapDir);
        $column_exits->append($item_allocateMapDir);
        # (Requires $self->currentRegionmap and $self->selectedExit and
        #   $self->selectedExit->drawMode is 'temp_alloc' or 'temp_unalloc')
        $self->ivAdd('menuToolItemHash', 'allocate_map_dir', $item_allocateMapDir);

        my $item_allocateShadow = Gtk2::MenuItem->new('Allocate _shadow...');
        $item_allocateShadow->signal_connect('activate' => sub {

            $self->allocateShadowCallback();
        });
        $column_exits->append($item_allocateShadow);
        # (Requires $self->currentRegionmap and $self->selectedExit and
        #   $self->selectedExit->drawMode is 'temp_alloc' or 'temp_unalloc')
        $self->ivAdd('menuToolItemHash', 'allocate_shadow', $item_allocateShadow);

        $column_exits->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_connectExitToClick = Gtk2::MenuItem->new('_Connect to click');
        $item_connectExitToClick->signal_connect('activate' => sub {

            $self->connectToClickCallback();
        });
        $column_exits->append($item_connectExitToClick);
        # (Requires $self->currentRegionmap, $self->selectedExit and
        #   $self->selectedExit->drawMode 'primary', 'temp_unalloc' or 'perm_alloc')
        $self->ivAdd('menuToolItemHash', 'connect_to_click', $item_connectExitToClick);

        my $item_disconnectExit = Gtk2::MenuItem->new('D_isconnect exit');
        $item_disconnectExit->signal_connect('activate' => sub {

            $self->disconnectExitCallback();
        });
        $column_exits->append($item_disconnectExit);
        # (Requires $self->currentRegionmap and $self->selectedExit)
        $self->ivAdd('menuToolItemHash', 'disconnect_exit', $item_disconnectExit);

        $column_exits->append(Gtk2::SeparatorMenuItem->new());  # Separator

            # 'Set ornaments' submenu
            my $subMenu_setOrnament = Gtk2::Menu->new();

            # Create a list of exit ornament types, in groups of two, in the form
            #   (menu_item_title, IV_to_be_set)
            @titleList = (
                '_No ornament', undef,
                '_Openable exit', 'openFlag',
                '_Lockable exit', 'lockFlag',
                '_Pickable exit', 'pickFlag',
                '_Breakable exit', 'breakFlag',
                '_Impassable exit', 'impassFlag',
            );

            do {

                my ($title, $iv);

                $title = shift @titleList;
                $iv = shift @titleList;

                my $menuItem = Gtk2::MenuItem->new($title);
                $menuItem->signal_connect('activate' => sub {

                    $self->exitOrnamentCallback($iv);
                });
                $subMenu_setOrnament->append($menuItem);

            } until (! @titleList);

            $subMenu_setOrnament->append(Gtk2::SeparatorMenuItem->new());   # Separator

            my $item_setTwinOrnament = Gtk2::CheckMenuItem->new('Also set _twin exits');
            $item_setTwinOrnament->set_active($self->worldModelObj->setTwinOrnamentFlag);
            $item_setTwinOrnament->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'setTwinOrnamentFlag',
                        $item_setTwinOrnament->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'also_set_twin_exits',
                    );
                }
            });
            $subMenu_setOrnament->append($item_setTwinOrnament);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'also_set_twin_exits', $item_setTwinOrnament);

        my $item_setOrnament = Gtk2::MenuItem->new('Set _ornaments');
        $item_setOrnament->set_submenu($subMenu_setOrnament);
        $column_exits->append($item_setOrnament);
        # (Requires $self->currentRegionmap & either $self->selectedExit or
        #   $self->selectedExitHash)
        $self->ivAdd('menuToolItemHash', 'set_ornament_sub', $item_setOrnament);

            # 'Set exit type' submenu
            my $subMenu_setExitType = Gtk2::Menu->new();

                # 'Set hidden' sub-submenu
                my $subMenu_setHidden = Gtk2::Menu->new();

                my $item_setHiddenExit = Gtk2::MenuItem->new('Mark exit _hidden');
                $item_setHiddenExit->signal_connect('activate' => sub {

                    $self->hiddenExitCallback(TRUE);
                });
                $subMenu_setHidden->append($item_setHiddenExit);

                my $item_setNotHiddenExit = Gtk2::MenuItem->new('Mark exit _not hidden');
                $item_setNotHiddenExit->signal_connect('activate' => sub {

                    $self->hiddenExitCallback(FALSE);
                });
                $subMenu_setHidden->append($item_setNotHiddenExit);

            my $item_setHidden = Gtk2::MenuItem->new('Set _hidden');
            $item_setHidden->set_submenu($subMenu_setHidden);
            $subMenu_setExitType->append($item_setHidden);
            # (Requires $self->currentRegionmap and $self->selectedExit)
            $self->ivAdd('menuToolItemHash', 'set_hidden_sub', $item_setHidden);

                # 'Set broken' sub-submenu
                my $subMenu_setBroken = Gtk2::Menu->new();

                my $item_markBrokenExit = Gtk2::MenuItem->new('_Mark exit as broken');
                $item_markBrokenExit->signal_connect('activate' => sub {

                    $self->markBrokenExitCallback();
                });
                $subMenu_setBroken->append($item_markBrokenExit);

                my $item_toggleBrokenExit = Gtk2::MenuItem->new('_Toggle bent broken exit');
                $item_toggleBrokenExit->signal_connect('activate' => sub {

                    $self->worldModelObj->toggleBentExit(
                        TRUE,                       # Update Automapper windows now
                        $self->selectedExit,
                    );
                });
                $subMenu_setBroken->append($item_toggleBrokenExit);
                # (Requires $self->currentRegionmap and a $self->selectedExit which is a broken
                #   exit)
                $self->ivAdd('menuToolItemHash', 'toggle_bent_exit', $item_toggleBrokenExit);

                $subMenu_setBroken->append(Gtk2::SeparatorMenuItem->new());    # Separator

                my $item_restoreBrokenExit = Gtk2::MenuItem->new('_Restore unbroken exit');
                $item_restoreBrokenExit->signal_connect('activate' => sub {

                    $self->restoreBrokenExitCallback();
                });
                $subMenu_setBroken->append($item_restoreBrokenExit);

            my $item_setBroken = Gtk2::MenuItem->new('Set _broken');
            $item_setBroken->set_submenu($subMenu_setBroken);
            $subMenu_setExitType->append($item_setBroken);
            # (Requires $self->currentRegionmap and $self->selectedExit)
            $self->ivAdd('menuToolItemHash', 'set_broken_sub', $item_setBroken);

                # 'Set one-way' sub-submenu
                my $subMenu_setOneWay = Gtk2::Menu->new();

                my $item_markOneWayExit = Gtk2::MenuItem->new('_Mark exit as one-way');
                $item_markOneWayExit->signal_connect('activate' => sub {

                    $self->markOneWayExitCallback();
                });
                $subMenu_setOneWay->append($item_markOneWayExit);

                $subMenu_setOneWay->append(Gtk2::SeparatorMenuItem->new());    # Separator

                my $item_restoreUncertainExit = Gtk2::MenuItem->new('Restore _uncertain exit');
                $item_restoreUncertainExit->signal_connect('activate' => sub {

                    $self->restoreOneWayExitCallback(FALSE);
                });
                $subMenu_setOneWay->append($item_restoreUncertainExit);

                my $item_restoreTwoWayExit = Gtk2::MenuItem->new('Restore _two-way exit');
                $item_restoreTwoWayExit->signal_connect('activate' => sub {

                    $self->restoreOneWayExitCallback(TRUE);
                });
                $subMenu_setOneWay->append($item_restoreTwoWayExit);

                $subMenu_setOneWay->append(Gtk2::SeparatorMenuItem->new());    # Separator

                my $item_setIncomingDir = Gtk2::MenuItem->new('Set incoming direction...');
                $item_setIncomingDir->signal_connect('activate' => sub {

                    $self->setIncomingDirCallback();
                });
                $subMenu_setOneWay->append($item_setIncomingDir);
                # (Requires $self->currentRegionmap and a $self->selectedExit which is a one-way
                #   exit)
                $self->ivAdd('menuToolItemHash', 'set_incoming_dir', $item_setIncomingDir);

            my $item_setOneWay = Gtk2::MenuItem->new('Set _one-way');
            $item_setOneWay->set_submenu($subMenu_setOneWay);
            $subMenu_setExitType->append($item_setOneWay);
            # (Requires $self->currentRegionmap and $self->selectedExit)
            $self->ivAdd('menuToolItemHash', 'set_oneway_sub', $item_setOneWay);

                # 'Set retracing' sub-submenu
                my $subMenu_setRetracing = Gtk2::Menu->new();

                my $item_markRetracingExit = Gtk2::MenuItem->new('_Mark exit as retracing');
                $item_markRetracingExit->signal_connect('activate' => sub {

                    $self->markRetracingExitCallback();
                });
                $subMenu_setRetracing->append($item_markRetracingExit);

                $subMenu_setRetracing->append(Gtk2::SeparatorMenuItem->new());    # Separator

                my $item_restoreRetracingExit = Gtk2::MenuItem->new('Restore _incomplete exit');
                $item_restoreRetracingExit->signal_connect('activate' => sub {

                    $self->restoreRetracingExitCallback();
                });
                $subMenu_setRetracing->append($item_restoreRetracingExit);

            my $item_setRetracing = Gtk2::MenuItem->new('Set _retracing');
            $item_setRetracing->set_submenu($subMenu_setRetracing);
            $subMenu_setExitType->append($item_setRetracing);
            # (Requires $self->currentRegionmap and $self->selectedExit)
            $self->ivAdd('menuToolItemHash', 'set_retracing_sub', $item_setRetracing);

                # 'Set random' sub-submenu
                my $subMenu_setRandomExit = Gtk2::Menu->new();

                my $item_markRandomRegion = Gtk2::MenuItem->new(
                    'Set random destination in same region',
                );
                $item_markRandomRegion->signal_connect('activate' => sub {

                    $self->markRandomExitCallback('same_region');
                });
                $subMenu_setRandomExit->append($item_markRandomRegion);

                my $item_markRandomAnywhere
                    = Gtk2::MenuItem->new('Set random destination anywhere');
                $item_markRandomAnywhere->signal_connect('activate' => sub {

                    $self->markRandomExitCallback('any_region');
                });
                $subMenu_setRandomExit->append($item_markRandomAnywhere);

                my $item_markRandomList = Gtk2::MenuItem->new('Use list of random destinations');
                $item_markRandomList->signal_connect('activate' => sub {

                    $self->markRandomExitCallback('room_list');
                });
                $subMenu_setRandomExit->append($item_markRandomList);

                $subMenu_setRandomExit->append(Gtk2::SeparatorMenuItem->new());    # Separator

                my $item_restoreRandomExit = Gtk2::MenuItem->new('Restore _incomplete exit');
                $item_restoreRandomExit->signal_connect('activate' => sub {

                    $self->restoreRandomExitCallback();
                });
                $subMenu_setRandomExit->append($item_restoreRandomExit);

            my $item_setRandomExit = Gtk2::MenuItem->new('Set r_andom');
            $item_setRandomExit->set_submenu($subMenu_setRandomExit);
            $subMenu_setExitType->append($item_setRandomExit);
            # (Requires $self->currentRegionmap and $self->selectedExit)
            $self->ivAdd('menuToolItemHash', 'set_random_sub', $item_setRandomExit);

                # 'Set super' sub-submenu
                my $subMenu_setSuperExit = Gtk2::Menu->new();

                my $item_markSuper = Gtk2::MenuItem->new('Mark exit as _super-region exit');
                $item_markSuper->signal_connect('activate' => sub {

                    $self->markSuperExitCallback(FALSE);
                });
                $subMenu_setSuperExit->append($item_markSuper);

                my $item_markSuperExcl = Gtk2::MenuItem->new(
                    '_Mark exit as exclusive super-region exit',
                );
                $item_markSuperExcl->signal_connect('activate' => sub {

                    $self->markSuperExitCallback(TRUE);
                });
                $subMenu_setSuperExit->append($item_markSuperExcl);

                $subMenu_setSuperExit->append(Gtk2::SeparatorMenuItem->new());    # Separator

                my $item_markNotSuper = Gtk2::MenuItem->new('Mark exit as _normal region exit');
                $item_markNotSuper->signal_connect('activate' => sub {

                    $self->restoreSuperExitCallback();
                });
                $subMenu_setSuperExit->append($item_markNotSuper);

            my $item_setSuperExit = Gtk2::MenuItem->new('Set _super');
            $item_setSuperExit->set_submenu($subMenu_setSuperExit);
            $subMenu_setExitType->append($item_setSuperExit);
            # (Requires $self->currentRegionmap and $self->selectedExit which is a region exit)
            $self->ivAdd('menuToolItemHash', 'set_super_sub', $item_setSuperExit);

            $subMenu_setExitType->append(Gtk2::SeparatorMenuItem->new());    # Separator

            my $item_setExitTwin = Gtk2::MenuItem->new('Set exit _twin...');
            $item_setExitTwin->signal_connect('activate' => sub {

                $self->setExitTwinCallback();
            });
            $subMenu_setExitType->append($item_setExitTwin);
            # (Requires $self->currentRegionmap and a $self->selectedExit which is either a one-way
            #   exit or an uncertain exit)
            $self->ivAdd('menuToolItemHash', 'set_exit_twin', $item_setExitTwin);

        my $item_setExitType = Gtk2::MenuItem->new('Set e_xit type');
        $item_setExitType->set_submenu($subMenu_setExitType);
        $column_exits->append($item_setExitType);
        # (Requires $self->currentRegionmap and $self->selectedExit)
        $self->ivAdd('menuToolItemHash', 'set_exit_type', $item_setExitType);

            # 'Exit tags' submenu
            my $subMenu_exitTags = Gtk2::Menu->new();

            my $item_setExitText = Gtk2::MenuItem->new('_Edit tag text');
            $item_setExitText->signal_connect('activate' => sub {

                $self->editExitTagCallback();
            });
            $subMenu_exitTags->append($item_setExitText);
            # (Requires $self->currentRegionmap and either a $self->selectedExit which is a region
            #   exit, or a $self->selectedExitTag)
            $self->ivAdd('menuToolItemHash', 'edit_tag_text', $item_setExitText);

            my $item_toggleExitTag = Gtk2::MenuItem->new('_Toggle exit tag');
            $item_toggleExitTag->signal_connect('activate' => sub {

                $self->toggleExitTagCallback();
            });
            $subMenu_exitTags->append($item_toggleExitTag);
            # (Requires $self->currentRegionmap and either a $self->selectedExit which is a region
            #   exit, or a $self->selectedExitTag)
            $self->ivAdd('menuToolItemHash', 'toggle_exit_tag', $item_toggleExitTag);

            $subMenu_exitTags->append(Gtk2::SeparatorMenuItem->new());    # Separator

            my $item_resetPositions = Gtk2::MenuItem->new('Reset tag positio_ns');
            $item_resetPositions->signal_connect('activate' => sub {

                $self->resetExitOffsetsCallback();
            });
            $subMenu_exitTags->append($item_resetPositions);
            # (Requires $self->currentRegionmap and one or more of $self->selectedExit,
            #   $self->selectedExitHash, $self->selectedExitTag and $self->selectedExitTagHash)
            $self->ivAdd('menuToolItemHash', 'reset_exit_tags', $item_resetPositions);

            $subMenu_exitTags->append(Gtk2::SeparatorMenuItem->new());    # Separator

            my $item_applyExitTags = Gtk2::MenuItem->new('_Apply all tags in region');
            $item_applyExitTags->signal_connect('activate' => sub {

                $self->applyExitTagsCallback(TRUE);
            });
            $subMenu_exitTags->append($item_applyExitTags);


            my $item_cancelExitTags = Gtk2::MenuItem->new('_Cancel all tags in region');
            $item_cancelExitTags->signal_connect('activate' => sub {

                $self->applyExitTagsCallback(FALSE);
            });
            $subMenu_exitTags->append($item_cancelExitTags);

        my $item_exitTags = Gtk2::MenuItem->new('Exit _tags');
        $item_exitTags->set_submenu($subMenu_exitTags);
        $column_exits->append($item_exitTags);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'exit_tags', $item_exitTags);

        $column_exits->append(Gtk2::SeparatorMenuItem->new());  # Separator

            # 'Find exits' submenu
            my $subMenu_findExits = Gtk2::Menu->new();

            my $item_identifyExit = Gtk2::MenuItem->new('_Identify selected exit(s)');
            $item_identifyExit->signal_connect('activate' => sub {

                $self->identifyExitsCallback();
            });
            $subMenu_findExits->append($item_identifyExit);
            # (Requires $self->currentRegionmap & either $self->selectedExit or
            #   $self->selectedExitHash)
            $self->ivAdd('menuToolItemHash', 'identify_exit', $item_identifyExit);

            $subMenu_findExits->append(Gtk2::SeparatorMenuItem->new());    # Separator

            my $item_selectUnallocated = Gtk2::MenuItem->new('Select _unallocated exits');
            $item_selectUnallocated->signal_connect('activate' => sub {

                $self->selectExitTypeCallback('unallocated');
            });
            $subMenu_findExits->append($item_selectUnallocated);

            my $item_selectUnallocatable = Gtk2::MenuItem->new('Select una_llocatable exits');
            $item_selectUnallocatable->signal_connect('activate' => sub {

                $self->selectExitTypeCallback('unallocatable');
            });
            $subMenu_findExits->append($item_selectUnallocatable);

            my $item_selectUncertain = Gtk2::MenuItem->new('Select un_certain exits');
            $item_selectUncertain->signal_connect('activate' => sub {

                $self->selectExitTypeCallback('uncertain');
            });
            $subMenu_findExits->append($item_selectUncertain);

            my $item_selectIncomplete = Gtk2::MenuItem->new('Select i_ncomplete exits');
            $item_selectIncomplete->signal_connect('activate' => sub {

                $self->selectExitTypeCallback('incomplete');
            });
            $subMenu_findExits->append($item_selectIncomplete);

            $subMenu_findExits->append(Gtk2::SeparatorMenuItem->new());    # Separator

            my $item_selectAllAbove = Gtk2::MenuItem->new('Select _all the above');
            $item_selectAllAbove->signal_connect('activate' => sub {

                $self->selectExitTypeCallback('all_above');
            });
            $subMenu_findExits->append($item_selectAllAbove);

            $subMenu_findExits->append(Gtk2::SeparatorMenuItem->new());    # Separator

            my $item_selectRegion = Gtk2::MenuItem->new('Select _region exits');
            $item_selectRegion->signal_connect('activate' => sub {

                $self->selectExitTypeCallback('region');
            });
            $subMenu_findExits->append($item_selectRegion);

            my $item_selectSuper = Gtk2::MenuItem->new('Select _super-region exits');
            $item_selectSuper->signal_connect('activate' => sub {

                $self->selectExitTypeCallback('super');
            });
            $subMenu_findExits->append($item_selectSuper);

        my $item_findExits = Gtk2::MenuItem->new('_Find exits');
        $item_findExits->set_submenu($subMenu_findExits);
        $column_exits->append($item_findExits);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'find_exits', $item_findExits);

        my $item_editExit = Gtk2::ImageMenuItem->new('_Edit exit...');
        my $img_editExit = Gtk2::Image->new_from_stock('gtk-edit', 'menu');
        $item_editExit->set_image($img_editExit);
        $item_editExit->signal_connect('activate' => sub {

            $self->editExitCallback();
        });
        $column_exits->append($item_editExit);
        # (Requires $self->currentRegionmap and $self->selectedExit)
        $self->ivAdd('menuToolItemHash', 'edit_exit', $item_editExit);

        $column_exits->append(Gtk2::SeparatorMenuItem->new());  # Separator

            # 'Uncertain exits' submenu
            my $subMenu_uncertainExits = Gtk2::Menu->new();

            my $item_completeSelected = Gtk2::MenuItem->new('Complete selected _uncertain exits');
            $item_completeSelected->signal_connect('activate' => sub {

                $self->completeExitsCallback();
            });
            $subMenu_uncertainExits->append($item_completeSelected);

            my $item_connectAdjacent = Gtk2::MenuItem->new('Connect selected _adjacent rooms');
            $item_connectAdjacent->signal_connect('activate' => sub {

                $self->connectAdjacentCallback();
            });
            $subMenu_uncertainExits->append($item_connectAdjacent);
            # (Requires $self->currentRegionmap and one or more selected rooms)
            $self->ivAdd('menuToolItemHash', 'connect_adjacent', $item_connectAdjacent);

            $subMenu_uncertainExits->append(Gtk2::SeparatorMenuItem->new());    # Separator

            my $item_autocomplete = Gtk2::CheckMenuItem->new('A_utocomplete uncertain exits');
            $item_autocomplete->set_active($self->worldModelObj->autocompleteExitsFlag);
            $item_autocomplete->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'autocompleteExitsFlag',
                        $item_autocomplete->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'autcomplete_uncertain',
                    );
                }
            });
            $subMenu_uncertainExits->append($item_autocomplete);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'autocomplete_uncertain', $item_autocomplete);

            my $item_intUncertain = Gtk2::CheckMenuItem->new('_Intelligent uncertain exits');
            $item_intUncertain->set_active(
                $self->worldModelObj->intelligentExitsFlag,
            );
            $item_intUncertain->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'intelligentExitsFlag',
                        $item_intUncertain->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'intelligent_uncertain',
                    );
                }
            });
            $subMenu_uncertainExits->append($item_intUncertain);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'intelligent_uncertain', $item_intUncertain);

        my $item_uncertainExits = Gtk2::MenuItem->new('_Uncertain exits');
        $item_uncertainExits->set_submenu($subMenu_uncertainExits);
        $column_exits->append($item_uncertainExits);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'uncertain_exits', $item_uncertainExits);

            # 'Exit lengths' submenu
            my $subMenu_exitLengths = Gtk2::Menu->new();

            my $item_horizontalLength = Gtk2::MenuItem->new('Set _horizontal length...');
            $item_horizontalLength->signal_connect('activate' => sub {

                $self->setExitLengthCallback('horizontal');
            });
            $subMenu_exitLengths->append($item_horizontalLength);

            my $item_verticalLength = Gtk2::MenuItem->new('Set _vertical length...');
            $item_verticalLength->signal_connect('activate' => sub {

                $self->setExitLengthCallback('vertical');
            });
            $subMenu_exitLengths->append($item_verticalLength);

            $subMenu_exitLengths->append(Gtk2::SeparatorMenuItem->new());    # Separator

            my $item_resetLength = Gtk2::MenuItem->new('_Reset exit lengths');
            $item_resetLength->signal_connect('activate' => sub {

                $self->resetExitLengthCallback();
            });
            $subMenu_exitLengths->append($item_resetLength);

        my $item_exitLengths = Gtk2::MenuItem->new('Exit _lengths...');
        $item_exitLengths->set_submenu($subMenu_exitLengths);
        $column_exits->append($item_exitLengths);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'exit_lengths', $item_exitLengths);

        $column_exits->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_deleteExit = Gtk2::ImageMenuItem->new('_Delete exit');
        my $img_deleteExit = Gtk2::Image->new_from_stock('gtk-delete', 'menu');
        $item_deleteExit->set_image($img_deleteExit);
        $item_deleteExit->signal_connect('activate' => sub {

            $self->deleteExitCallback();
        });
        $column_exits->append($item_deleteExit);
        # (Requires $self->currentRegionmap and $self->selectedExit)
        $self->ivAdd('menuToolItemHash', 'delete_exit', $item_deleteExit);

        # Setup complete
        return $column_exits;
    }

    sub enableLabelsColumn {

        # Called by $self->enableMenu
        # Sets up the 'Labels' column of the Automapper window's menu bar
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableLabelsColumn', @_);
        }

        # Set up column
        my $column_labels = Gtk2::Menu->new();
        if (! $column_labels) {

            return undef;
        }

        my $item_addLabelAtClick = Gtk2::ImageMenuItem->new('Add label at _click');
        my $img_addLabelAtClick = Gtk2::Image->new_from_stock('gtk-add', 'menu');
        $item_addLabelAtClick->set_image($img_addLabelAtClick);
        $item_addLabelAtClick->signal_connect('activate' => sub {

            # Set the free click mode; $self->canvasEventHandler will create the new label when the
            #   user next clicks on an empty part of the map
            if ($self->currentRegionmap) {

                $self->ivPoke('freeClickMode', 'add_label');
            }
        });
        $column_labels->append($item_addLabelAtClick);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'add_label_at_click', $item_addLabelAtClick);

        my $item_addLabelAtBlock = Gtk2::ImageMenuItem->new('Add label at _block');
        my $img_addLabelAtBlock = Gtk2::Image->new_from_stock('gtk-add', 'menu');
        $item_addLabelAtBlock->set_image($img_addLabelAtBlock);
        $item_addLabelAtBlock->signal_connect('activate' => sub {

            $self->addLabelAtBlockCallback();
        });
        $column_labels->append($item_addLabelAtBlock);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'add_label_at_block', $item_addLabelAtBlock);

        $column_labels->append(Gtk2::SeparatorMenuItem->new()); # Separator

        my $item_editLabel = Gtk2::MenuItem->new('_Edit label...');
        $item_editLabel->signal_connect('activate' => sub {

            $self->editLabelCallback();
        });
        $column_labels->append($item_editLabel);
        # (Requires $self->currentRegionmap and $self->selectedLabel)
        $self->ivAdd('menuToolItemHash', 'edit_label', $item_editLabel);

            # 'Set size' submenu
            my $subMenu_labelSize = Gtk2::Menu->new();

            my $item_setLabelNormal = Gtk2::MenuItem->new('Set _normal size');
            $item_setLabelNormal->signal_connect('activate' => sub {

                $self->worldModelObj->setLabelSize(TRUE, $self->selectedLabel, 1);
            });
            $subMenu_labelSize->append($item_setLabelNormal);

            my $item_setLabelLarge = Gtk2::MenuItem->new('Set _large size');
            $item_setLabelLarge->signal_connect('activate' => sub {

                $self->worldModelObj->setLabelSize(TRUE, $self->selectedLabel, 2);
            });
            $subMenu_labelSize->append($item_setLabelLarge);

            my $item_setLabelHuge = Gtk2::MenuItem->new('Set _huge size');
            $item_setLabelHuge->signal_connect('activate' => sub {

                $self->worldModelObj->setLabelSize(TRUE, $self->selectedLabel, 4);
            });
            $subMenu_labelSize->append($item_setLabelHuge);

        my $item_labelSize = Gtk2::MenuItem->new('Set label si_ze');
        $item_labelSize->set_submenu($subMenu_labelSize);
        $column_labels->append($item_labelSize);
        # (Requires $self->currentRegionmap and $self->selectedLabel)
        $self->ivAdd('menuToolItemHash', 'set_label_size', $item_labelSize);

        $column_labels->append(Gtk2::SeparatorMenuItem->new()); # Separator

        my $item_selectLabel = Gtk2::MenuItem->new('_Select label...');
        $item_selectLabel->signal_connect('activate' => sub {

            $self->selectLabelCallback();
        });
        $column_labels->append($item_selectLabel);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'select_label', $item_selectLabel);

        $column_labels->append(Gtk2::SeparatorMenuItem->new()); # Separator

        my $item_deleteLabel = Gtk2::ImageMenuItem->new('_Delete labels...');
        my $img_deleteLabel = Gtk2::Image->new_from_stock('gtk-delete', 'menu');
        $item_deleteLabel->set_image($img_deleteLabel);
        $item_deleteLabel->signal_connect('activate' => sub {

            $self->worldModelObj->deleteLabels(
                TRUE,           # Update Automapper windows now
                $self->compileSelectedLabels(),
            );
        });
        $column_labels->append($item_deleteLabel);
        # (Requires $self->currentRegionmap & either $self->selectedLabel or
        #   $self->selectedLabelHash)
        $self->ivAdd('menuToolItemHash', 'delete_label', $item_deleteLabel);

        # Setup complete
        return $column_labels;
    }

    # Popup menu widget methods

    sub enableCanvasPopupMenu {

        # Called by $self->canvasObjEventHandler
        # Creates a popup-menu for the Gtk2::Canvas when no rooms, exits, room tags or labels are
        #   selected
        #
        # Expected arguments
        #   $clickXPosPixels, $clickYPosPixels
        #       - Coordinates of the pixel that was right-clicked on the map
        #   $clickXPosBlocks, $clickYPosBlocks
        #       - Coordinates of the gridblock that was right-clicked on the map
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my (
            $self, $clickXPosPixels, $clickYPosPixels, $clickXPosBlocks, $clickYPosBlocks, $check,
        ) = @_;

        # Check for improper arguments
        if (
            ! defined $clickXPosPixels || ! defined $clickYPosPixels
            || ! defined $clickXPosBlocks || ! defined $clickYPosBlocks || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableCanvasPopupMenu', @_);
        }

        # Set up the popup menu
        my $menu_canvas = Gtk2::Menu->new();
        if (! $menu_canvas) {

            return undef;
        }

        # (Everything here assumes $self->currentRegionmap)

        my $item_addFirstRoom = Gtk2::ImageMenuItem->new('Add _first room');
        my $img_addFirstRoom = Gtk2::Image->new_from_stock('gtk-add', 'menu');
        $item_addFirstRoom->set_image($img_addFirstRoom);
        $item_addFirstRoom->signal_connect('activate' => sub {

            $self->addFirstRoomCallback();
        });
        $menu_canvas->append($item_addFirstRoom);
        # (Also requires empty $self->currentRegionmap->gridRoomHash)
        if ($self->currentRegionmap->gridRoomHash) {

            $item_addFirstRoom->set_sensitive(FALSE);
        }

        my $item_addRoomHere = Gtk2::ImageMenuItem->new('Add room _here');
        my $img_addRoomHere = Gtk2::Image->new_from_stock('gtk-add', 'menu');
        $item_addRoomHere->set_image($img_addRoomHere);
        $item_addRoomHere->signal_connect('activate' => sub {

            my $roomObj;

            # The 'Add room at click' operation from the main menu resets the value of
            #   ->freeClickMode; we must do the same here
            $self->ivPoke('freeClickMode', 'default');

            # Create the room
            $roomObj = $self->createNewRoom(
                $self->currentRegionmap,
                $clickXPosBlocks,
                $clickYPosBlocks,
                $self->currentRegionmap->currentLevel,
            );

            # When using the 'Add room at block' menu item, the new room is selected to make it
            #   easier to see where it was drawn. To make things consistent, select this new room,
            #   too
            if ($roomObj) {

                $self->setSelectedObj(
                    [$roomObj, 'room'],
                    FALSE,      # Select this object; unselect all other objects
                );
            }
        });
        $menu_canvas->append($item_addRoomHere);

        $menu_canvas->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_addLabelHere = Gtk2::ImageMenuItem->new('Add _label here');
        my $img_addLabelHere = Gtk2::Image->new_from_stock('gtk-add', 'menu');
        $item_addLabelHere->set_image($img_addLabelHere);
        $item_addLabelHere->signal_connect('activate' => sub {

            $self->addLabelAtClickCallback($clickXPosPixels, $clickYPosPixels);
        });
        $menu_canvas->append($item_addLabelHere);

        $menu_canvas->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_centreMap = Gtk2::MenuItem->new('_Centre map here');
        $item_centreMap->signal_connect('activate' => sub {

            $self->centreMapOverRoom(
                undef,              # Centre the map, not over a room...
                $clickXPosBlocks,   # ...but over this gridblock
                $clickYPosBlocks,
            );
        });
        $menu_canvas->append($item_centreMap);

        $menu_canvas->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_editRegionmap = Gtk2::ImageMenuItem->new('Add _label here');
        my $img_editRegionmap = Gtk2::Image->new_from_stock('gtk-edit', 'menu');
        $item_editRegionmap->set_image($img_editRegionmap);
        $item_editRegionmap->signal_connect('activate' => sub {

            # Open an 'edit' window for the regionmap
            $self->createFreeWin(
                'Games::Axmud::EditWin::Regionmap',
                $self,
                $self->session,
                'Edit \'' . $self->currentRegionmap->name . '\' regionmap',
                $self->currentRegionmap,
                FALSE,                          # Not temporary
            );
        });
        $menu_canvas->append($item_editRegionmap);

        my $item_preferences = Gtk2::ImageMenuItem->new('Edit _world model...');
        my $img_preferences = Gtk2::Image->new_from_stock('gtk-preferences', 'menu');
        $item_preferences->set_image($img_preferences);
        $item_preferences->signal_connect('activate' => sub {

            # Open an 'edit' window for the world model
            $self->createFreeWin(
                'Games::Axmud::EditWin::WorldModel',
                $self,
                $self->session,
                'Edit world model',
                $self->session->worldModelObj,
                FALSE,                          # Not temporary
            );
        });
        $menu_canvas->append($item_preferences);

        # Setup complete
        $menu_canvas->show_all();

        return $menu_canvas;
    }

    sub enableRoomsPopupMenu {

        # Called by $self->canvasObjEventHandler
        # Creates a popup-menu for the selected room
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableRoomsPopupMenu', @_);
        }

        # Set up the popup menu
        my $menu_rooms = Gtk2::Menu->new();
        if (! $menu_rooms) {

            return undef;
        }

        # (Everything here assumes $self->currentRegionmap and $self->selectedRoom)

        my $item_setCurrentRoom = Gtk2::MenuItem->new('_Set current room');
        $item_setCurrentRoom->signal_connect('activate' => sub {

            $self->mapObj->setCurrentRoom($self->selectedRoom);
        });
        $menu_rooms->append($item_setCurrentRoom);

        my $item_goToRoom = Gtk2::MenuItem->new('_Go to room');
        $item_goToRoom->signal_connect('activate' => sub {

            $self->processPathCallback('send_char');
        });
        $menu_rooms->append($item_goToRoom);
        # (Also requires $self->mapObj->currentRoom)
        if (! $self->mapObj->currentRoom) {

            $item_goToRoom->set_sensitive(FALSE);
        }

        my $item_centreMap = Gtk2::MenuItem->new('Centre _map over room');
        $item_centreMap->signal_connect('activate' => sub {

            $self->centreMapOverRoom($self->selectedRoom);
        });
        $menu_rooms->append($item_centreMap);

        $menu_rooms->append(Gtk2::SeparatorMenuItem->new());  # Separator

             # 'Add exit' submenu
            my $subMenu_addExit = Gtk2::Menu->new();

            my $item_addExit = Gtk2::MenuItem->new('Add _normal exit...');
            $item_addExit->signal_connect('activate' => sub {

                $self->addExitCallback(FALSE);      # FALSE - not a hidden exit
            });
            $subMenu_addExit->append($item_addExit);

            my $item_addMultiple = Gtk2::MenuItem->new('Add _multiple exits...');
            $item_addMultiple->signal_connect('activate' => sub {

                $self->addMultipleExitsCallback();
            });
            $subMenu_addExit->append($item_addMultiple);

            $subMenu_addExit->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_addHiddenExit = Gtk2::MenuItem->new('Add _hidden exit...');
            $item_addHiddenExit->signal_connect('activate' => sub {

                $self->addExitCallback(TRUE);       # TRUE - a hidden exit
            });
            $subMenu_addExit->append($item_addHiddenExit);

        my $item_exits = Gtk2::ImageMenuItem->new('Add e_xit');
        my $img_exits = Gtk2::Image->new_from_stock('gtk-add', 'menu');
        $item_exits->set_image($img_exits);
        $item_exits->set_submenu($subMenu_addExit);
        $menu_rooms->append($item_exits);

            # 'Add patterns' submenu
            my $subMenu_exitPatterns = Gtk2::Menu->new();

            my $item_addFailedExitRoom = Gtk2::MenuItem->new('Add _failed exit...');
            $item_addFailedExitRoom->signal_connect('activate' => sub {

                $self->addFailedExitCallback(FALSE, $self->selectedRoom);
            });
            $subMenu_exitPatterns->append($item_addFailedExitRoom);

            my $item_addInvoluntaryExitRoom = Gtk2::MenuItem->new('Add _involuntary exit...');
            $item_addInvoluntaryExitRoom->signal_connect('activate' => sub {

                $self->addInvoluntaryExitCallback($self->selectedRoom);
            });
            $subMenu_exitPatterns->append($item_addInvoluntaryExitRoom);

            my $item_addRepulseExitRoom = Gtk2::MenuItem->new('Add r_epulse exit...');
            $item_addRepulseExitRoom->signal_connect('activate' => sub {

                $self->addRepulseExitCallback($self->selectedRoom);
            });
            $subMenu_exitPatterns->append($item_addRepulseExitRoom);

            my $item_addSpecialDepartRoom = Gtk2::MenuItem->new('Add _special departure...');
            $item_addSpecialDepartRoom->signal_connect('activate' => sub {

                $self->addSpecialDepartureCallback($self->selectedRoom);
            });
            $subMenu_exitPatterns->append($item_addSpecialDepartRoom);

        my $item_patterns = Gtk2::MenuItem->new('Add patter_n');
        $item_patterns->set_submenu($subMenu_exitPatterns);
        $menu_rooms->append($item_patterns);

            # 'Add to model' submenu
            my $subMenu_addToModel = Gtk2::Menu->new();

            my $item_addRoomContents = Gtk2::MenuItem->new('Add _contents...');
            $item_addRoomContents->signal_connect('activate' => sub {

                $self->addContentsCallback(FALSE);
            });
            $subMenu_addToModel->append($item_addRoomContents);
            # (Also requires $self->mapObj->currentRoom that's the same as $self->selectedRoom
            if (! $self->mapObj->currentRoom || $self->mapObj->currentRoom ne $self->selectedRoom) {

                $item_addRoomContents->set_sensitive(FALSE);
            }

            my $item_addContentsString = Gtk2::MenuItem->new('Add contents from _string...');
            $item_addContentsString->signal_connect('activate' => sub {

                $self->addContentsCallback(TRUE);
            });
            $subMenu_addToModel->append($item_addContentsString);

            $subMenu_addToModel->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_addHiddenObj = Gtk2::MenuItem->new('Add _hidden object...');
            $item_addHiddenObj->signal_connect('activate' => sub {

                $self->addHiddenObjCallback(FALSE);
            });
            $subMenu_addToModel->append($item_addHiddenObj);
            # (Also requires $self->mapObj->currentRoom that's the same as $self->selectedRoom
            if (! $self->mapObj->currentRoom || $self->mapObj->currentRoom ne $self->selectedRoom) {

                $item_addHiddenObj->set_sensitive(FALSE);
            }

            my $item_addHiddenString = Gtk2::MenuItem->new('Add hidden object _from string...');
            $item_addHiddenString->signal_connect('activate' => sub {

                $self->addHiddenObjCallback(TRUE);
            });
            $subMenu_addToModel->append($item_addHiddenString);

            $subMenu_addToModel->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_addSearchResult = Gtk2::MenuItem->new('Add search _result...');
            $item_addSearchResult->signal_connect('activate' => sub {

                $self->addSearchResultCallback();
            });
            $subMenu_addToModel->append($item_addSearchResult);
            # (Also requires $self->mapObj->currentRoom that's the same as $self->selectedRoom)
            if (! $self->mapObj->currentRoom || $self->mapObj->currentRoom ne $self->selectedRoom) {

                $item_addSearchResult->set_sensitive(FALSE);
            }

        my $item_addToModel = Gtk2::MenuItem->new('_Add to model');
        $item_addToModel->set_submenu($subMenu_addToModel);
        $menu_rooms->append($item_addToModel);

        $menu_rooms->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_selectExit = Gtk2::MenuItem->new('Se_lect exit...');
        $item_selectExit->signal_connect('activate' => sub {

            $self->selectExitCallback();
        });
        $menu_rooms->append($item_selectExit);

        my $item_executeScripts = Gtk2::MenuItem->new('Run A_Basic scripts');
        $item_executeScripts->signal_connect('activate' => sub {

            $self->executeScriptsCallback();
        });
        $menu_rooms->append($item_executeScripts);
        # (Also requires $self->mapObj->currentRoom that's the same as $self->selectedRoom)
        if (! $self->mapObj->currentRoom || $self->mapObj->currentRoom ne $self->selectedRoom) {

            $item_executeScripts->set_sensitive(FALSE);
        }

        $menu_rooms->append(Gtk2::SeparatorMenuItem->new());  # Separator

            # 'Source code' submenu
            my $subMenu_updateVisits = Gtk2::Menu->new();

            my $item_increaseVisits = Gtk2::MenuItem->new('_Increase by one');
            $item_increaseVisits->signal_connect('activate' => sub {

                $self->updateVisitsCallback('increase');
            });
            $subMenu_updateVisits->append($item_increaseVisits);

            my $item_decreaseVisits = Gtk2::MenuItem->new('_Decrease by one');
            $item_decreaseVisits->signal_connect('activate' => sub {

                $self->updateVisitsCallback('decrease');
            });
            $subMenu_updateVisits->append($item_decreaseVisits);

            my $item_manualVisits = Gtk2::MenuItem->new('Set _manually');
            $item_manualVisits->signal_connect('activate' => sub {

                $self->updateVisitsCallback('manual');
            });
            $subMenu_updateVisits->append($item_manualVisits);

            $subMenu_updateVisits->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_increaseAndCurrent = Gtk2::MenuItem->new('_Increase & set current');
            $item_increaseAndCurrent->signal_connect('activate' => sub {

                $self->updateVisitsCallback('increase');
                $self->mapObj->setCurrentRoom($self->selectedRoom);
            });
            $subMenu_updateVisits->append($item_increaseAndCurrent);

            $subMenu_updateVisits->append(Gtk2::SeparatorMenuItem->new()); # Separator

            my $item_resetVisits = Gtk2::MenuItem->new('_Reset to zero');
            $item_resetVisits->signal_connect('activate' => sub {

                $self->updateVisitsCallback('reset');
            });
            $subMenu_updateVisits->append($item_resetVisits);

        my $item_updateVisits = Gtk2::MenuItem->new('Update character visits');
        $item_updateVisits->set_submenu($subMenu_updateVisits);
        $menu_rooms->append($item_updateVisits);

        my $item_editRoom = Gtk2::ImageMenuItem->new('_Edit room...');
        my $img_editRoom = Gtk2::Image->new_from_stock('gtk-edit', 'menu');
        $item_editRoom->set_image($img_editRoom);
        $item_editRoom->signal_connect('activate' => sub {

            if ($self->selectedRoom) {

                # Open the room's 'edit' window
                $self->createFreeWin(
                    'Games::Axmud::EditWin::ModelObj::Room',
                    $self,
                    $self->session,
                    'Edit ' . $self->selectedRoom->category . ' model object',
                    $self->selectedRoom,
                    FALSE,                          # Not temporary
                );
            }
        });
        $menu_rooms->append($item_editRoom);

            # 'Source code' submenu
            my $subMenu_sourceCode = Gtk2::Menu->new();

            my $item_setFilePath = Gtk2::MenuItem->new('_Set file path...');
            $item_setFilePath->signal_connect('activate' => sub {

                $self->setFilePathCallback();
            });
            $subMenu_sourceCode->append($item_setFilePath);

            my $item_setVirtualArea = Gtk2::MenuItem->new('Set virtual _area...');
            $item_setVirtualArea->signal_connect('activate' => sub {

                $self->setVirtualAreaCallback(TRUE);
            });
            $subMenu_sourceCode->append($item_setVirtualArea);

            my $item_resetVirtualArea = Gtk2::MenuItem->new('_Reset virtual area...');
            $item_resetVirtualArea->signal_connect('activate' => sub {

                $self->setVirtualAreaCallback(FALSE);
            });
            $subMenu_sourceCode->append($item_resetVirtualArea);

            $subMenu_sourceCode->append(Gtk2::SeparatorMenuItem->new());  # Separator

            my $item_viewSource = Gtk2::MenuItem->new('_View source file...');
            $item_viewSource->signal_connect('activate' => sub {

                my $flag;

                if ($self->selectedRoom) {

                    if (! $self->selectedRoom->virtualAreaPath) {
                        $flag = FALSE;
                    } else {
                        $flag = TRUE;
                    }

                    # Show source code file
                    $self->quickFreeWin(
                        'Games::Axmud::OtherWin::SourceCode',
                        $self->session,
                        # Config
                        'model_obj' => $self->selectedRoom,
                        'virtual_flag' => $flag,
                    );
                }
            });
            $subMenu_sourceCode->append($item_viewSource);
            # (Also requires either $self->selectedRoom->sourceCodePath or
            #   $self->selectedRoom->virtualAreaPath)
            if (! $self->selectedRoom->sourceCodePath && ! $self->selectedRoom->virtualAreaPath) {

                $item_viewSource->set_sensitive(FALSE);
            }

            my $item_editSource = Gtk2::MenuItem->new('Edit so_urce file...');
            $item_editSource->signal_connect('activate' => sub {

                if ($self->selectedRoom) {

                    if (! $self->selectedRoom->virtualAreaPath) {

                        # Edit source code file
                        $self->editFileCallback();

                    } else {

                        # Edit virtual area file
                        $self->editFileCallback(TRUE);
                    }
                }
            });
            $subMenu_sourceCode->append($item_editSource);
            # (Also requires either $self->selectedRoom->sourceCodePath or
            #   $self->selectedRoom->virtualAreaPath)
            if (! $self->selectedRoom->sourceCodePath && ! $self->selectedRoom->virtualAreaPath) {

                $item_editSource->set_sensitive(FALSE);
            }

        my $item_sourceCode = Gtk2::MenuItem->new('Source _code');
        $item_sourceCode->set_submenu($subMenu_sourceCode);
        $menu_rooms->append($item_sourceCode);

        $menu_rooms->append(Gtk2::SeparatorMenuItem->new());  # Separator

            # 'Toggle room flag' submenu
            my $subMenu_toggleRoomFlag = Gtk2::Menu->new();

            foreach my $filter ($self->worldModelObj->roomFilterList) {

                my $flagListRef;

                # A sub-sub menu for $filter
                my $subMenu = Gtk2::Menu->new();

                $flagListRef = $self->worldModelObj->ivShow('roomFlagReverseHash', $filter);
                foreach my $flag (@$flagListRef) {

                    my $menuItem = Gtk2::MenuItem->new(
                        $self->worldModelObj->ivShow('roomFlagDescripHash', $flag),
                    );
                    $menuItem->signal_connect('activate' => sub {

                        # Toggle the flags for all selected rooms, redraw them and (if the flag is
                        #   one of the hazardous room flags) recalculate the regionmap's paths. The
                        #   TRUE argument tells the world model to redraw the rooms
                        $self->worldModelObj->toggleRoomFlags(
                            $self->session,
                            TRUE,
                            $flag,
                            $self->compileSelectedRooms(),
                        );
                    });
                    $subMenu->append($menuItem);
                }

                my $menuItem = Gtk2::MenuItem->new(ucfirst($filter));
                $menuItem->set_submenu($subMenu);
                $subMenu_toggleRoomFlag->append($menuItem);
            }

        my $item_toggleRoomFlag = Gtk2::MenuItem->new('_Toggle room flags');
        $item_toggleRoomFlag->set_submenu($subMenu_toggleRoomFlag);
        $menu_rooms->append($item_toggleRoomFlag);

            # 'Set room text' submenu
            my $subMenu_setRoomText = Gtk2::Menu->new();

            my $item_setRoomTag = Gtk2::MenuItem->new('Set room _tag...');
            $item_setRoomTag->signal_connect('activate' => sub {

                $self->setRoomTagCallback();
            });
            $subMenu_setRoomText->append($item_setRoomTag);

            my $item_setGuild = Gtk2::MenuItem->new('Set roo_m guild...');
            $item_setGuild->signal_connect('activate' => sub {

                $self->setRoomGuildCallback();
            });
            $subMenu_setRoomText->append($item_setGuild);

            $subMenu_setRoomText->append(Gtk2::SeparatorMenuItem->new());  # Separator

            my $item_resetPositions = Gtk2::MenuItem->new('Reset text posit_ions');
            $item_resetPositions->signal_connect('activate' => sub {

                $self->resetRoomOffsetsCallback();
            });
            $subMenu_setRoomText->append($item_resetPositions);

        my $item_setRoomText = Gtk2::MenuItem->new('Set r_oom text');
        $item_setRoomText->set_submenu($subMenu_setRoomText);
        $menu_rooms->append($item_setRoomText);

            # 'Room exclusivity' submenu
            my $subMenu_exclusivity = Gtk2::Menu->new();

            my $item_toggleExclusivity = Gtk2::MenuItem->new('_Toggle exclusivity');
            $item_toggleExclusivity->signal_connect('activate' => sub {

                $self->toggleExclusiveProfileCallback();
            });
            $subMenu_exclusivity->append($item_toggleExclusivity);

            my $item_addExclusiveProf = Gtk2::MenuItem->new('_Add exclusive profile...');
            $item_addExclusiveProf->signal_connect('activate' => sub {

                $self->addExclusiveProfileCallback();
            });
            $subMenu_exclusivity->append($item_addExclusiveProf);

            my $item_clearExclusiveProf = Gtk2::MenuItem->new('_Clear exclusive profiles');
            $item_clearExclusiveProf->signal_connect('activate' => sub {

                $self->resetExclusiveProfileCallback();
            });
            $subMenu_exclusivity->append($item_clearExclusiveProf);

        my $item_exclusivity = Gtk2::MenuItem->new('_Room exclusivity');
        $item_exclusivity->set_submenu($subMenu_exclusivity);
        $menu_rooms->append($item_exclusivity);

        $menu_rooms->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_deleteRoom = Gtk2::ImageMenuItem->new('_Delete room');
        my $img_deleteRoom = Gtk2::Image->new_from_stock('gtk-delete', 'menu');
        $item_deleteRoom->set_image($img_deleteRoom);
        $item_deleteRoom->signal_connect('activate' => sub {

            if ($self->selectedRoom) {

                $self->worldModelObj->deleteRooms(
                    $self->session,
                    TRUE,           # Update Automapper windows now
                    $self->selectedRoom,
                );
            }
        });
        $menu_rooms->append($item_deleteRoom);

        # Setup complete
        $menu_rooms->show_all();

        return $menu_rooms;
    }

    sub enableRoomTagsPopupMenu {

        # Called by $self->canvasObjEventHandler
        # Creates a popup-menu for the selected room tag
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->enableRoomTagsPopupMenu',
                @_,
            );
        }

        # Set up the popup menu
        my $menu_tags = Gtk2::Menu->new();
        if (! $menu_tags) {

            return undef;
        }

        # (Everything here assumes $self->currentRegionmap and $self->selectedRoomTag)

        my $item_editTag = Gtk2::MenuItem->new('_Set room tag...');
        $item_editTag->signal_connect('activate' => sub {

            $self->setRoomTagCallback();
        });
        $menu_tags->append($item_editTag);

        my $item_resetPosition = Gtk2::MenuItem->new('_Reset position');
        $item_resetPosition->signal_connect('activate' => sub {

            if ($self->selectedRoomTag) {

                $self->worldModelObj->resetRoomOffsets(
                    TRUE,                       # Update Automapper windows now
                    1,                          # Mode 1 - reset room tag only
                    $self->selectedRoomTag,     # Set to the parent room's blessed reference
                );
            }
        });
        $menu_tags->append($item_resetPosition);

        # Setup complete
        $menu_tags->show_all();

        return $menu_tags;
    }

    sub enableRoomGuildsPopupMenu {

        # Called by $self->canvasObjEventHandler
        # Creates a popup-menu for the selected room guild
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->enableRoomGuildsPopupMenu',
                @_,
            );
        }

        # Set up the popup menu
        my $menu_guilds = Gtk2::Menu->new();
        if (! $menu_guilds) {

            return undef;
        }

        # (Everything here assumes $self->currentRegionmap and $self->selectedRoomGuild)

        my $item_editGuild = Gtk2::MenuItem->new('_Set room guild...');
        $item_editGuild->signal_connect('activate' => sub {

            $self->setRoomGuildCallback();
        });
        $menu_guilds->append($item_editGuild);

        my $item_resetPosition = Gtk2::MenuItem->new('_Reset position');
        $item_resetPosition->signal_connect('activate' => sub {

            if ($self->selectedRoomGuild) {

                $self->worldModelObj->resetRoomOffsets(
                    TRUE,                       # Update Automapper windows now
                    2,                          # Mode 2 - reset room guild only
                    $self->selectedRoomGuild,   # Set to the parent room's blessed reference
                );
            }
        });
        $menu_guilds->append($item_resetPosition);

        # Setup complete
        $menu_guilds->show_all();

        return $menu_guilds;
    }

    sub enableExitsPopupMenu {

        # Called by $self->canvasObjEventHandler
        # Creates a popup-menu for the selected exit
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Local variables
        my @titleList;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableExitsPopupMenu', @_);
        }

        # Set up the popup menu
        my $menu_exits = Gtk2::Menu->new();
        if (! $menu_exits) {

            return undef;
        }

        # (Everything here assumes $self->currentRegionmap and $self->selectedExit)

        my $item_changeDirection = Gtk2::MenuItem->new('C_hange direction...');
        $item_changeDirection->signal_connect('activate' => sub {

            $self->changeDirCallback();
        });
        $menu_exits->append($item_changeDirection);
        # (Also requires $self->selectedExit->drawMode is 'primary' or 'perm_alloc'
        if (
            $self->selectedExit->drawMode ne 'primary'
            && $self->selectedExit->drawMode ne 'perm_alloc'
        ) {
            $item_changeDirection->set_sensitive(FALSE);
        }

        my $item_setAssisted = Gtk2::MenuItem->new('Set assisted _move...');
        $item_setAssisted->signal_connect('activate' => sub {

            $self->setAssistedMoveCallback();
        });
        $menu_exits->append($item_setAssisted);
        # (Also requires $self->selectedExit->drawMode 'primary', 'temp_unalloc' or 'perm_unalloc')
        if ($self->selectedExit->drawMode eq 'temp_alloc') {

            $item_setAssisted->set_sensitive(FALSE);
        }

            # 'Allocate map direction' submenu
            my $subMenu_allocateMapDir = Gtk2::Menu->new();

            my $item_allocatePrimary = Gtk2::MenuItem->new('Choose _direction...');
            $item_allocatePrimary->signal_connect('activate' => sub {

                $self->allocateMapDirCallback();
            });
            $subMenu_allocateMapDir->append($item_allocatePrimary);


            my $item_confirmTwoWay = Gtk2::MenuItem->new('Confirm _two-way exit...');
            $item_confirmTwoWay->signal_connect('activate' => sub {

                $self->confirmTwoWayCallback();
            });
            $subMenu_allocateMapDir->append($item_confirmTwoWay);

        my $item_allocateMapDir = Gtk2::MenuItem->new('_Allocate map direction...');
        $item_allocateMapDir->set_submenu($subMenu_allocateMapDir);
        $menu_exits->append($item_allocateMapDir);
        # (Also requires $self->selectedExit->drawMode is 'temp_alloc' or 'temp_unalloc')
        if (
            $self->selectedExit->drawMode ne 'temp_alloc'
            && $self->selectedExit->drawMode ne 'temp_unalloc'
        ) {
            $item_allocateMapDir->set_sensitive(FALSE);
        }

        my $item_allocateShadow = Gtk2::MenuItem->new('Allocate _shadow...');
        $item_allocateShadow->signal_connect('activate' => sub {

            $self->allocateShadowCallback();
        });
        $menu_exits->append($item_allocateShadow);
        # (Also requires $self->selectedExit->drawMode is 'temp_alloc' or 'temp_unalloc')
        if (
            $self->selectedExit->drawMode ne 'temp_alloc'
            && $self->selectedExit->drawMode ne 'temp_unalloc'
        ) {
            $item_allocateShadow->set_sensitive(FALSE);
        }

        $menu_exits->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_connectExitToClick = Gtk2::MenuItem->new('_Connect to click');
        $item_connectExitToClick->signal_connect('activate' => sub {

            $self->connectToClickCallback();
        });
        $menu_exits->append($item_connectExitToClick);
        # (Also requires $self->selectedExit->drawMode 'primary', 'temp_unalloc' or 'perm_unalloc')
        if ($self->selectedExit->drawMode eq 'temp_alloc') {

            $item_connectExitToClick->set_sensitive(FALSE);
        }

        my $item_disconnectExit = Gtk2::MenuItem->new('D_isconnect exit');
        $item_disconnectExit->signal_connect('activate' => sub {

            $self->disconnectExitCallback();
        });
        $menu_exits->append($item_disconnectExit);

        $menu_exits->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_addExitBend = Gtk2::MenuItem->new('Add _bend');
        $item_addExitBend->signal_connect('activate' => sub {

            $self->addBendCallback();
        });
        $menu_exits->append($item_addExitBend);
        # (Also requires a $self->selectedExit that's a one-way or two-way broken exit, and also
        #   defined values for $self->exitClickXPosn and $self->exitClickYPosn)
        if (
            (! $self->selectedExit->oneWayFlag && ! $self->selectedExit->twinExit)
            || ! $self->selectedExit->brokenFlag
            || ! defined $self->exitClickXPosn
            || ! defined $self->exitClickYPosn
        ) {
            $item_addExitBend->set_sensitive(FALSE);
        }

        my $item_removeExitBend = Gtk2::MenuItem->new('_Remove bend');
        $item_removeExitBend->signal_connect('activate' => sub {

            $self->removeBendCallback();
        });
        $menu_exits->append($item_removeExitBend);
        # (Also requires a $self->selectedExit that's a one-way or two-way exit with a bend, and
        #   also defined values for $self->exitClickXPosn and $self->exitClickYPosn)
        if (
            (! $self->selectedExit->oneWayFlag && ! $self->selectedExit->twinExit)
            || ! $self->selectedExit->bendOffsetList
            || ! defined $self->exitClickXPosn
            || ! defined $self->exitClickYPosn
        ) {
            $item_removeExitBend->set_sensitive(FALSE);
        }

        $menu_exits->append(Gtk2::SeparatorMenuItem->new());  # Separator

            # 'Set ornaments' submenu
            my $subMenu_setOrnament = Gtk2::Menu->new();

            # Create a list of exit ornament types, in groups of two, in the form
            #   (menu_item_title, IV_to_be_set)
            @titleList = (
                '_No ornament', undef,
                '_Openable exit', 'openFlag',
                '_Lockable exit', 'lockFlag',
                '_Pickable exit', 'pickFlag',
                '_Breakable exit', 'breakFlag',
                '_Impassable exit', 'impassFlag',
            );

            do {

                my ($title, $iv);

                $title = shift @titleList;
                $iv = shift @titleList;

                my $menuItem = Gtk2::MenuItem->new($title);
                $menuItem->signal_connect('activate' => sub {

                    $self->exitOrnamentCallback($iv);
                });
                $subMenu_setOrnament->append($menuItem);

            } until (! @titleList);

            $subMenu_setOrnament->append(Gtk2::SeparatorMenuItem->new());   # Separator

            my $item_setTwinOrnament = Gtk2::CheckMenuItem->new('Also set _twin exits');
            $item_setTwinOrnament->set_active($self->worldModelObj->setTwinOrnamentFlag);
            $item_setTwinOrnament->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFlag(
                        'setTwinOrnamentFlag',
                        $item_setTwinOrnament->get_active(),
                        FALSE,      # Don't call $self->drawRegion
                        'also_set_twin_exits',
                    );
                }
            });
            $subMenu_setOrnament->append($item_setTwinOrnament);

        my $item_setOrnament = Gtk2::MenuItem->new('Set _ornaments');
        $item_setOrnament->set_submenu($subMenu_setOrnament);
        $menu_exits->append($item_setOrnament);

            # 'Set exit type' submenu
            my $subMenu_setExitType = Gtk2::Menu->new();

                # 'Set hidden' sub-submenu
                my $subMenu_setHidden = Gtk2::Menu->new();

                my $item_setHiddenExit = Gtk2::MenuItem->new('Mark exit _hidden');
                $item_setHiddenExit->signal_connect('activate' => sub {

                    $self->hiddenExitCallback(TRUE);
                });
                $subMenu_setHidden->append($item_setHiddenExit);

                my $item_setNotHiddenExit = Gtk2::MenuItem->new('Mark exit _not hidden');
                $item_setNotHiddenExit->signal_connect('activate' => sub {

                    $self->hiddenExitCallback(FALSE);
                });
                $subMenu_setHidden->append($item_setNotHiddenExit);

            my $item_setHidden = Gtk2::MenuItem->new('Set h_idden');
            $item_setHidden->set_submenu($subMenu_setHidden);
            $subMenu_setExitType->append($item_setHidden);

                # 'Set broken' sub-submenu
                my $subMenu_setBroken = Gtk2::Menu->new();

                my $item_markBrokenExit = Gtk2::MenuItem->new('_Mark exit as broken');
                $item_markBrokenExit->signal_connect('activate' => sub {

                    $self->markBrokenExitCallback();
                });
                $subMenu_setBroken->append($item_markBrokenExit);

                my $item_toggleBrokenExit = Gtk2::MenuItem->new('_Toggle bent broken exit');
                $item_toggleBrokenExit->signal_connect('activate' => sub {

                    $self->worldModelObj->toggleBentExit(
                        TRUE,                       # Update Automapper windows now
                        $self->selectedExit,
                    );
                });
                $subMenu_setBroken->append($item_toggleBrokenExit);
                # (Also requires $self->selectedExit->brokenFlag)
                if (! $self->selectedExit->brokenFlag) {

                    $item_toggleBrokenExit->set_sensitive(FALSE);
                }

                $subMenu_setBroken->append(Gtk2::SeparatorMenuItem->new());  # Separator

                my $item_restoreBrokenExit = Gtk2::MenuItem->new('_Restore unbroken exit');
                $item_restoreBrokenExit->signal_connect('activate' => sub {

                    $self->restoreBrokenExitCallback();
                });
                $subMenu_setBroken->append($item_restoreBrokenExit);

            my $item_setBroken = Gtk2::MenuItem->new('Set bro_ken');
            $item_setBroken->set_submenu($subMenu_setBroken);
            $subMenu_setExitType->append($item_setBroken);

                # 'Set one-way' sub-submenu
                my $subMenu_setOneWay = Gtk2::Menu->new();

                my $item_markOneWayExit = Gtk2::MenuItem->new('_Mark exit as one-way');
                $item_markOneWayExit->signal_connect('activate' => sub {

                    $self->markOneWayExitCallback();
                });
                $subMenu_setOneWay->append($item_markOneWayExit);

                $subMenu_setOneWay->append(Gtk2::SeparatorMenuItem->new());  # Separator

                my $item_restoreUncertainExit = Gtk2::MenuItem->new('Restore _uncertain exit');
                $item_restoreUncertainExit->signal_connect('activate' => sub {

                    $self->restoreOneWayExitCallback(FALSE);
                });
                $subMenu_setOneWay->append($item_restoreUncertainExit);

                my $item_restoreTwoWayExit = Gtk2::MenuItem->new('Restore _two-way exit');
                $item_restoreTwoWayExit->signal_connect('activate' => sub {

                    $self->restoreOneWayExitCallback(TRUE);
                });
                $subMenu_setOneWay->append($item_restoreTwoWayExit);

                $subMenu_setOneWay->append(Gtk2::SeparatorMenuItem->new());  # Separator

                my $item_setIncomingDir = Gtk2::MenuItem->new('Set incoming direction...');
                $item_setIncomingDir->signal_connect('activate' => sub {

                    $self->setIncomingDirCallback();
                });
                $subMenu_setOneWay->append($item_setIncomingDir);
                # (Also requires either a $self->selectedExit which is a one-way exit)
                if (! $self->selectedExit->oneWayFlag) {

                    $item_setIncomingDir->set_sensitive(FALSE);
                }

            my $item_setOneWay = Gtk2::MenuItem->new('Set one-_way');
            $item_setOneWay->set_submenu($subMenu_setOneWay);
            $subMenu_setExitType->append($item_setOneWay);

                # 'Set retracing' sub-submenu
                my $subMenu_setRetracing = Gtk2::Menu->new();

                my $item_markRetracingExit = Gtk2::MenuItem->new('_Mark exit as retracing');
                $item_markRetracingExit->signal_connect('activate' => sub {

                    $self->markRetracingExitCallback();
                });
                $subMenu_setRetracing->append($item_markRetracingExit);

                $subMenu_setRetracing->append(Gtk2::SeparatorMenuItem->new());    # Separator

                my $item_restoreRetracingExit = Gtk2::MenuItem->new('_Restore incomplete exit');
                $item_restoreRetracingExit->signal_connect('activate' => sub {

                    $self->restoreRetracingExitCallback();
                });
                $subMenu_setRetracing->append($item_restoreRetracingExit);

            my $item_setRetracing = Gtk2::MenuItem->new('Set _retracing');
            $item_setRetracing->set_submenu($subMenu_setRetracing);
            $subMenu_setExitType->append($item_setRetracing);

                # 'Set random' sub-submenu
                my $subMenu_setRandomExit = Gtk2::Menu->new();

                my $item_markRandomRegion = Gtk2::MenuItem->new(
                    'Set random destination in same _region',
                );
                $item_markRandomRegion->signal_connect('activate' => sub {

                    $self->markRandomExitCallback('same_region');
                });
                $subMenu_setRandomExit->append($item_markRandomRegion);

                my $item_markRandomAnywhere = Gtk2::MenuItem->new(
                    'Set random destination _anywhere',
                );
                $item_markRandomAnywhere->signal_connect('activate' => sub {

                    $self->markRandomExitCallback('any_region');
                });
                $subMenu_setRandomExit->append($item_markRandomAnywhere);

                my $item_markRandomList = Gtk2::MenuItem->new('_Use list of random destinations');
                $item_markRandomList->signal_connect('activate' => sub {

                    $self->markRandomExitCallback('room_list');
                });
                $subMenu_setRandomExit->append($item_markRandomList);

                $subMenu_setRandomExit->append(Gtk2::SeparatorMenuItem->new());    # Separator

                my $item_restoreRandomExit = Gtk2::MenuItem->new('_Restore incomplete exit');
                $item_restoreRandomExit->signal_connect('activate' => sub {

                    $self->restoreRandomExitCallback();
                });
                $subMenu_setRandomExit->append($item_restoreRandomExit);

            my $item_setRandomExit = Gtk2::MenuItem->new('Set ra_ndom');
            $item_setRandomExit->set_submenu($subMenu_setRandomExit);
            $subMenu_setExitType->append($item_setRandomExit);

                # 'Set super' sub-submenu
                my $subMenu_setSuperExit = Gtk2::Menu->new();

                my $item_markSuper = Gtk2::MenuItem->new('Mark exit as _super-region exit');
                $item_markSuper->signal_connect('activate' => sub {

                    $self->markSuperExitCallback(FALSE);
                });
                $subMenu_setSuperExit->append($item_markSuper);

                my $item_markSuperExcl = Gtk2::MenuItem->new(
                    '_Mark exit as exclusive super-region exit',
                );
                $item_markSuperExcl->signal_connect('activate' => sub {

                    $self->markSuperExitCallback(TRUE);
                });
                $subMenu_setSuperExit->append($item_markSuperExcl);

                $subMenu_setSuperExit->append(Gtk2::SeparatorMenuItem->new());    # Separator

                my $item_markNotSuper = Gtk2::MenuItem->new('Mark exit as _normal region exit');
                $item_markNotSuper->signal_connect('activate' => sub {

                    $self->restoreSuperExitCallback();
                });
                $subMenu_setSuperExit->append($item_markNotSuper);

            my $item_setSuperExit = Gtk2::MenuItem->new('Set s_uper');
            $item_setSuperExit->set_submenu($subMenu_setSuperExit);
            $subMenu_setExitType->append($item_setSuperExit);
            # (Also requires $self->selectedExit->regionFlag)
            if (! $self->selectedExit->regionFlag) {

                $item_setSuperExit->set_sensitive(FALSE);
            }

            $subMenu_setExitType->append(Gtk2::SeparatorMenuItem->new());    # Separator

            my $item_setExitTwin = Gtk2::MenuItem->new('Set exit _twin...');
            $item_setExitTwin->signal_connect('activate' => sub {

                $self->setExitTwinCallback();
            });
            $subMenu_setExitType->append($item_setExitTwin);
            # (Also requires either a $self->selectedExit which is either a one-way exit or an
            #   uncertain exit)
            if (
                ! $self->selectedExit->oneWayFlag
                || ! (
                    $self->selectedExit->destRoom
                    && ! $self->selectedExit->twinExit
                    && ! $self->selectedExit->retraceFlag
                    && $self->selectedExit->randomType eq 'none'
                )
            ) {
                $item_setExitTwin->set_sensitive(FALSE);
            }

        my $item_setExitType = Gtk2::MenuItem->new('Set e_xit type');
        $item_setExitType->set_submenu($subMenu_setExitType);
        $menu_exits->append($item_setExitType);

            # 'Exit tags' submenu
            my $subMenu_exitTags = Gtk2::Menu->new();

            my $item_editTag = Gtk2::MenuItem->new('_Edit exit tag');
            $item_editTag->signal_connect('activate' => sub {

                $self->editExitTagCallback();
            });
            $subMenu_exitTags->append($item_editTag);

            my $item_toggleExitTag = Gtk2::MenuItem->new('_Toggle exit tag');
            $item_toggleExitTag->signal_connect('activate' => sub {

                $self->toggleExitTagCallback();
            });
            $subMenu_exitTags->append($item_toggleExitTag);

            $subMenu_exitTags->append(Gtk2::SeparatorMenuItem->new());    # Separator

            my $item_resetPosition = Gtk2::MenuItem->new('Reset text positio_n');
            $item_resetPosition->signal_connect('activate' => sub {

                $self->resetExitOffsetsCallback();
            });
            $subMenu_exitTags->append($item_resetPosition);

        my $item_exitTags = Gtk2::MenuItem->new('Exit _tags');
        $item_exitTags->set_submenu($subMenu_exitTags);
        $menu_exits->append($item_exitTags);
        # (Also requires either a $self->selectedExit which is a region exit)
        if (! $self->selectedExit->regionFlag) {

            $item_exitTags->set_sensitive(FALSE);
        }

        $menu_exits->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_editExit = Gtk2::ImageMenuItem->new('_Edit exit...');
        my $img_editExit = Gtk2::Image->new_from_stock('gtk-edit', 'menu');
        $item_editExit->set_image($img_editExit);
        $item_editExit->signal_connect('activate' => sub {

            $self->editExitCallback();
        });
        $menu_exits->append($item_editExit);

        $menu_exits->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_deleteExit = Gtk2::ImageMenuItem->new('_Delete exit');
        my $img_deleteExit = Gtk2::Image->new_from_stock('gtk-add', 'menu');
        $item_deleteExit->set_image($img_deleteExit);
        $item_deleteExit->signal_connect('activate' => sub {

            $self->deleteExitCallback();
        });
        $menu_exits->append($item_deleteExit);

        # Setup complete
        $menu_exits->show_all();

        return $menu_exits;
    }

    sub enableExitTagsPopupMenu {

        # Called by $self->canvasObjEventHandler
        # Creates a popup-menu for the selected exit tag
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->enableExitTagsPopupMenu',
                @_,
            );
        }

        # Set up the popup menu
        my $menu_tags = Gtk2::Menu->new();
        if (! $menu_tags) {

            return undef;
        }

        # (Everything here assumes $self->currentRegionmap and $self->selectedExitTag)

        my $item_editTag = Gtk2::MenuItem->new('_Edit exit tag');
        $item_editTag->signal_connect('activate' => sub {

            $self->editExitTagCallback();
        });
        $menu_tags->append($item_editTag);

        my $item_cancelTag = Gtk2::MenuItem->new('_Cancel exit tag');
        $item_cancelTag->signal_connect('activate' => sub {

            $self->toggleExitTagCallback();
        });
        $menu_tags->append($item_cancelTag);

        $menu_tags->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_viewDestination = Gtk2::MenuItem->new('_View destination');
        $item_viewDestination->signal_connect('activate' => sub {

            $self->viewExitDestination();
        });
        $menu_tags->append($item_viewDestination);

        $menu_tags->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_resetPosition = Gtk2::MenuItem->new('_Reset position');
        $item_resetPosition->signal_connect('activate' => sub {

            $self->resetExitOffsetsCallback();
        });
        $menu_tags->append($item_resetPosition);

        # Setup complete
        $menu_tags->show_all();

        return $menu_tags;
    }

    sub enableLabelsPopupMenu {

        # Called by $self->canvasObjEventHandler
        # Creates a popup-menu for the selected label
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Menu created

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableLabelsPopupMenu', @_);
        }

        # Set up the popup menu
        my $menu_labels = Gtk2::Menu->new();
        if (! $menu_labels) {

            return undef;
        }

        # (Everything here assumes $self->currentRegionmap and $self->selectedLabel)

        my $item_editLabel = Gtk2::MenuItem->new('_Edit label...');
        $item_editLabel->signal_connect('activate' => sub {

            $self->editLabelCallback();
        });
        $menu_labels->append($item_editLabel);

            # 'Set label size' submenu
            my $subMenu_labelSize = Gtk2::Menu->new();

            my $item_setLabelNormal = Gtk2::MenuItem->new('Set _normal size');
            $item_setLabelNormal->signal_connect('activate' => sub {

                $self->worldModelObj->setLabelSize(TRUE, $self->selectedLabel, 1);
            });
            $subMenu_labelSize->append($item_setLabelNormal);

            my $item_setLabelLarge = Gtk2::MenuItem->new('Set _large size');
            $item_setLabelLarge->signal_connect('activate' => sub {

                $self->worldModelObj->setLabelSize(TRUE, $self->selectedLabel, 2);
            });
            $subMenu_labelSize->append($item_setLabelLarge);

            my $item_setLabelHuge = Gtk2::MenuItem->new('Set _huge size');
            $item_setLabelHuge->signal_connect('activate' => sub {

                $self->worldModelObj->setLabelSize(TRUE, $self->selectedLabel, 4);
            });
            $subMenu_labelSize->append($item_setLabelHuge);

        my $item_labelSize = Gtk2::MenuItem->new('Set label si_ze');
        $item_labelSize->set_submenu($subMenu_labelSize);
        $menu_labels->append($item_labelSize);

        $menu_labels->append(Gtk2::SeparatorMenuItem->new());  # Separator

        my $item_deleteLabel = Gtk2::ImageMenuItem->new('_Delete label...');
        my $img_deleteLabel = Gtk2::Image->new_from_stock('gtk-delete', 'menu');
        $item_deleteLabel->set_image($img_deleteLabel);
        $item_deleteLabel->signal_connect('activate' => sub {

            if ($self->selectedLabel) {

                $self->worldModelObj->deleteLabels(
                    TRUE,           # Update Automapper windows now
                    $self->selectedLabel,
                );
            }
        });
        $menu_labels->append($item_deleteLabel);

        # Setup complete
        $menu_labels->show_all();

        return $menu_labels;
    }

    # Toolbar widget methods

    sub enableToolbar {

        # Called by $self->drawWidgets
        # Sets up the Automapper window's Gtk2::Toolbar widget
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Toolbar created

        my ($self, $check) = @_;

        # Local variables
        my $label;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableToolbar', @_);
        }

        # Create the toolbar
        my $toolbar = Gtk2::Toolbar->new();
        if (! $toolbar) {

            return undef;
        }

        # Store the widget
        $self->ivPoke('toolbar', $toolbar);
        # Use large icons, and allow the menu to shrink when there's not enough space for it
        $toolbar->set_icon_size('large-toolbar');
        $toolbar->set_show_arrow(TRUE);

        if ($axmud::CLIENT->toolbarLabelFlag) {

            # Otherwise, $label remains as 'undef', which is what Gtk2::ToolButton is expecting
            $label = 'Switch icons';
        }

        # The buttons on the toolbar consist of a switcher icon - which is always present - and a
        #   set of other icons. Clicking on the switcher icon cycles through the sets of other icons
        # Create the switcher icon
        my $toolButton = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_switch.png'),
            $label,
        );
        $toolButton->signal_connect('clicked' => sub {

            # Switch to the next set of toolbar buttons
            $self->switchToolbarButtons();
        });
        $toolButton->set_tooltip_text('Icon switcher');
        $toolbar->insert($toolButton, -1);
        # Store it in an IV
        $self->ivPoke('toolbarSwitchIcon', $toolButton);

        # Immediately to the right of the switcher icon is a separator, which is also always present
        my $separator = Gtk2::SeparatorToolItem->new();
        $toolbar->insert($separator, -1);
        # Store it in an IV
        $self->ivPoke('toolbarMainSeparator', $separator);

        # Set up the remaining sets of icons. We can change the order in which the sets are
        #   displayed by changing the arguments to each setup function
        for (my $count = 1; $count <= $self->toolbarSetCount; $count++) {

            my $package = 'enableToolbarButtonSet' . $count;

            $self->$package($count);
        }

        # Show the first set of icons
        $self->switchToolbarButtons();

        # Setup complete
        return $toolbar;
    }

    sub switchToolbarButtons {

        # Called by $self->enableToolbar, and whenever the user clicks on the switcher icon
        # Removes the icon set currently visible on the toolbar, and displays the next set
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $listRef;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->switchToolbarButtons', @_);
        }

        # Remove the currently visible set of buttons from view
        if ($self->toolbarCurrentSet) {

            $listRef = $self->ivShow('toolbarButtonHash', $self->toolbarCurrentSet);
            foreach my $button (@$listRef) {

                $axmud::CLIENT->desktopObj->removeWidget($self->toolbar, $button);
            }
        }

        # Decide which set of buttons to show next
        $self->ivIncrement('toolbarCurrentSet');
        if ($self->toolbarCurrentSet > $self->toolbarSetCount) {

            # Go back to the first set
            $self->ivPoke('toolbarCurrentSet', 1);
        }

        # Display a set of buttons
        $listRef = $self->ivShow('toolbarButtonHash', $self->toolbarCurrentSet);
        foreach my $button (@$listRef) {

            my $label;

            # (Separators don't have labels, so we need to check for that)
            if (! $axmud::CLIENT->toolbarLabelFlag && $button->isa('Gtk2::ToolButton')) {

                $button->set_label(undef);
            }

            $self->toolbar->insert($button, -1);
        }

        $self->toolbar->show_all();

        return 1;
    }

    sub enableToolbarButtonSet1 {

        # Called by $self->enableToolbar to set up one set of toolbar buttons
        #
        # Expected arguments
        #   $setNumber  - The number of the set (which can vary from the number of this function, if
        #                   we want to change the order in which the sets are displayed)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $setNumber, $check) = @_;

        # Local variables
        my @iconList;

        # Check for improper arguments
        if (! defined $setNumber || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->enableToolbarButtonSet1',
                @_,
            );
        }

        # Radio button for 'wait mode'
        my $radioButton_waitMode = Gtk2::RadioToolButton->new(undef);
        if ($self->mode eq 'wait') {

            $radioButton_waitMode->set_active(TRUE);
        }
        $radioButton_waitMode->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_wait.png')
        );
        $radioButton_waitMode->set_label('Wait mode');
        $radioButton_waitMode->set_tooltip_text('Wait mode');
        $radioButton_waitMode->signal_connect('toggled' => sub {

            # (To stop the equivalent menu item from being toggled by the call to ->setMode, make
            #   use of $self->ignoreMenuUpdateFlag)
            if ($radioButton_waitMode->get_active && ! $self->ignoreMenuUpdateFlag) {

                $self->setMode('wait');
            }
        });
        push (@iconList, $radioButton_waitMode);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_set_wait_mode', $radioButton_waitMode);

        # Radio button for 'follow mode'
        my $radioButton_followMode = Gtk2::RadioToolButton->new_from_widget($radioButton_waitMode);
        if ($self->mode eq 'follow') {

            $radioButton_followMode->set_active(TRUE);
        }
        $radioButton_followMode->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_follow.png')
        );
        $radioButton_followMode->set_label('Follow mode');
        $radioButton_followMode->set_tooltip_text('Follow mode');
        $radioButton_followMode->signal_connect('toggled' => sub {

            if ($radioButton_followMode->get_active && ! $self->ignoreMenuUpdateFlag) {

                $self->setMode('follow');
            }
        });
        push (@iconList, $radioButton_followMode);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'icon_set_follow_mode', $radioButton_followMode);

        # Radio button for 'update' mode
        my $radioButton_updateMode = Gtk2::RadioToolButton->new_from_widget(
            $radioButton_followMode,
        );
        if ($self->mode eq 'update') {

            $radioButton_updateMode->set_active(TRUE);
        }
        $radioButton_updateMode->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_update.png')
        );
        $radioButton_updateMode->set_label('Update mode');
        $radioButton_updateMode->set_tooltip_text('Update mode');
        $radioButton_updateMode->signal_connect('toggled' => sub {

            if ($radioButton_updateMode->get_active && ! $self->ignoreMenuUpdateFlag) {

                $self->setMode('update');
            }
        });
        push (@iconList, $radioButton_updateMode);
        # (Requires $self->currentRegionmap, GA::Obj::WorldModel->disableUpdateModeFlag set to
        #   FALSE and a session not in 'connect offline' mode
        $self->ivAdd('menuToolItemHash', 'icon_set_update_mode', $radioButton_updateMode);

        # Separator
        my $separator = Gtk2::SeparatorToolItem->new();
        push (@iconList, $separator);

        # Toolbutton for 'move up level'
        my $toolButton_moveUpLevel = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_move_up.png'),
            'Move up level',
        );
        $toolButton_moveUpLevel->set_tooltip_text('Move up level');
        $toolButton_moveUpLevel->signal_connect('clicked' => sub {

            $self->setCurrentLevel($self->currentRegionmap->currentLevel + 1);

            # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
            $self->restrictWidgets();
        });
        push (@iconList, $toolButton_moveUpLevel);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'icon_move_up_level', $toolButton_moveUpLevel);

        # Toolbutton for 'move down level'
        my $toolButton_moveDownLevel = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_move_down.png'),
            'Move down level',
        );
        $toolButton_moveDownLevel->set_tooltip_text('Move down level');
        $toolButton_moveDownLevel->signal_connect('clicked' => sub {

            $self->setCurrentLevel($self->currentRegionmap->currentLevel - 1);

            # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
            $self->restrictWidgets();
        });
        push (@iconList, $toolButton_moveDownLevel);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'icon_move_down_level', $toolButton_moveDownLevel);

        # Separator
        my $separator2 = Gtk2::SeparatorToolItem->new();
        push (@iconList, $separator2);

        # Toolbutton for 'set current room'
        my $toolButton_setCurrentRoom = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_set.png'),
            'Set current room',
        );
        $toolButton_setCurrentRoom->set_tooltip_text('Set current room');
        $toolButton_setCurrentRoom->signal_connect('clicked' => sub {

            $self->mapObj->setCurrentRoom($self->selectedRoom);
        });
        push (@iconList, $toolButton_setCurrentRoom);
        # (Requires $self->currentRegionmap & $self->selectedRoom)
        $self->ivAdd('menuToolItemHash', 'icon_set_current_room', $toolButton_setCurrentRoom);

        # Toolbutton for 'reset locator'
        my $toolButton_resetLocator = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_reset_locator.png'),
            'Reset Locator task',
        );
        $toolButton_resetLocator->set_tooltip_text('Reset locator task');
        $toolButton_resetLocator->signal_connect('clicked' => sub {

            $self->resetLocatorCallback();
        });
        push (@iconList, $toolButton_resetLocator);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'icon_reset_locator', $toolButton_resetLocator);

        # Toolbutton for 'horizontal exit length'
        my $toolButton_exitLengths = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_exit_lengths.png'),
            'Horizontal exit length',
        );
        $toolButton_exitLengths->set_tooltip_text('Exit lengths');
        $toolButton_exitLengths->signal_connect('clicked' => sub {

            $self->setExitLengthCallback('horizontal');
        });
        push (@iconList, $toolButton_exitLengths);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'icon_exit_lengths', $toolButton_exitLengths);

        # Toggle button for 'drag mode'
        my $toggleButton_dragMode = Gtk2::ToggleToolButton->new();
        $toggleButton_dragMode->set_active($self->dragModeFlag);
        $toggleButton_dragMode->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_drag_mode.png'),
        );
        $toggleButton_dragMode->set_label('Drag mode');
        $toggleButton_dragMode->set_tooltip_text('Drag mode');
        $toggleButton_dragMode->signal_connect('toggled' => sub {

            if ($toggleButton_dragMode->get_active()) {
                $self->ivPoke('dragModeFlag', TRUE);
            } else {
                $self->ivPoke('dragModeFlag', FALSE);
            }

            # Set the equivalent menu item
            if ($self->ivExists('menuToolItemHash', 'drag_mode')) {

                my $menuItem = $self->ivShow('menuToolItemHash', 'drag_mode');
                $menuItem->set_active($self->dragModeFlag);
            }
        });
        push (@iconList, $toggleButton_dragMode);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'icon_drag_mode', $toggleButton_dragMode);

        # Toolbutton for 'connect to click'
        my $toolButton_connectClick = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_connect_click.png'),
            'Connect selected exit to room',
        );
        $toolButton_connectClick->set_tooltip_text('Connect selected exit to room');
        $toolButton_connectClick->signal_connect('clicked' => sub {

            $self->connectToClickCallback();
        });
        push (@iconList, $toolButton_connectClick);
        # (Requires $self->currentRegionmap, $self->selectedExit and
        #   $self->selectedExit->drawMode is 'primary', 'temp_unalloc' or 'perm_alloc')
        $self->ivAdd('menuToolItemHash', 'icon_connect_click', $toolButton_connectClick);

        # Toolbutton for 'move selected rooms to click'
        my $toolButton_moveClick = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_move_click.png'),
            'Move selected rooms to click',
        );
        $toolButton_moveClick->set_tooltip_text('Move selected rooms to click');
        $toolButton_moveClick->signal_connect('clicked' => sub {

            # Set the free clicking mode: $self->mouseClickEvent will move the objects  when the
            #   user next clicks on an empty part of the map
            $self->ivPoke('freeClickMode', 'move_room');
        });
        push (@iconList, $toolButton_moveClick);
        # (Requires $self->currentRegionmap and one or more selected rooms)
        $self->ivAdd('menuToolItemHash', 'icon_move_to_click', $toolButton_moveClick);

        # Store the buttons created
        $self->ivAdd('toolbarButtonHash', $setNumber, \@iconList);

        return 1;
    }

    sub enableToolbarButtonSet2 {

        # Called by $self->enableToolbar to set up one set of toolbar buttons
        #
        # Expected arguments
        #   $setNumber  - The number of the set (which can vary from the number of this function, if
        #                   we want to change the order in which the sets are displayed)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $setNumber, $check) = @_;

        # Local variables
        my @iconList;

        # Check for improper arguments
        if (! defined $setNumber || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->enableToolbarButtonSet2',
                @_,
            );
        }

        # Toolbutton for 'select all'
        my $toolButton_selectAll = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_select_all.png'),
            'Select all',
        );
        $toolButton_selectAll->set_tooltip_text('Select all rooms, exits and labels');
        $toolButton_selectAll->signal_connect('clicked' => sub {

            $self->selectAllCallback();
        });
        push (@iconList, $toolButton_selectAll);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'icon_select_all', $toolButton_selectAll);

        # Toolbutton for 'search world model'
        my $toolButton_searchWorldModel = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_search_model.png'),
            'Search world model',
        );
        $toolButton_searchWorldModel->set_tooltip_text('Search world model');
        $toolButton_searchWorldModel->signal_connect('clicked' => sub {

            # Open a 'pref' window to conduct the search
            $self->createFreeWin(
                'Games::Axmud::PrefWin::Search',
                $self,
                $self->session,
                'World model search',
            );
        });
        push (@iconList, $toolButton_searchWorldModel);

        # Toolbutton for 'edit dictionary'
        my $toolButton_editDictionary = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_edit_dict.png'),
            'Edit current dictionary',
        );
        $toolButton_editDictionary->set_tooltip_text('Edit current dictionary');
        $toolButton_editDictionary->signal_connect('clicked' => sub {

            # Open an 'edit' window for the current dictionary
            $self->createFreeWin(
                'Games::Axmud::EditWin::Dict',
                $self,
                $self->session,
                'Edit \'' . $self->session->currentDict->name . '\' dictionary',
                $self->session->currentDict,
                FALSE,          # Not temporary
            );
        });
        push (@iconList, $toolButton_editDictionary);

        # Toolbutton for 'add words'
        my $toolButton_addQuickWords = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_add_word.png'),
            'Add dictionary words',
        );
        $toolButton_addQuickWords->set_tooltip_text('Add dictionary words');
        $toolButton_addQuickWords->signal_connect('clicked' => sub {

            $self->createFreeWin(
                'Games::Axmud::OtherWin::QuickWord',
                $self,
                $self->session,
                'Quick word adder',
            );
        });
        push (@iconList, $toolButton_addQuickWords);

        # Toolbutton for 'edit preferences'
        my $toolButton_editPreferences = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_edit_model.png'),
            'Edit world model preferences',
        );
        $toolButton_editPreferences->set_tooltip_text('Edit world model');
        $toolButton_editPreferences->signal_connect('clicked' => sub {

            # Open an 'edit' window for the world model
            $self->createFreeWin(
                'Games::Axmud::EditWin::WorldModel',
                $self,
                $self->session,
                'Edit world model',
                $self->session->worldModelObj,
                FALSE,                          # Not temporary
            );
        });
        push (@iconList, $toolButton_editPreferences);

        # Separator
        my $separator = Gtk2::SeparatorToolItem->new();
        push (@iconList, $separator);

        # Toggle button for 'enable painter'
        my $toggleButton_enablePainter = Gtk2::ToggleToolButton->new();
        $toggleButton_enablePainter->set_active($self->painterFlag);
        $toggleButton_enablePainter->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_enable_painter.png'),
        );
        $toggleButton_enablePainter->set_label('Enable painter');
        $toggleButton_enablePainter->set_tooltip_text('Enable painter');
        $toggleButton_enablePainter->signal_connect('toggled' => sub {

            my $item;

            # Toggle the flag
            if ($toggleButton_enablePainter->get_active()) {
                $self->ivPoke('painterFlag', TRUE);
            } else {
                $self->ivPoke('painterFlag', FALSE);
            }

            # Update the corresponding menu item
            $item = $self->ivShow('menuToolItemHash', 'enable_painter');
            if ($item) {

                $item->set_active($self->painterFlag);
            }
        });
        push (@iconList, $toggleButton_enablePainter);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_enable_painter', $toggleButton_enablePainter);

        # Toolbutton for 'edit painter'
        my $toolButton_editPainter = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_edit_painter.png'),
            'Edit painter',
        );
        $toolButton_editPainter->set_tooltip_text('Edit painter');
        $toolButton_editPainter->signal_connect('clicked' => sub {

            # Open an 'edit' window for the painter object
            $self->createFreeWin(
                'Games::Axmud::EditWin::Painter',
                $self,
                $self->session,
                'Edit world model painter',
                $self->worldModelObj->painterObj,
                FALSE,          # Not temporary
            );
        });
        push (@iconList, $toolButton_editPainter);

        # Radio button for 'paint all rooms'
        my $radioButton_paintAllRooms = Gtk2::RadioToolButton->new(undef);
        $radioButton_paintAllRooms->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_paint_all.png'),
        );
        $radioButton_paintAllRooms->set_label('Paint all rooms');
        $radioButton_paintAllRooms->set_tooltip_text('Paint all rooms');
        $radioButton_paintAllRooms->signal_connect('toggled' => sub {

            if ($radioButton_paintAllRooms->get_active()) {

                $self->worldModelObj->set_paintAllRoomsFlag(TRUE);

                # Set the equivalent menu item
                if ($self->ivExists('menuToolItemHash', 'paint_all')) {

                    $self->ivShow('menuToolItemHash', 'paint_all')->set_active(TRUE);
                }
            }
        });
        push (@iconList, $radioButton_paintAllRooms);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_paint_all', $radioButton_paintAllRooms);

        # Radio button for 'paint only new rooms'
        my $radioButton_paintNewRooms = Gtk2::RadioToolButton->new($radioButton_paintAllRooms);
        if (! $self->worldModelObj->paintAllRoomsFlag) {

            $radioButton_paintNewRooms->set_active(TRUE);
        }
        $radioButton_paintNewRooms->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_paint_new.png'),
        );
        $radioButton_paintNewRooms->set_label('Paint only new rooms');
        $radioButton_paintNewRooms->set_tooltip_text('Paint only new rooms');
        $radioButton_paintNewRooms->signal_connect('toggled' => sub {

            if ($radioButton_paintNewRooms->get_active) {

                $self->worldModelObj->set_paintAllRoomsFlag(FALSE);

                # Set the equivalent menu item
                if ($self->ivExists('menuToolItemHash', 'paint_new')) {

                    $self->ivShow('menuToolItemHash', 'paint_new')->set_active(TRUE);
                }
            }
        });
        push (@iconList, $radioButton_paintNewRooms);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_paint_new', $radioButton_paintNewRooms);

        # Separator
        my $separator2 = Gtk2::SeparatorToolItem->new();
        push (@iconList, $separator2);

        # Toolbutton for 'take screenshot'
        my $toolButton_visibleScreenshot = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_take_screenshot.png'),
            'Take screenshot of visible map',
        );
        $toolButton_visibleScreenshot->set_tooltip_text('Take screenshot of visible map');
        $toolButton_visibleScreenshot->signal_connect('clicked' => sub {

            $self->visibleScreenshotCallback();
        });
        push (@iconList, $toolButton_visibleScreenshot);
        # (Requires $self->currentRegionmap)
        $self->ivAdd('menuToolItemHash', 'icon_visible_screenshot', $toolButton_visibleScreenshot);

        # Store the buttons created
        $self->ivAdd('toolbarButtonHash', $setNumber, \@iconList);

        return 1;
    }

    sub enableToolbarButtonSet3 {

        # Called by $self->enableToolbar to set up one set of toolbar buttons
        #
        # Expected arguments
        #   $setNumber  - The number of the set (which can vary from the number of this function, if
        #                   we want to change the order in which the sets are displayed)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $setNumber, $check) = @_;

        # Local variables
        my @iconList;

        # Check for improper arguments
        if (! defined $setNumber || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->enableToolbarButtonSet3',
                @_,
            );
        }

        # Toolbutton for 'centre map on current room'
        my $toolButton_centreCurrentRoom = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_centre_current.png'),
            'Centre map on current room',
        );
        $toolButton_centreCurrentRoom->set_tooltip_text('Centre map on current room');
        $toolButton_centreCurrentRoom->signal_connect('clicked' => sub {

            $self->centreMapOverRoom($self->mapObj->currentRoom);
        });
        push (@iconList, $toolButton_centreCurrentRoom);
        # (Requires $self->currentRegionmap & $self->mapObj->currentRoom)
        $self->ivAdd(
            'menuToolItemHash',
            'icon_centre_map_current_room',
            $toolButton_centreCurrentRoom,
        );

        # Toolbutton for 'centre map on selected room'
        my $toolButton_centreSelectedRoom = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_centre_selected.png'),
            'Centre map on selected room',
        );
        $toolButton_centreSelectedRoom->set_tooltip_text('Centre map on selected room');
        $toolButton_centreSelectedRoom->signal_connect('clicked' => sub {

            $self->centreMapOverRoom($self->selectedRoom);
        });
        push (@iconList, $toolButton_centreSelectedRoom);
        # (Requires $self->currentRegionmap & $self->selectedRoom)
        $self->ivAdd(
            'menuToolItemHash',
            'icon_centre_map_selected_room',
            $toolButton_centreSelectedRoom,
        );

        # Toolbutton for 'centre map on last known room'
        my $toolButton_centreLastKnownRoom = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_centre_last.png'),
            'Centre map on last known room',
        );
        $toolButton_centreLastKnownRoom->set_tooltip_text('Centre map on last known room');
        $toolButton_centreLastKnownRoom->signal_connect('clicked' => sub {

            $self->centreMapOverRoom($self->mapObj->lastKnownRoom);
        });
        push (@iconList, $toolButton_centreLastKnownRoom);
        # (Requires $self->currentRegionmap & $self->mapObj->lastknownRoom)
        $self->ivAdd(
            'menuToolItemHash',
            'icon_centre_map_last_known_room',
            $toolButton_centreLastKnownRoom,
        );

        # Toolbutton for 'centre map on middle of grid'
        my $toolButton_centreMiddleGrid = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_centre_middle.png'),
            'Centre map on middle of grid',
        );
        $toolButton_centreMiddleGrid->set_tooltip_text('Centre map on middle of grid');
        $toolButton_centreMiddleGrid->signal_connect('clicked' => sub {

            $self->setMapPosn(0.5, 0.5);
        });
        push (@iconList, $toolButton_centreMiddleGrid);
        # (Requires $self->currentRegionmap)
        $self->ivAdd(
            'menuToolItemHash',
            'icon_centre_map_middle_grid',
            $toolButton_centreMiddleGrid,
        );

        # Separator
        my $separator = Gtk2::SeparatorToolItem->new();
        push (@iconList, $separator);

        # Toggle button for 'track current room'
        my $toggleButton_trackCurrentRoom = Gtk2::ToggleToolButton->new();
        $toggleButton_trackCurrentRoom->set_active($self->worldModelObj->trackPosnFlag);
        $toggleButton_trackCurrentRoom->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_track_room.png'),
        );
        $toggleButton_trackCurrentRoom->set_label('Track current room');
        $toggleButton_trackCurrentRoom->set_tooltip_text('Track current room');
        $toggleButton_trackCurrentRoom->signal_connect('toggled' => sub {

            if (! $self->ignoreMenuUpdateFlag) {

                $self->worldModelObj->toggleFlag(
                    'trackPosnFlag',
                    $toggleButton_trackCurrentRoom->get_active(),
                    FALSE,      # Don't call $self->drawRegion
                    'track_current_room',
                    'icon_track_current_room',
                );
            }
        });
        push (@iconList, $toggleButton_trackCurrentRoom);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_track_current_room', $toggleButton_trackCurrentRoom);

        # Separator
        my $separator2 = Gtk2::SeparatorToolItem->new();
        push (@iconList, $separator2);

        # Radio button for 'always track position'
        my $radioButton_trackAlways = Gtk2::RadioToolButton->new(undef);
        if (
            $self->worldModelObj->trackingSensitivity != 0.33
            && $self->worldModelObj->trackingSensitivity != 0.66
            && $self->worldModelObj->trackingSensitivity != 1
        ) {
            # Only the sensitivity values 0, 0.33, 0.66 and 1 are curently allowed; act as
            #   though the IV was set to 0
            $radioButton_trackAlways->set_active(TRUE);
        }
        $radioButton_trackAlways->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_track_always.png'),
        );
        $radioButton_trackAlways->set_label('Always track position');
        $radioButton_trackAlways->set_tooltip_text('Always track position');
        $radioButton_trackAlways->signal_connect('toggled' => sub {

            if (! $self->ignoreMenuUpdateFlag && $radioButton_trackAlways->get_active()) {

                $self->worldModelObj->setTrackingSensitivity(0);
            }
        });
        push (@iconList, $radioButton_trackAlways);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_track_always', $radioButton_trackAlways);

        # Radio button for 'track position near centre'
        my $radioButton_trackNearCentre = Gtk2::RadioToolButton->new($radioButton_trackAlways);
        if ($self->worldModelObj->trackingSensitivity == 0.33) {

            $radioButton_trackNearCentre->set_active(TRUE);
        }
        $radioButton_trackNearCentre->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_track_centre.png'),
        );
        $radioButton_trackNearCentre->set_label('Track position near centre');
        $radioButton_trackNearCentre->set_tooltip_text('Track position near centre');
        $radioButton_trackNearCentre->signal_connect('toggled' => sub {

            if (! $self->ignoreMenuUpdateFlag && $radioButton_trackNearCentre->get_active()) {

                $self->worldModelObj->setTrackingSensitivity(0.33);
            }
        });
        push (@iconList, $radioButton_trackNearCentre);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_track_near_centre', $radioButton_trackNearCentre);

        # Radio button for 'track near edge'
        my $radioButton_trackNearEdge = Gtk2::RadioToolButton->new($radioButton_trackNearCentre);
        if ($self->worldModelObj->trackingSensitivity == 0.66) {

            $radioButton_trackNearEdge->set_active(TRUE);
        }
        $radioButton_trackNearEdge->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_track_edge.png'),
        );
        $radioButton_trackNearEdge->set_label('Track position near edge');
        $radioButton_trackNearEdge->set_tooltip_text('Track position near edge');
        $radioButton_trackNearEdge->signal_connect('toggled' => sub {

            if (! $self->ignoreMenuUpdateFlag && $radioButton_trackNearEdge->get_active()) {

                $self->worldModelObj->setTrackingSensitivity(0.66);
            }
        });
        push (@iconList, $radioButton_trackNearEdge);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_track_near_edge', $radioButton_trackNearEdge);

        # Radio button for 'track if not visible'
        my $radioButton_trackNotVisible = Gtk2::RadioToolButton->new($radioButton_trackNearEdge);
        if ($self->worldModelObj->trackingSensitivity == 1) {

            $radioButton_trackNotVisible->set_active(TRUE);
        }
        $radioButton_trackNotVisible->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_track_visible.png'),
        );
        $radioButton_trackNotVisible->set_label('Track if not visible');
        $radioButton_trackNotVisible->set_tooltip_text('Track position if not visible');
        $radioButton_trackNotVisible->signal_connect('toggled' => sub {

            if (! $self->ignoreMenuUpdateFlag && $radioButton_trackNotVisible->get_active()) {

                $self->worldModelObj->setTrackingSensitivity(1);
            }
        });
        push (@iconList, $radioButton_trackNotVisible);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_track_not_visible', $radioButton_trackNotVisible);

        # Store the buttons created
        $self->ivAdd('toolbarButtonHash', $setNumber, \@iconList);

        return 1;
    }

    sub enableToolbarButtonSet4 {

        # Called by $self->enableToolbar to set up one set of toolbar buttons
        #
        # Expected arguments
        #   $setNumber  - The number of the set (which can vary from the number of this function, if
        #                   we want to change the order in which the sets are displayed)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $setNumber, $check) = @_;

        # Local variables
        my @iconList;

        # Check for improper arguments
        if (! defined $setNumber || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->enableToolbarButtonSet4',
                @_,
            );
        }

        # Radio button for 'use region exit settings' mode
        my $radioButton_deferDrawExits = Gtk2::RadioToolButton->new(undef);
        $radioButton_deferDrawExits->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_use_region.png'),
        );
        $radioButton_deferDrawExits->set_label('Use region exit settings');
        $radioButton_deferDrawExits->set_tooltip_text('Use region exit settings');
        $radioButton_deferDrawExits->signal_connect('toggled' => sub {

            if (! $self->ignoreMenuUpdateFlag && $radioButton_deferDrawExits->get_active()) {

                $self->worldModelObj->switchMode(
                    'drawExitMode',
                    'ask_regionmap',    # New value of ->drawExitMode
                    TRUE,               # Do call $self->drawRegion
                    'draw_defer_exits',
                    'icon_draw_defer_exits',
                );
            }
        });
        push (@iconList, $radioButton_deferDrawExits);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_draw_defer_exits', $radioButton_deferDrawExits);

        # Radio button for 'draw no exits' mode
        my $radioButton_drawNoExits = Gtk2::RadioToolButton->new_from_widget(
            $radioButton_deferDrawExits,
        );
        if ($self->worldModelObj->drawExitMode eq 'no_exit') {

            $radioButton_drawNoExits->set_active(TRUE);
        }
        $radioButton_drawNoExits->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_draw_none.png'),
        );
        $radioButton_drawNoExits->set_label('Draw no exits');
        $radioButton_drawNoExits->set_tooltip_text('Draw no exits');
        $radioButton_drawNoExits->signal_connect('toggled' => sub {

            if (! $self->ignoreMenuUpdateFlag && $radioButton_drawNoExits->get_active()) {

                $self->worldModelObj->switchMode(
                    'drawExitMode',
                    'no_exit',          # New value of ->drawExitMode
                    TRUE,               # Do call $self->drawRegion
                    'draw_no_exits',
                    'icon_draw_no_exits',
                );
            }
        });
        push (@iconList, $radioButton_drawNoExits);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_draw_no_exits', $radioButton_drawNoExits);

        # Radio button for 'draw simple exits' mode
        my $radioButton_drawSimpleExits = Gtk2::RadioToolButton->new_from_widget(
            $radioButton_drawNoExits,
        );
        if ($self->worldModelObj->drawExitMode eq 'simple_exit') {

            $radioButton_drawSimpleExits->set_active(TRUE);
        }
        $radioButton_drawSimpleExits->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_draw_simple.png'),
        );
        $radioButton_drawSimpleExits->set_label('Draw simple exits');
        $radioButton_drawSimpleExits->set_tooltip_text('Draw simple exits');
        $radioButton_drawSimpleExits->signal_connect('toggled' => sub {

            if (! $self->ignoreMenuUpdateFlag && $radioButton_drawSimpleExits->get_active()) {

                $self->worldModelObj->switchMode(
                    'drawExitMode',
                    'simple_exit',      # New value of ->drawExitMode
                    TRUE,               # Do call $self->drawRegion
                    'draw_simple_exits',
                    'icon_draw_simple_exits',
                );
            }
        });
        push (@iconList, $radioButton_drawSimpleExits);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_draw_simple_exits', $radioButton_drawSimpleExits);

        # Radio button for 'draw complex exits' mode
        my $radioButton_drawComplexExits = Gtk2::RadioToolButton->new_from_widget(
            $radioButton_drawSimpleExits,
        );
        if ($self->worldModelObj->drawExitMode eq 'complex_exit') {

            $radioButton_drawComplexExits->set_active(TRUE);
        }
        $radioButton_drawComplexExits->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_draw_complex.png'),
        );
        $radioButton_drawComplexExits->set_label('Draw complex exits');
        $radioButton_drawComplexExits->set_tooltip_text('Draw complex exits');
        $radioButton_drawComplexExits->signal_connect('toggled' => sub {

            if (! $self->ignoreMenuUpdateFlag && $radioButton_drawComplexExits->get_active()) {

                $self->worldModelObj->switchMode(
                    'drawExitMode',
                    'complex_exit',     # New value of ->drawExitMode
                    TRUE,               # Do call $self->drawRegion
                    'draw_complex_exits',
                    'icon_draw_complex_exits',
                );
            }
        });
        push (@iconList, $radioButton_drawComplexExits);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_draw_complex_exits', $radioButton_drawComplexExits);

        # Separator
        my $separator = Gtk2::SeparatorToolItem->new();
        push (@iconList, $separator);

        # Toggle button for 'draw exit ornaments'
        my $toggleButton_drawExitOrnaments = Gtk2::ToggleToolButton->new();
        $toggleButton_drawExitOrnaments->set_active($self->worldModelObj->drawOrnamentsFlag);
        $toggleButton_drawExitOrnaments->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_draw_ornaments.png'),
        );
        $toggleButton_drawExitOrnaments->set_label('Draw exit ornaments');
        $toggleButton_drawExitOrnaments->set_tooltip_text('Draw exit ornaments');
        $toggleButton_drawExitOrnaments->signal_connect('toggled' => sub {

            if (! $self->ignoreMenuUpdateFlag) {

                $self->worldModelObj->toggleFlag(
                    'drawOrnamentsFlag',
                    $toggleButton_drawExitOrnaments->get_active(),
                    TRUE,      # Do call $self->drawRegion
                    'draw_ornaments',
                    'icon_draw_ornaments',
                );
            }
        });
        push (@iconList, $toggleButton_drawExitOrnaments);
        # (Never desensitised)
        $self->ivAdd('menuToolItemHash', 'icon_draw_ornaments', $toggleButton_drawExitOrnaments);

        # Toolbutton for 'no ornament'
        my $toolButton_noOrnament = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_no_ornament.png'),
            'Set no ornament',
        );
        $toolButton_noOrnament->set_tooltip_text('Set no ornament');
        $toolButton_noOrnament->signal_connect('clicked' => sub {

            $self->exitOrnamentCallback(undef);
        });
        push (@iconList, $toolButton_noOrnament);
        # (Requires $self->currentRegionmap & either $self->selectedRoom or
        #   $self->selectedRoomHash)
        $self->ivAdd('menuToolItemHash', 'icon_no_ornament', $toolButton_noOrnament);

        # Separator
        my $separator2 = Gtk2::SeparatorToolItem->new();
        push (@iconList, $separator2);

        # Toolbutton for 'openable exit'
        my $toolButton_openableExit = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_openable_exit.png'),
            'Set openable exit',
        );
        $toolButton_openableExit->set_tooltip_text('Set openable exit');
        $toolButton_openableExit->signal_connect('clicked' => sub {

            $self->exitOrnamentCallback('openFlag');
        });
        push (@iconList, $toolButton_openableExit);
        # (Requires $self->currentRegionmap & either $self->selectedRoom or
        #   $self->selectedRoomHash)
        $self->ivAdd('menuToolItemHash', 'icon_openable_exit', $toolButton_openableExit);

        # Toolbutton for 'lockable exit'
        my $toolButton_lockableExit = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_lockable_exit.png'),
            'Set lockable exit',
        );
        $toolButton_lockableExit->set_tooltip_text('Set lockable exit');
        $toolButton_lockableExit->signal_connect('clicked' => sub {

            $self->exitOrnamentCallback('lockFlag');
        });
        push (@iconList, $toolButton_lockableExit);
        # (Requires $self->currentRegionmap & either $self->selectedRoom or
        #   $self->selectedRoomHash)
        $self->ivAdd('menuToolItemHash', 'icon_lockable_exit', $toolButton_lockableExit);

        # Toolbutton for 'pickable exit'
        my $toolButton_pickableExit = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_pickable_exit.png'),
            'Set pickable exit',
        );
        $toolButton_pickableExit->set_tooltip_text('Set pickable exit');
        $toolButton_pickableExit->signal_connect('clicked' => sub {

            $self->exitOrnamentCallback('pickFlag');
        });
        push (@iconList, $toolButton_pickableExit);
        # (Requires $self->currentRegionmap & either $self->selectedRoom or
        #   $self->selectedRoomHash)
        $self->ivAdd('menuToolItemHash', 'icon_pickable_exit', $toolButton_pickableExit);

        # Toolbutton for 'breakable exit'
        my $toolButton_breakableExit = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_breakable_exit.png'),
            'Set breakable exit',
        );
        $toolButton_breakableExit->set_tooltip_text('Set breakable exit');
        $toolButton_breakableExit->signal_connect('clicked' => sub {

            $self->exitOrnamentCallback('breakFlag');
        });
        push (@iconList, $toolButton_breakableExit);
        # (Requires $self->currentRegionmap & either $self->selectedRoom or
        #   $self->selectedRoomHash)
        $self->ivAdd('menuToolItemHash', 'icon_breakable_exit', $toolButton_breakableExit);

        # Toolbutton for 'impassable exit'
        my $toolButton_impassableExit = Gtk2::ToolButton->new(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_impassable_exit.png'),
            'Set impassable exit',
        );
        $toolButton_impassableExit->set_tooltip_text('Set impassable exit');
        $toolButton_impassableExit->signal_connect('clicked' => sub {

            $self->exitOrnamentCallback('impassFlag');
        });
        push (@iconList, $toolButton_impassableExit);
        # (Requires $self->currentRegionmap & either $self->selectedRoom or
        #   $self->selectedRoomHash)
        $self->ivAdd('menuToolItemHash', 'icon_impassable_exit', $toolButton_impassableExit);

        # Store the buttons created
        $self->ivAdd('toolbarButtonHash', $setNumber, \@iconList);

        return 1;
    }

    sub enableToolbarButtonSet5 {

        # Called by $self->enableToolbar to set up one set of toolbar buttons
        #
        # Expected arguments
        #   $setNumber  - The number of the set (which can vary from the number of this function, if
        #                   we want to change the order in which the sets are displayed)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $setNumber, $check) = @_;

        # Local variables
        my @iconList;

        # Check for improper arguments
        if (! defined $setNumber || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->enableToolbarButtonSet5',
                @_,
            );
        }

        # Toggle button for 'release all filters'
        my $radioButton_releaseAllFilters = Gtk2::ToggleToolButton->new();
        $radioButton_releaseAllFilters->set_active($self->worldModelObj->allRoomFiltersFlag);
        $radioButton_releaseAllFilters->set_icon_widget(
            Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/icon_all_filters.png'),
        );
        $radioButton_releaseAllFilters->set_label('Release all filters');
        $radioButton_releaseAllFilters->set_tooltip_text('Release all filters');
        $radioButton_releaseAllFilters->signal_connect('toggled' => sub {

            if (! $self->ignoreMenuUpdateFlag) {

                $self->worldModelObj->toggleFlag(
                    'allRoomFiltersFlag',
                    $radioButton_releaseAllFilters->get_active(),
                    TRUE,      # Do call $self->drawRegion
                    'release_all_filters',
                    'icon_release_all_filters',
                );
            }
        });
        push (@iconList, $radioButton_releaseAllFilters);
        # (Never desensitised)
        $self->ivAdd(
            'menuToolItemHash',
            'icon_release_all_filters',
            $radioButton_releaseAllFilters,
        );

        # Separator
        my $separator = Gtk2::SeparatorToolItem->new();
        push (@iconList, $separator);

        # Filter icons
        foreach my $filter ($self->worldModelObj->roomFilterList) {

            # Filter button
            my $toolButton_filter = Gtk2::ToggleToolButton->new();
            $toolButton_filter->set_active(
                $self->worldModelObj->ivShow('roomFilterHash', $filter),
            );
            $toolButton_filter->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag) {

                    $self->worldModelObj->toggleFilter(
                        $filter,
                        $toolButton_filter->get_active(),
                    );
                }
            });

            # If it's one of the standard filters, we can use one of the existing icons;
            #   otherwise, use a spare icon
            my $iconFile = $axmud::SHARE_DIR . '/icons/map/icon_' . $filter . '.png';
            if (! -e $iconFile) {

                $iconFile = $axmud::SHARE_DIR . '/icons/map/icon_spare_filter.png'
            }

            $toolButton_filter->set_icon_widget(
                Gtk2::Image->new_from_file($iconFile)
            );

            $toolButton_filter->set_label('Toggle ' . $filter . ' filter');
            $toolButton_filter->set_tooltip_text('Toggle ' . $filter . ' filter');
            push (@iconList, $toolButton_filter);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'icon_' . $filter . '_filter', $toolButton_filter);
        }

        # Store the buttons created
        $self->ivAdd('toolbarButtonHash', $setNumber, \@iconList);

        return 1;
    }

    sub enableToolbarButtonSet6 {

        # Called by $self->enableToolbar to set up one set of toolbar buttons
        #
        # Expected arguments
        #   $setNumber  - The number of the set (which can vary from the number of this function, if
        #                   we want to change the order in which the sets are displayed)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $setNumber, $check) = @_;

        # Local variables
        my (
            $lastButton,
            @initList, @interiorList, @iconList,
            %interiorHash, %iconHash,
        );

        # Check for improper arguments
        if (! defined $setNumber || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->enableToolbarButtonSet6',
                @_,
            );
        }

        @initList = (
            'none',
                'Don\'t draw interior counts',
                'icon_no_counts.png',
            'shadow_count',
                'Draw shadow/unallocated exits',
                'icon_draw_shadow.png',
            'region_count',
                'Draw region/super-region exits',
                'icon_draw_super.png',
            'room_content',
                'Draw room contents',
                'icon_draw_contents.png',
            'hidden_count',
                'Draw hidden contents',
                'icon_draw_hidden.png',
            'temp_count',
                'Draw temporary contents',
                'icon_draw_temp.png',
            'word_count',
                'Draw recognised words',
                'icon_draw_words.png',
            'room_flag',
                'Draw room flag text',
                'icon_draw_flags.png',
            'visit_count',
                'Draw character visits',
                'icon_draw_visits.png',
            'profile_count',
                'Draw exclusive profiles',
                'icon_draw_exclusive.png',
            'title_descrip',
                'Draw titles/descriptions',
                'icon_draw_descrips.png',
            'exit_pattern',
                'Draw exit patterns',
                'icon_draw_patterns.png',
            'source_code',
                'Draw room source code',
                'icon_draw_code.png',
            'vnum',
                'Draw world\'s room _vnum',
                'icon_draw_vnum.png',
        );

        do {

            my ($mode, $descrip, $icon);

            $mode = shift @initList;
            $descrip = shift @initList;
            $icon = shift @initList;

            push (@interiorList, $mode);
            $interiorHash{$mode} = $descrip;
            $iconHash{$mode} = $icon;

        } until (! @initList);

        for (my $count = 0; $count < (scalar @interiorList); $count++) {

            my ($icon, $mode);

            $mode = $interiorList[$count];

            # (For $count = 0, $buttonGroup is 'undef')
            my $radioButton;
            if ($mode eq 'none') {
                $radioButton = Gtk2::RadioToolButton->new(undef);
            } else {
                $radioButton = Gtk2::RadioToolButton->new_from_widget($lastButton);
            }

            if ($self->worldModelObj->roomInteriorMode eq $mode) {

                $radioButton->set_active(TRUE);
            }
            $radioButton->set_icon_widget(
                Gtk2::Image->new_from_file($axmud::SHARE_DIR . '/icons/map/' . $iconHash{$mode}),
            );
            $radioButton->set_label($interiorHash{$mode});
            $radioButton->set_tooltip_text($interiorHash{$mode});

            $radioButton->signal_connect('toggled' => sub {

                if (! $self->ignoreMenuUpdateFlag && $radioButton->get_active()) {

                    $self->worldModelObj->switchRoomInteriorMode($mode);
                }
            });
            push (@iconList, $radioButton);
            # (Never desensitised)
            $self->ivAdd('menuToolItemHash', 'icon_interior_mode_' . $mode, $radioButton);

            $lastButton = $radioButton;

            # (Add a separator after the first toolbar button)
            if ($mode eq 'none') {

                # Separator
                my $separator = Gtk2::SeparatorToolItem->new();
                push (@iconList, $separator);
            }
        }

        # Store the buttons created
        $self->ivAdd('toolbarButtonHash', $setNumber, \@iconList);

        return 1;
    }

    # Treeview widget methods

    sub enableTreeView {

        # Called by $self->drawWidgets
        # Sets up the Automapper window's treeview widget
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::ScrolledWindow containing the Gtk2::TreeView created

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableTreeView', @_);
        }

        # Create the treeview
        my $objectModel = Gtk2::TreeStore->new('Glib::String');
        my $treeView = Gtk2::TreeView->new($objectModel);
        if (! $objectModel || ! $treeView) {

            return undef;
        }

        # Append a single column to the treeview
        $treeView->append_column(
            Gtk2::TreeViewColumn->new_with_attributes(
                'Regions',
                Gtk2::CellRendererText->new,
                text => 0,
            )
        );

        # Make the treeview scrollable
        my $treeViewScroller = Gtk2::ScrolledWindow->new;
        $treeViewScroller->add($treeView);
        $treeViewScroller->set_policy(qw/automatic automatic/);

        # Make the branches of the list tree clickable, so the rows can be expanded and collapsed
        $treeView->signal_connect('row_activated' => sub {

            my ($treeView, $path, $column) = @_;

            $self->treeViewRowActivatedCallback();
        });

        $treeView->get_selection->set_mode('browse');
        $treeView->get_selection->signal_connect('changed' => sub {

            my ($selection) = @_;

            $self->treeViewRowChangedCallback($selection);
        });

        # Respond when the user expands/collapses rows
        $treeView->signal_connect('row_expanded' => sub {

            my ($widget, $iter, $path) = @_;

            $self->treeViewRowExpandedCallback($iter);
        });
        $treeView->signal_connect('row_collapsed' => sub {

            my ($widget, $iter, $path) = @_;

            $self->treeViewRowCollapsedCallback($iter);
        });

        # Store the widgets
        $self->ivPoke('treeView', $treeView);
        $self->ivPoke('treeViewScroller', $treeViewScroller);
        $self->ivPoke('treeViewModel', $objectModel);

        # Fill the tree with a list of regions
        $self->resetTreeView();

        # Setup complete
        return $treeViewScroller;
    }

    sub resetTreeView {

        # Called by $self->winEnable and various other functions
        # Fills the object tree on the left of the Automapper window, listing all the regions in
        #   the current world model
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $expandRegion   - If specified, this function makes sure the object tree is expanded to
        #                       make the specified region visible. $expandRegion is the region's
        #                       name
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $expandRegion, $check) = @_;

        # Local variables
        my (
            $model, $count, $firstRegionObj,
            @initList, @modList, @sortedList, @childList,
            %pointerHash, %regionHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->resetTreeView', @_);
        }

        # Fill a model of the tree, not the tree itself
        $model = $self->treeView->get_model();
        $model->clear();

        # Import the list of regions
        @initList = $self->worldModelObj->ivValues('regionModelHash');

        # If one region is supposed to be at the top of the list, remove it from @initList
        if ($self->worldModelObj->firstRegion) {

            foreach my $regionObj (@initList) {

                if ($regionObj->name ne $self->worldModelObj->firstRegion) {

                    push (@modList, $regionObj);

                } else {

                    $firstRegionObj = $regionObj;
                }
            }

        } else {

            @modList = @initList;
        }

        # Get a sorted list of regions (use the lc() function so that capitalised region names
        #   don't appear first, followed by lower case region names)
        # NB If the flag is set to TRUE, the regions are shown in reverse alphabetical order
        if ($self->worldModelObj->reverseRegionListFlag) {

            # Reverse order
            @sortedList = sort {lc($b->name) cmp lc($a->name)} (@modList);

        } else {

            # Normal order
            @sortedList = sort {lc($a->name) cmp lc($b->name)} (@modList);
        }

        # If there is supposed to be a specific region at the top of the list, move it there
        if ($firstRegionObj) {

            unshift (@sortedList, $firstRegionObj);
        }

        # Import the hash which records the rows that have been expanded (and not then collapsed),
        #   before emptying it, ready for re-filling
        %regionHash = $self->treeViewRegionHash;
        $self->ivEmpty('treeViewRegionHash');

        # We need to add parent regions to the treeview before we add any child regions. Go through
        #   the list, removing regions that have no parent, and adding them to the treeview
        foreach my $regionObj (@sortedList) {

            my $pointer;

            # Each row containing a region is, by default, not expanded
            $self->ivAdd('treeViewRegionHash', $regionObj->name, 0);

            if ($regionObj->parent) {

                # This is a child region; add it to the treeview later
                push (@childList, $regionObj);

            } else {

                # Add this region to the treeview now
                $pointer = $model->append(undef);
                $model->set($pointer, 0 => $regionObj->name);

                # Store $pointer in a hash, so that if this region has any child regions, they can
                #   be added directly below in the treeview
                $pointerHash{$regionObj->name} = $pointer;
            }
        }

        # Now, if there are any child regions, add them to the treeview just below their parent
        #   regions. Do this operation recursively until there are no regions left
        do {

            my (
                @grandChildList,
                %newPointerHash,
            );

            $count = 0;

            foreach my $regionObj (@childList) {

                my ($parentObj, $pointer, $childPointer);

                $parentObj = $self->worldModelObj->ivShow('modelHash', $regionObj->parent);
                if (! exists $pointerHash{$parentObj->name}) {

                    # This region's parent hasn't been added to the treeview yet; add it later
                    push (@grandChildList, $regionObj);

                } else {

                    $count++;

                    # Add this region to the treeview, just below its parent
                    $pointer = $pointerHash{$parentObj->name};
                    $childPointer = $model->append($pointer);
                    $model->set($childPointer, 0 => $regionObj->name);

                    # Store $childPointer in a hash, so that if this region has any child regions,
                    #   they can be added directly below in the treeview
                    # (Don't add it to %pointerHash until the end of this loop iteration, otherwise
                    #   some regions won't appear in alphabetical order in the treeview)
                    $newPointerHash{$regionObj->name} = $childPointer;
                }
            }

            # All regions that were added in this loop must be moved from %newPointerHash to
            #   %pointerHash
            foreach my $key (keys %newPointerHash) {

                $pointerHash{$key} = $newPointerHash{$key};
            }

            %newPointerHash = ();

            # If there is anything in @grandChildList, they must be processed on the next iteration
            @childList = @grandChildList;

        } until (! @childList || ! $count);

        # If @childList still contains any regions, their parent(s) are either not regions (this
        #   should never happen), or the regions don't exist any more (ditto)
        # Display them at the end of the treeview
        foreach my $regionObj (@childList) {

            my $pointer;

            # Add this region to the treeview now
            $pointer = $model->append(undef);
            $model->set($pointer, 0 => $regionObj->name);
        }

        # Now expand any of the rows that were expanded before the call to this function
        if (%regionHash) {

            foreach my $regionName (keys %regionHash) {

                my $path;

                if (
                    $regionHash{$regionName}
                    && $self->ivExists('treeViewRegionHash', $regionName)
                ) {
                    # This row must be expanded
                    $path = $model->get_path($pointerHash{$regionName});
                    $self->treeView->expand_row($path, FALSE);
                    # Mark it as expanded
                    $self->ivAdd('treeViewRegionHash', $regionName, TRUE);
                }
            }
        }

        # Store the hash of pointers ($self->treeViewSelectLine needs them)
        $self->ivPoke('treeViewPointerHash', %pointerHash);

        # If a specific region was specified as $expandRegion, it must be visible (we must expand
        #   all its parents, if not already expanded)
        if ($expandRegion) {

            $self->expandTreeView($model, $expandRegion);
        }

        # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
        $self->restrictWidgets();

        # Operation complete
        return 1;
    }

    sub expandTreeView {

        # Called by $self->resetTreeView and by this function, recursively
        # Expands rows in the tree model, to make sure that a certain region is visible.
        #
        # Expected arguments
        #   $model          - The treeview model (a Gtk2::TreeModel object)
        #   $expandRegion   - The name of a region that should be visible. This function expands
        #                       the row belonging to $expandRegion's parent (if any), then calls
        #                       this function recursively, to expand the row for the parent's
        #                       parent (if any)
        #
        # Return values
        #   'undef' on improper arguments or if no further expansions are required
        #   1 otherwise

        my ($self, $model, $expandRegion, $check) = @_;

        # Local variables
        my ($expandNum, $expandObj, $parentObj, $pointer, $path);

        # Check for improper arguments
        if (! defined $model || ! defined $expandRegion || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->expandTreeView', @_);
        }

        # Find the corresponding world model object
        $expandNum = $self->findRegion($expandRegion);
        if ($expandNum) {

            $expandObj = $self->worldModelObj->ivShow('modelHash', $expandNum);
        }

        if (! $expandObj || ! $expandObj->parent) {

            # No further expansions required
            return undef;
        }

        # Get the parent object's name
        $parentObj = $self->worldModelObj->ivShow('modelHash', $expandObj->parent);

        # Expand the parent's row (if it's not already expanded)
        if (
            $self->ivExists('treeViewRegionHash', $parentObj->name)
            && ! $self->ivShow('treeViewRegionHash', $parentObj->name)
        ) {
            # This row must be expanded
            $pointer = $self->ivShow('treeViewPointerHash', $parentObj->name);
            $path = $model->get_path($pointer);
            $self->treeView->expand_row($path, TRUE);
            # Mark it as expanded
            $self->ivAdd('treeViewRegionHash', $parentObj->name, TRUE);
        }

        # Call this function recursively, to expand the parent's parent (if it has one)
        $self->expandTreeView($model, $parentObj->name);

        return 1;
    }

    sub treeViewRowActivatedCallback {

        # Treeview's 'row_activated' callback - called when the user double-clicks on one of the
        #   treeview's cells
        # Called from an anonymous sub in $self->enableTreeView
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $regionName;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->treeViewRowActivatedCallback',
                @_,
            );
        }

        # Don't do anything if the canvas is currently invisible
        if ($self->worldModelObj->showCanvasFlag) {

            # Get the selected region
            $regionName = $self->treeViewSelectedLine;

            # Find the regionmap matching the selected region
            if ($regionName && $self->worldModelObj->ivExists('regionmapHash', $regionName)) {

                # Make it the selected region, and draw it on the map
                $self->setCurrentRegion($regionName);
            }
        }

        # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
        $self->restrictWidgets();

        return 1;
    }

    sub treeViewRowChangedCallback {

        # Treeview's 'changed' callback - called when the user single-clicks on one of the
        #   treeview's cells
        # Called from an anonymous sub in $self->enableTreeView
        #
        # Expected arguments
        #   $selection  - A Gtk2::Selection
        #
        # Return values
        #   'undef' on improper arguments or if the selection is not recognised
        #   1 otherwise

        my ($self, $selection, $check) = @_;

        # Local variables
        my ($model, $iter, $region);

        # Check for improper arguments
        if (! defined $selection || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->treeViewRowChangedCallback',
                @_,
            );
        }

        ($model, $iter) = $selection->get_selected();
        if (! $iter) {

            return undef;

        } else {

            # Get the region on the selected line
            $region = $model->get($iter, 0);
            # Store it, so that other methods can access the region on the selected line
            $self->ivPoke('treeViewSelectedLine', $region);
            # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
            $self->restrictWidgets();

            return 1;
        }
    }

    sub treeViewRowExpandedCallback {

        # Treeview's 'row_expanded' callback - called when the user expands one of the treeview's
        #   rows to reveal a region's child regions
        # Called from an anonymous sub in $self->enableTreeView
        #
        # Expected arguments
        #   $iter       - A Gtk2::TreeIter
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $iter, $check) = @_;

        # Local variables
        my $region;

        # Check for improper arguments
        if (! defined $iter || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->treeViewRowExpandedCallback',
                @_,
            );
        }

        # Get the region in the expanded row
        $region = $self->treeViewModel->get($iter, 0);

        # Mark the row as expanded
        $self->ivAdd('treeViewRegionHash', $region, TRUE);

        return 1;
    }

    sub treeViewRowCollapsedCallback {

        # Treeview's 'row_collapsed' callback - called when the user collapses one of the treeview's
        #   rows to hide a region's child regions
        # Called from an anonymous sub in $self->enableTreeView
        #
        # Expected arguments
        #   $iter       - A Gtk2::TreeIter
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $iter, $check) = @_;

        # Local variables
        my $region;

        # Check for improper arguments
        if (! defined $iter || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->treeViewRowCollapsedCallback',
                @_,
            );
        }

        # Get the region in the collapsed row
        $region = $self->treeViewModel->get($iter, 0);

        # Mark the row as collapsed
        $self->ivAdd('treeViewRegionHash', $region, FALSE);

        return 1;
    }

    sub treeViewSelectLine {

        # Called by $self->setCurrentRegion when the current region is set (or unset)
        # Makes sure that, if there's a new current region, it is the one highlighted in the
        #   treeview's list
        #
        # Expected arguments
        #   $region     - The name of the highlighted region (matches $self->currentRegionmap->name)
        #                   - if not specified, there is no current region
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $region, $check) = @_;

        # Local variables
        my ($pointer, $path);

        # Check for improper arguments
        if (! defined $region || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->treeViewSelectLine', @_);
        }

        # Highlight the region named $region
        $pointer = $self->ivShow('treeViewPointerHash', $region);
        $path = $self->treeViewModel->get_path($pointer);

        # If the new region has a parent, we need to expand the parent so that the new region is
        #   visible, once highlighted
        if ($path->up()) {

            $self->treeView->expand_to_path($path);
            # Reset the path to the region we want to highlight
            $path = $self->treeViewModel->get_path($pointer);
        }

        # Highlight the region
        $self->treeView->set_cursor($path);

        return 1;
    }

    # Canvas widget methods

    sub enableCanvas {

        # Called by $self->drawWidgets
        # Sets up the Automapper window's canvas widget
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the widget can't be created
        #   Otherwise returns the Gtk2::Frame containing the Gtk2::Canvas created

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->enableCanvas', @_);
        }

        # Create a frame
        my $canvasFrame = Gtk2::Frame->new(undef);
        $canvasFrame->set_border_width(3);

        # Create a scrolled window
        my $canvasScroller = Gtk2::ScrolledWindow->new();
        my $canvasHAdjustment = $canvasScroller->get_hadjustment();
        my $canvasVAdjustment = $canvasScroller->get_vadjustment();
        $canvasScroller->set_border_width(3);
        # Set the scrolling policy
        $canvasScroller->set_policy('always','always');

        # Create the canvas and store it as an IV (immediately)
        my $canvas = Gnome2::Canvas->new();
        $self->ivPoke('canvas', $canvas);
        # Get the canvas group (root) and store it immediately
        my $canvasRoot = $canvas->root();
        $self->ivPoke('canvasRoot', $canvasRoot);

        # Set the default canvas size
        $canvas->set_scroll_region(
            0,
            0,
            $self->worldModelObj->defaultMapWidthPixels,
            $self->worldModelObj->defaultMapHeightPixels,
        );
        $canvas->set_center_scroll_region(1);

        # Add the canvas to the scrolled window
        $canvasScroller->add($canvas);
        # Add the scrolled window to the frame
        $canvasFrame->add($canvasScroller);

        # Handle mouse button scrolls from here, though
        $canvas->signal_connect('scroll-event' => sub {

            my ($widget, $event) = @_;

            if ($event->direction eq 'up') {

                # Zoom in
                $self->zoomCallback('in');

            } elsif ($event->direction eq 'down') {

                # Zoom out
                $self->zoomCallback('out');
            }
        });

        # Create a Gtk2::Window to act as a tooltip, being visible (or not) as appropriate
        my $tooltipLabel = Gtk2::Label->new();
        my $tooltipWin = Gtk2::Window->new('popup');
        $tooltipWin->set_decorated(FALSE);
        $tooltipWin->set_position('mouse');
        $tooltipWin->set_border_width(2);
        $tooltipWin->modify_fg('normal', Gtk2::Gdk::Color->parse('black'));
        $tooltipWin->modify_bg('normal', Gtk2::Gdk::Color->parse('yellow'));
        $tooltipWin->add($tooltipLabel);

        # Draw the background in the no-map colour (default is white)
        $self->resetMap(FALSE);

        # Store the remaining widgets
        $self->ivPoke('canvasFrame', $canvasFrame);
        $self->ivPoke('canvasScroller', $canvasScroller);
        $self->ivPoke('canvasHAdjustment', $canvasHAdjustment);
        $self->ivPoke('canvasVAdjustment', $canvasVAdjustment);
        $self->ivPoke('canvasTooltipObj', undef);
        $self->ivPoke('canvasTooltipObjType', undef);
        $self->ivPoke('canvasTooltipFlag', FALSE);

        # Setup complete
        return $canvasFrame;
    }

    sub setMapPosn {

        # Can be called by anything
        # Sets the position of the canvas scrollbars, revealing a portion of the map
        #
        # Expected arguments
        #   $xPos   - Value between 0 (far left) and 1 (far right)
        #   $yPos   - Value between 0 (far top) and 1 (far bottom)
        #
        # Return values
        #   'undef' on improper arguments or if there is no current regionmap
        #   1 otherwise

        my ($self, $xPos, $yPos, $check) = @_;

        # Local variables
        my ($xLow, $xHigh, $xLength, $newXPos, $yLow, $yHigh, $yLength, $newYPos);

        # Check for improper arguments
        if (! defined $xPos || ! defined $yPos || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setMapPosn', @_);
        }

        # Do nothing if there is no current regionmap
        if (! $self->currentRegionmap) {

            return undef;
        }

        # This line is needed because, during a zoom, we have to wait for the scrollbars'
        #   properties (e.g. $self->canvasHAdjustment->lower) to be set to their proper values
        # Update Gtk2's events queue
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->setMapPosn');

        # Get the lowest and highest horizontal positions of the horizontal scrollbar, and the
        #   distance between them
        $xLow = $self->canvasHAdjustment->lower;
        $xHigh = ($self->canvasHAdjustment->upper - $self->canvasHAdjustment->page_size);
        $xLength = $xHigh - $xLow;
        # Set the new position of the horizontal scrollbar
        $newXPos = $xLow + ($xLength * $xPos);

        # Get the lowest and highest vertical positions of the vertical scrollbar, and the distance
        #   between them
        $yLow = $self->canvasVAdjustment->lower;
        $yHigh = ($self->canvasVAdjustment->upper - $self->canvasVAdjustment->page_size);
        $yLength = $yHigh - $yLow;
        # Set the new position of the vertical scrollbar
        $newYPos = $yLow + ($yLength * $yPos);

        # Move the scrollbars
        $self->canvasHAdjustment->set_value($newXPos);
        $self->canvasVAdjustment->set_value($newYPos);

        return 1;
    }

    sub getMapPosn {

        # Can be called by anything
        # Gets the position and size of canvas scrollbars, expressed as values between 0 and 1
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return arguments
        #   An empty list on improper arguments or if there is no current regionmap
        #   Otherwise, a list in the form ($xOffset, $yOffset, $xPos, $yPos, $width, $height):
        #       $xOffset    - Position of the horizontal scrollbar. 0 - as far left as possible,
        #                       1 - as far right as possible
        #       $yOffset    - Position of the vertical scrollbar. 0 - as far up as possible,
        #                       1 - as down right as possible
        #       $xPos       - Position of the left end of the visible portion of the map. 0 - left
        #                       edge is visible, 0.5 - middle of the map is visible on the left
        #                       border, etc
        #       $yPos       - Position of the top end of the visible portion of the map. 0 - top
        #                       edge is visible, 0.5 - middle of the map is visible on the top
        #                       border, etc
        #       $width      - Width of the currently visible portion of the map. 1 - total width of
        #                       map is visible; 0.5 - 50% of the total width of the map is visible,
        #                       0.1 - 10% of the total width of the map is visible (etc)
        #       $height     - Height of the currently visible portion of the map. 1 - total height
        #                       of map is visible; 0.5 - 50% of the total height of the map is
        #                       visible, 0.1 - 10% of the total height of the map is visible (etc)

        my ($self, $check) = @_;

        # Local variables
        my (
            $xOffset, $yOffset, $xPos, $yPos, $width, $height,
            @emptyList,
        );

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->getMapPosn', @_);
            return @emptyList;
        }

        # Do nothing if there is no current regionmap
        if (! $self->currentRegionmap) {

            return @emptyList;
        }

        # Get the position of the horizontal scrollbar (a value between 0 and 1)
        if ($self->canvasHAdjustment->upper == $self->canvasHAdjustment->page_size) {

            $xOffset = 0;

        } else {

            $xOffset = $self->canvasHAdjustment->value
                / ($self->canvasHAdjustment->upper - $self->canvasHAdjustment->page_size);
        }

        # Get the position of the vertical scrollbar (a value between 0 and 1)
        if ($self->canvasVAdjustment->upper == $self->canvasVAdjustment->page_size) {

            $yOffset = 0;

        } else {

            $yOffset = $self->canvasVAdjustment->value
                / ($self->canvasVAdjustment->upper - $self->canvasVAdjustment->page_size);
        }

        # Get the position of the left end of the visible portion of the map (a value between
        #   0 and 1)
        $xPos = $self->canvasHAdjustment->value / $self->canvasHAdjustment->upper;
        # Get the position of the top end of the visible portion of the map (a value between
        #   0 and 1)
        $yPos = $self->canvasVAdjustment->value / $self->canvasVAdjustment->upper;

        # Get the size of the horizontal scrollbar (a value between 0 and 1)
        $width = $self->canvasHAdjustment->page_size / $self->canvasHAdjustment->upper;
        # Get the size of the horizontal scrollbar (a value between 0 and 1)
        $height = $self->canvasVAdjustment->page_size / $self->canvasVAdjustment->upper;

        return ($xOffset, $yOffset, $xPos, $yPos, $width, $height);
    }

    sub getMapPosnInBlocks {

        # Can be called by anything (e.g. by ->trackPosn)
        # Converts the output of $self->getMapPosn (a list of six values, all in the range 0-1)
        #   into the position and size of the visible map, measured in gridblocks
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return arguments
        #   An empty list on improper arguments, or if there is no current regionmap
        #   Otherwise, a list in the form ($xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks):
        #       $xPosBlocks, $yPosBlocks
        #           - The grid coordinates of top-left corner of the visible portion of the map
        #       $widthBlocks, $heightBlocks
        #           - The size of the visible map, in gridblocks

        my ($self, $check) = @_;

        # Local variables
        my (
            $xOffsetRatio, $yOffsetRatio, $xPosRatio, $yPosRatio, $widthRatio, $heightRatio,
            $xOffsetBlocks, $yOffsetBlocks, $xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks,
            @emptyList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->getMapPosnInBlocks', @_);
        }

        # Do nothing if there is no current regionmap
        if (! $self->currentRegionmap) {

            return @emptyList;
        }

        # Get the size and position of the visible map. The return values of $self->getMapPosn are
        #   all values in the range 0-1
        ($xOffsetRatio, $yOffsetRatio, $xPosRatio, $yPosRatio, $widthRatio, $heightRatio)
            = $self->getMapPosn();

        # Convert these values into gridblocks (we don't need $xOffsetRatio or $yOffsetRatio)
        $xPosBlocks = int ($xPosRatio * $self->currentRegionmap->gridWidthBlocks);
        $yPosBlocks = int ($yPosRatio * $self->currentRegionmap->gridHeightBlocks);
        $widthBlocks = POSIX::ceil($widthRatio * $self->currentRegionmap->gridWidthBlocks);
        $heightBlocks = POSIX::ceil($heightRatio * $self->currentRegionmap->gridHeightBlocks);

        return ($xPosBlocks, $yPosBlocks, $widthBlocks, $heightBlocks);
    }

    sub centreMapOverRoom {

        # Can be called by anything
        # Centres the map over a specified room, as far as possible (if the room is near map's
        #   edges, the map will be centred as close as possible to the room)
        # If the specified room isn't in the current region, a new current region is set
        # Alternatively, instead of specifying a room, the calling function can specify a gridblock;
        #   the map is centred over that gridblock, even if it doesn't contain a room
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $roomObj    - The room over which to centre the map. If specified, the remaining
        #                   arguments are ignored
        #   $xPosBlocks, $yPosBlocks
        #               - A gridblock on the map, in the current region, on the current level
        #                   (ignored if $roomObj is specified)
        #
        # Return values
        #   'undef' on improper arguments or if no arguments are specified at all
        #   1 otherwise

        my ($self, $roomObj, $xPosBlocks, $yPosBlocks, $check) = @_;

        # Local variables
        my (
            $regionObj, $width, $height, $blockCentreXPosPixels, $blockCentreYPosPixels,
            $blockCentreXPos, $blockCentreYPos, $newVisibleCentreXPos, $newVisibleCentreYPos,
            $newVisibleCornerXPos, $newVisibleCornerYPos, $scrollBarXPos, $scrollBarYPos,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->centreMapOverRoom', @_);
        }

        # If a room was specified...
        if ($roomObj) {

            # Check that the specified room is in the current region
            if ($roomObj->parent && $self->currentRegionmap) {

                if ($roomObj->parent != $self->currentRegionmap->number) {

                    $regionObj = $self->worldModelObj->ivShow('modelHash', $roomObj->parent);
                    # Change the current region to the one containing the specified room
                    $self->setCurrentRegion($regionObj->name);
                }

                # Set the right level
                if ($self->currentRegionmap->currentLevel != $roomObj->zPosBlocks) {

                    $self->setCurrentLevel($roomObj->zPosBlocks);
                }
            }

            $xPosBlocks = $roomObj->xPosBlocks;
            $yPosBlocks = $roomObj->yPosBlocks;

        } elsif (! defined $xPosBlocks || ! defined $yPosBlocks) {

            # Can't do anything without arguments
            return undef;
        }

        # Get the size of the horizontal scrollbar (a value between 0 and 1)
        $width = $self->canvasHAdjustment->page_size / $self->canvasHAdjustment->upper;
        # Get the size of the horizontal scrollbar (a value between 0 and 1)
        $height = $self->canvasVAdjustment->page_size / $self->canvasVAdjustment->upper;

        # Get the centre of a hypothetical visible window, exactly the same size as the current one,
        #   as if it were centred exactly over the gridblock (values between 0 and 1)
        ($blockCentreXPosPixels, $blockCentreYPosPixels) = $self->getBlockCentre(
            $xPosBlocks,
            $yPosBlocks,
        );

        $blockCentreXPos = $blockCentreXPosPixels / $self->currentRegionmap->mapWidthPixels;
        $blockCentreYPos = $blockCentreYPosPixels / $self->currentRegionmap->mapHeightPixels;

        # If any part of this hypothetical visible window is outside the map, correct its position,
        #   so that the whole visible window is inside the map (as close to the room as possible)
        if ($blockCentreXPos < ($width / 2)) {
            $newVisibleCentreXPos = ($width / 2);
        } elsif ($blockCentreXPos > (1 - ($width / 2))) {
            $newVisibleCentreXPos = (1 - ($width / 2));
        } else {
            $newVisibleCentreXPos = $blockCentreXPos;
        }

        if ($blockCentreYPos < ($height / 2)) {
            $newVisibleCentreYPos = ($height / 2);
        } elsif ($blockCentreYPos > (1 - ($height / 2))) {
            $newVisibleCentreYPos = (1 - ($height / 2));
        } else {
            $newVisibleCentreYPos = $blockCentreYPos;
        }

        # Get the coordinates of the top-left corner of the new visible window (values between 0
        #   and 1)
        $newVisibleCornerXPos = $newVisibleCentreXPos - ($width / 2);
        $newVisibleCornerYPos = $newVisibleCentreYPos - ($height / 2);

        # Get the position that the scrollbars have to be in, in order to be showing this new
        #   visible window (values in the range 0 to 1)
        if ($width == 1) {
            $scrollBarXPos = 0;
        } else {
            $scrollBarXPos = $newVisibleCornerXPos / (1 - $width);
        }

        if ($height == 1) {
            $scrollBarYPos = 0;
        } else {
            $scrollBarYPos = $newVisibleCornerYPos / (1 - $height);
        }

        # Correct for rounding errors when the new visible window is touching the edge of the map
        if ($scrollBarXPos < 0.005) {
            $scrollBarXPos = 0;
        } elsif ($scrollBarXPos > 0.995) {
            $scrollBarXPos = 1;
        }

        if ($scrollBarYPos < 0.005) {
            $scrollBarYPos = 0;
        } elsif ($scrollBarXPos > 0.995) {
            $scrollBarXPos = 1;
        }

        # Move the scrollbars to this position, thus centreing the map as close as possible to the
        #   gridblock
        $self->setMapPosn($scrollBarXPos, $scrollBarYPos);

        return 1;
    }

    sub doZoom {

        # Called by $self->worldModelObj->setMagnification
        # Zooms the map in or out, depending on the new value of $self->regionmapObj->magnification
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if no arguments are specified at all
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            @redrawList,
            %newHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->doZoom', @_);
        }

        # Set the visible map's size. The Gnome2::Canvas automatically takes care of its position,
        #   so that the same part of the visible map is at the centre
        $self->canvas->set_pixels_per_unit($self->currentRegionmap->magnification);

        # Gnome2::Canvas helpfully redraws text after a zoom, so it's the same size it was before.
        #   Therefore, we have to redraw any rooms with interior text/room tags/room guilds, and
        #   any exits with exit tags, and also all labels (but only if they're on the current level)
        # Create a list of objects that must be redrawn. Use %newHash to make sure the same room
        #   isn't drawn more than once
        foreach my $posn ($self->ivKeys('drawnRoomTagHash')) {

            my $roomNum = $self->currentRegionmap->fetchRoom(split(/_/, $posn));
            if ($roomNum && ! exists $newHash{$roomNum}) {

                push(@redrawList, 'room', $self->worldModelObj->ivShow('modelHash', $roomNum));
                $newHash{$roomNum} = undef;
            }
        }

        foreach my $posn ($self->ivKeys('drawnRoomGuildHash')) {

            my $roomNum = $self->currentRegionmap->fetchRoom(split(/_/, $posn));
            if ($roomNum && ! exists $newHash{$roomNum}) {

                push(@redrawList, 'room', $self->worldModelObj->ivShow('modelHash', $roomNum));
                $newHash{$roomNum} = undef;
            }
        }

        foreach my $posn ($self->ivKeys('drawnRoomTextHash')) {

            my $roomNum = $self->currentRegionmap->fetchRoom(split(/_/, $posn));
            if ($roomNum && ! exists $newHash{$roomNum}) {

                push(@redrawList, 'room', $self->worldModelObj->ivShow('modelHash', $roomNum));
                $newHash{$roomNum} = undef;
            }
        }

        foreach my $number ($self->ivKeys('drawnExitTagHash')) {

            if ($self->currentRegionmap->ivExists('gridExitTagHash', $number)) {

                push (@redrawList, 'exit', $self->worldModelObj->ivShow('exitModelHash', $number));
            }
        }

        foreach my $number ($self->ivKeys('drawnLabelHash')) {

            my $labelObj = $self->currentRegionmap->ivShow('gridLabelHash', $number);
            if ($labelObj) {

                push (@redrawList, 'label', $labelObj);
            }
        }

        # Redraw any rooms or labels that must be redrawn, to get text the right size
        if (@redrawList) {

            $self->doDraw(@redrawList);
        }

        # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
        $self->restrictWidgets();

        return 1;
    }

    # Menu bar/toolbar widget sensitisers

    sub restrictWidgets {

        # Many menu bar and toolbar items can be sensitised, or desensitised, depending on
        #   conditions
        # This function can be called by anything, any time one of those conditions changes, so that
        #   every menu bar/toolbar item can be sensitised or desensitised correctly
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $regionObj,
            @list, @sensitiseList, @desensitiseList, @magList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->restrictWidgets', @_);
        }

        # Modified v1.0.150 - anything that requires the current regionmap, also requires
        #   the character to be logged in (with a handful of exceptions)
        # Modified v1.0.363 - we now allow zooming and a few other things from the 'View' menu
        #   when the character isn't logged in

        # Menu items that require a current regionmap AND a logged in character
        @list = (
            'select_all', 'icon_select_all',
            'select', 'unselect_all',
            'set_follow_mode', 'icon_set_follow_mode',
#            'zoom_sub',
#            'level_sub',
#            'centre_map_middle_grid', 'icon_centre_map_middle_grid',
#            'centre_map_sub',
            'screenshots', 'icon_visible_screenshot',
            'drag_mode', 'icon_drag_mode',
            'edit_region',
            'edit_regionmap',
            'edit_region_features',
            'recalculate_paths',
            'exit_tags',
            'find_exits',
            'uncertain_exits',
            'exit_lengths', 'icon_exit_lengths',
            'empty_region',
            'delete_region',
            'add_room',
            'add_label_at_click',
            'add_label_at_block',
            'room_text',
            'room_exclusivity',
            'select_label',
#            'move_up_level', 'icon_move_up_level',
#            'move_down_level', 'icon_move_down_level',
            'report_region',
            'report_visits_2',
            'report_guilds_2',
            'report_flags_2',
            'report_flags_4',
            'report_rooms_2',
            'report_exits_2',
            'reset_locator', 'icon_reset_locator',
        );

        if ($self->currentRegionmap && $self->session->loginFlag) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap BUT NOT a logged in character
        @list = (
            'zoom_sub',
            'level_sub',
            'centre_map_middle_grid', 'icon_centre_map_middle_grid',
            'centre_map_sub',
            'move_up_level', 'icon_move_up_level',
            'move_down_level', 'icon_move_down_level',
        );

        if ($self->currentRegionmap) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap, GA::Obj::WorldModel->disableUpdateModeFlag
        #   set to FALSE and a session not in 'connect offline' mode
        @list = (
            'set_update_mode', 'icon_set_update_mode',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && ! $self->worldModelObj->disableUpdateModeFlag
            && $self->session->status ne 'offline'
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap for a region that doesn't have a parent region
        @list = (
            'move_region_top',
        );

        if ($self->currentRegionmap) {

            $regionObj
                = $self->worldModelObj->ivShow('regionModelHash', $self->currentRegionmap->number);
        }

        if ($regionObj && ! $regionObj->parent) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a current room
        @list = (
            'centre_map_current_room', 'icon_centre_map_current_room',
            'add_room_contents',
            'add_hidden_object',
            'add_search_result',
            'unset_current_room',
            'update_locator',
            'repaint_current',
            'execute_scripts',
            'add_failed_room',
            'add_involuntary_exit',
            'add_repulse_exit',
            'add_special_depart',
        );

        if ($self->currentRegionmap && $self->session->loginFlag && $self->mapObj->currentRoom) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a single selected room
        @list = (
            'centre_map_selected_room', 'icon_centre_map_selected_room',
            'set_current_room', 'icon_set_current_room',
            'room_exits',
            'select_exit',
            'edit_room',
            'set_file_path',
            'add_contents_string',
            'add_hidden_string',
            'add_exclusive_prof',
        );

        if ($self->currentRegionmap && $self->session->loginFlag && $self->selectedRoom) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and either a single selected room or a single
        #   selected room tag
        @list = (
            'set_room_tag',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && ($self->selectedRoom || $self->selectedRoomTag)
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap, a current room and a single selected room
        #   (the current room and selected room shouldn't be the same)
        @list = (
            'path_finding_highlight',
            'path_finding_edit',
            'path_finding_go',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && $self->mapObj->currentRoom
            && $self->selectedRoom
            && $self->mapObj->currentRoom ne $self->selectedRoom
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and either a current room or a single selected
        #   room
        @list = (
            'add_to_model',
        );
        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && ($self->mapObj->currentRoom || $self->selectedRoom)
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a single selected room with
        #   ->sourceCodePath set, but ->virtualAreaPath not set
        @list = (
            'view_source_code',
            'edit_source_code',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && $self->selectedRoom
            && $self->selectedRoom->sourceCodePath
            && ! $self->selectedRoom->virtualAreaPath
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a single selected room with
        #   ->virtualAreaPath set
        @list = (
            'view_virtual_area',
            'edit_virtual_area',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && $self->selectedRoom
            && $self->selectedRoom->virtualAreaPath
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and one or more selected rooms
        @list = (
            'move_selected',
            'move_to_click', 'icon_move_to_click',
            'toggle_room_flag_sub',
            'reset_positions',
            'room_exclusivity_sub',
            'delete_room',
            'repaint_selected',
            'set_virtual_area',
            'reset_virtual_area',
            'toggle_exclusivity',
            'clear_exclusive_profs',
            'connect_adjacent',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && ($self->selectedRoom || $self->selectedRoomHash)
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and either one or more selected rooms or one
        #   or more selected room guilds (or a mixture of both)
        @list = (
            'set_room_guild',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && (
                $self->selectedRoom || $self->selectedRoomHash || $self->selectedRoomGuild
                || $self->selectedRoomGuildHash
            )
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and EITHER one or more selected rooms OR a
        #   current room
        @list = (
            'identify_room',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && ($self->selectedRoom || $self->selectedRoomHash || $self->mapObj->currentRoom)
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a last known room
        @list = (
            'centre_map_last_known_room', 'icon_centre_map_last_known_room'
        );

        if ($self->currentRegionmap && $self->session->loginFlag && $self->mapObj->lastKnownRoom) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and an empty
        #   $self->currentRegionmap->gridRoomHash
        @list = (
            'add_first_room',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && ! $self->currentRegionmap->gridRoomHash
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Meny items that require a current regionmap and a non-empty
        #   $self->currentRegionmap->gridRoomHash
        @list = (
            'recalculate_in_region',
            'locate_room_in_current',
        );
        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && $self->currentRegionmap->gridRoomHash
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a single selected exit
        @list = (
            'edit_exit',
            'disconnect_exit',
            'delete_exit',
            'set_exit_type',
        );

        if ($self->currentRegionmap && $self->session->loginFlag && $self->selectedExit) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and one or more selected exits
        @list = (
            'set_ornament_sub',
            'icon_no_ornament', 'icon_openable_exit', 'icon_lockable_exit',
            'icon_pickable_exit', 'icon_breakable_exit', 'icon_impassable_exit',
            'identify_exit',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && ($self->selectedExit || $self->selectedExitHash)
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap, a single selected exit and
        #   $self->selectedExit->drawMode is 'temp_alloc' or 'temp_unalloc'
        @list = (
            'allocate_map_dir',
            'allocate_shadow',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && $self->selectedExit
            && (
                $self->selectedExit->drawMode eq 'temp_alloc'
                || $self->selectedExit->drawMode eq 'temp_unalloc'
            )
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap, a single selected exit and
        #   $self->selectedExit->drawMode is 'primary' or 'perm_alloc'
        @list = (
            'change_direction',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && $self->selectedExit
            && (
                $self->selectedExit->drawMode eq 'primary'
                || $self->selectedExit->drawMode eq 'perm_alloc'
            )
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap, a single selected exit and
        #   $self->selectedExit->drawMode is 'primary', 'temp_unalloc' or 'perm_alloc'
        @list = (
            'connect_to_click',
            'set_assisted_move',
            'icon_connect_click',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && $self->selectedExit
            && $self->selectedExit->drawMode ne 'temp_alloc'
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a single selected exit which is a broken
        #   exit
        @list = (
            'toggle_bent_exit',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && $self->selectedExit
            && $self->selectedExit->brokenFlag
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a single selected exit which is a region
        #   exit
        @list = (
            'set_super_sub',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && $self->selectedExit
            && $self->selectedExit->regionFlag
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and either a single selected exit which is a
        #   region exit, or a single selected exit tag
        @list = (
            'toggle_exit_tag',
            'edit_tag_text',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && (
                ($self->selectedExit && $self->selectedExit->regionFlag)
                || $self->selectedExitTag
            )
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and one or more selected exits or selected
        #   exit tags
        @list = (
            'reset_exit_tags',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && (
                $self->selectedExit || $self->selectedExitHash
                || $self->selectedExitTag || $self->selectedExitTagHash
            )
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a single selected exit which is a
        #   super-region exit
        @list = (
            'recalculate_from_exit',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && $self->selectedExit
            && $self->selectedExit->superFlag
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a single selected exit which is an
        #   uncertain exit or a one-way exit
        @list = (
            'set_exit_twin',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && $self->selectedExit
            && (
                $self->selectedExit->oneWayFlag
                || (
                    $self->selectedExit->destRoom
                    && ! $self->selectedExit->twinExit
                    && ! $self->selectedExit->retraceFlag
                    && $self->selectedExit->randomType eq 'none'
                )
            )
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a single selected exit which is a one-way
        #   exit
        @list = (
            'set_incoming_dir',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && $self->selectedExit
            && $self->selectedExit->oneWayFlag
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a single selected label
        @list = (
            'edit_label',
            'set_label_size',
        );

        if ($self->currentRegionmap && $self->session->loginFlag && $self->selectedLabel) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and one or more selected labels
        @list = (
            'delete_label',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && ($self->selectedLabel || $self->selectedLabelHash)
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a selected region (in the treeview)
        @list = (
            'identify_region',
        );

        if ($self->treeViewSelectedLine) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap, and $self->currentRegionmap->magnification
        #   to be within a certain range of values
        @magList = $self->constMagnifyList;

        @list = (
            'zoom_out',
        );

        # (Don't try to zoom out, if already zoomed out to the maximum extent)
        if (
            $self->currentRegionmap
            && $self->currentRegionmap->magnification > $magList[0]
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        @list = (
            'zoom_in',
        );

        # (Don't try to zoom in, if already zoomed in to the maximum extent)
        if (
            $self->currentRegionmap
            && $self->currentRegionmap->magnification < $magList[-1]
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and $self->worldModelObj->drawExitMode is
        #   'ask_regionmap'
        @list = (
            'draw_region_exits',
        );

        if (
            $self->currentRegionmap
            && $self->session->loginFlag
            && $self->worldModelObj->drawExitMode eq 'ask_regionmap'
        ) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current character profile
        @list = (
            'report_visits_3',
        );

        if ($self->session->currentChar) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a current character profile
        @list = (
            'report_visits_4',
        );

        if ($self->currentRegionmap && $self->session->loginFlag && $self->session->currentChar) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current guild profile
        @list = (
            'report_guilds_3',
        );

        if ($self->session->currentGuild) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require a current regionmap and a current guild profile
        @list = (
            'report_guilds_4',
        );

        if ($self->currentRegionmap && $self->session->loginFlag && $self->session->currentGuild) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Menu items that require assisted moves to be turned on
        @list = (
            'allow_protected_moves',
            'allow_super_protected_moves',
        );

        if ($self->worldModelObj->assistedMovesFlag) {
            push (@sensitiseList, @list);
        } else {
            push (@desensitiseList, @list);
        }

        # Sensitise and desensitise menu items and toolbar buttons, as required
        $self->sensitiseWidgets(@sensitiseList);
        $self->desensitiseWidgets(@desensitiseList);

        return 1;
    }

    sub sensitiseWidgets {

        # Called by anything. Frequently called by $self->restrictWidgets
        # Given a list of Gtk2 widgets (all of them menu/toolbar items), sets them as sensitive
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   @widgetList - A list of widgets - keys in the hash $self->menuToolItemHash
        #                 (e.g. 'move_up_level')
        #
        # Return values
        #   1

        my ($self, @widgetList) = @_;

        # (No improper arguments to check)

        foreach my $widgetName (@widgetList) {

            my $widget = $self->ivShow('menuToolItemHash', $widgetName);
            if ($widget) {

                $widget->set_sensitive(TRUE);
            }
        }

        return 1;
    }

    sub desensitiseWidgets {

        # Called by anything. Frequently called by $self->restrictWidgets
        # Given a list of Gtk2 widgets (all of them menu/toolbar items), sets them as insensitive
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   @widgetList - A list of widgets - keys in the hash $self->menuToolItemHash
        #                 (e.g. 'move_up_level')
        #
        # Return values
        #   1

        my ($self, @widgetList) = @_;

        # (No improper arguments to check)

        foreach my $widgetName (@widgetList) {

            my $widget = $self->ivShow('menuToolItemHash', $widgetName);
            if ($widget) {

                $widget->set_sensitive(FALSE);
            }
        }

        return 1;
    }

    sub restrictUpdateMode {

        # Called by $self->setMode
        # Sensitises or desensitises the menu and toolbar buttons that allow the user to switch to
        #   update mode, depending on the value of GA::Obj::WorldModel->disableUpdateModeFlag and
        #   GA::Session->status
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($radioMenuItem, $toolbarButton);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->restrictUpdateMode', @_);
        }

        # Mark the radio/toolbar buttons for 'update mode' as sensitive, or not
        $radioMenuItem = $self->ivShow('menuToolItemHash', 'set_update_mode');
        $toolbarButton = $self->ivShow('menuToolItemHash', 'icon_set_update_mode');

        if ($self->worldModelObj->disableUpdateModeFlag || $self->session->status eq 'offline') {

            if ($radioMenuItem) {

                $radioMenuItem->set_sensitive(FALSE);
            }

            if ($toolbarButton) {

                $toolbarButton->set_sensitive(FALSE);
            }

        } else {

            if ($radioMenuItem) {

                $radioMenuItem->set_sensitive(TRUE);
            }

            if ($toolbarButton) {

                $toolbarButton->set_sensitive(TRUE);
            }
        }

        return 1;
    }

    # Pause windows handlers

    sub showPauseWin {

        # Can be called by anything
        # Makes the pause window visible (a 'dialogue' window used only by this automapper)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->showPauseWin', @_);
        }

        if (! $axmud::CLIENT->busyWin) {

            # Show the window widget
            $self->showBusyWin(
                $axmud::SHARE_DIR . '/icons/system/mapper.png',
                'Working...',
            );
        }

        return 1;
    }

    sub hidePauseWin {

        # Can be called by anything
        # Makes the pause window invisible
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->hidePauseWin', @_);
        }

        if ($axmud::CLIENT->busyWin) {

            $self->closeDialogueWin($axmud::CLIENT->busyWin);
        }

        return 1;
    }

    # Canvas callbacks

    sub setupCanvasEvent {

        # Called by $self->resetMap() to create an anonymous function to intercept signals from the
        #   map background, filter out the signals we don't want, and pass the signals we do want to
        #   an event handler
        # Because the background is at the 'bottom', the anonymous function is only called when the
        #   user is clicking on an empty part of the map
        # Also called by $self->drawRoomEcho when the user clicks on a room echo, which we should
        #   treat as if it were a click on the map background
        #
        # Expected arguments
        #   $canvasObj     - The Gnome2::Canvas::Item::Rect which is the map's background (or the
        #                       Gnome2::Canvas::Item which is a room echo, which should be treated
        #                       as part of the map background)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $canvasObj, $check) = @_;

        # Check for improper arguments
        if (! defined $canvasObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setupCanvasEvent', @_);
        }

        $canvasObj->signal_connect (event => sub {

            my ($obj, $event) = @_;

            if (
                $event->type eq 'button-press'
                || $event->type eq '2button-press'
                || $event->type eq '3button-press'
                || $event->type eq 'button-release'
            ) {
                # If the tooltips are visible, hide them
                $self->hideTooltips();

                # All clicks on the canvas itself are handled by this function
                $self->canvasEventHandler($obj, $event);
            }
        });

        # Setup complete
        return 1;
    }

    sub setupCanvasObjEvent {

        # Called by various functions to create an anonymous function to intercept signals from
        #   canvas objects above the map background, filter out the signals we don't want, and pass
        #   the signals we do want to an event handler
        # Because canvas objects are 'above' the background, the anonymous function (and not the
        #   one in $self->setupCanvasEvent) is called when the user clicks on a room, room tag, room
        #   guild, exit or label directly
        #
        # Expected arguments
        #   $type       - What type of canvas object this is - 'room', 'room_tag', 'room_guild',
        #                   'exit', 'exit_tag' or 'label'
        #   $canvasObj  - The Gnome2::Canvas::Item on which the user has clicked
        #   $modelObj   - The GA::ModelObj::Room, GA::Obj::Exit or GA::Obj::MapLabel which is
        #                   represented by this canvas object
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $type, $canvasObj, $modelObj, $check) = @_;

        # Local variables
        my ($xPos, $yPos);

        # Check for improper arguments
        if (! defined $type || ! defined $canvasObj || ! defined $modelObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setupCanvasObjEvent', @_);
        }

        $canvasObj->signal_connect (event => sub {

            my ($obj, $event) = @_;

            # Get the coordinates of the click on the canvas (required for dragging operations)
            ($xPos, $yPos) = $canvasObj->parent->w2i($event->coords());

            # Process left- or right-clicks on a canvas object
            if ($event->type eq 'button-press' || $event->type eq '2button-press') {

                # If the Alt-Gr key is pressed down (or if we're in drag mode), it's a drag
                #   operation
                if ($event->state =~ m/mod5-mask/ || $self->dragModeFlag) {

                    $self->startDrag($type, $canvasObj, $modelObj, $event, $xPos, $yPos);

                } else {

                    # All other clicks on a canvas object are handled by the event handler
                    $self->canvasObjEventHandler($type, $canvasObj, $modelObj, $event);
                }

            } elsif (
                $event->type eq 'button-release'
                && $self->dragFlag && $canvasObj eq $self->dragCanvasObj
            ) {
                # Respond to the end of a drag operation
                $self->stopDrag($event, $xPos, $yPos);

            # Process mouse events - when the mouse moves over a canvas object, or leaves it -
            #   in order to display tooltips, etc
            } elsif (
                $event->type eq 'motion-notify'
                && $self->dragFlag
                && $canvasObj eq $self->dragCanvasObj
                && $event->state =~ m/button1-mask/
            ) {
                # Continue the drag operation by re-drawing the object(s) at their new position
                $self->continueDrag($event, $xPos, $yPos);

            } elsif (
                $event->type eq 'enter-notify'
                && $self->worldModelObj->showTooltipsFlag
                && ! $self->canvasTooltipObj
            ) {
                # Show the tooltips window
                $self->showTooltips($type, $obj, $modelObj);

            } elsif (
                $event->type eq 'leave-notify'
                && $self->canvasTooltipFlag
                && $self->canvasTooltipObj eq $obj
                && $self->canvasTooltipObjType eq $type
            ) {
                $self->hideTooltips();
            }
        });

        # Setup complete
        return 1;
    }

    sub canvasEventHandler {

        # Handles events on the map background (i.e. clicking on an empty part of the background
        #   which doesn't contain a room, room tag, room guild, exit, exit tag or label)
        # The calling function, an anonymous sub defined in $self->setupCanvasEvent, filters out the
        #   signals we don't want
        # At the moment, the signals let through the filter are:
        #   button_press, 2button_press, 3button_press, button_release
        #
        # Expected arguments
        #   $canvasObj  - The canvas object which intercepted an event signal
        #   $event      - The Gtk2::Gdk::Event that caused the signal
        #
        # Return values
        #   'undef' on improper arguments, if there is no region map or if the signal $event is one
        #       that this function doesn't handle
        #   1 otherwise

        my ($self, $canvasObj, $event, $check) = @_;

        # Local variables
        my (
            $clickXPosPixels, $clickYPosPixels, $clickType, $button, $shiftFlag, $ctrlFlag,
            $clickXPosBlocks, $clickYPosBlocks, $newRoomObj, $roomNum, $roomObj,
            $borderCornerXPosPixels, $borderCornerYPosPixels, $exitObj, $result,
            $result2, $twinExitObj, $popupMenu,
        );

        # Check for improper arguments
        if (! defined $canvasObj || ! defined $event || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->canvasEventHandler', @_);
        }

        # Don't do anything if there is no current regionmap
        if (! $self->currentRegionmap) {

            return undef;
        }

        # In case the previous click on the canvas was a right-click on an exit, we no longer need
        #   the coordinates of the click
        $self->ivUndef('exitClickXPosn');
        $self->ivUndef('exitClickYPosn');

        # Get the coordinates on the map of the clicked pixel. If the map is magnified we might get
        #   fractional values, so we need to use int()
        ($clickXPosPixels, $clickYPosPixels) = (int($event->x), int($event->y));

        # For mouse button clicks, get the click type (single- or double-click) and whether or not
        #   the SHIFT and/or CTRL keys were held down
        ($clickType, $button, $shiftFlag, $ctrlFlag) = $self->checkMouseClick($event);
        if (! $clickType) {

            # Not an event in which we're interested
            return undef;
        }

        # Work out which gridblock is underneath the mouse click
        ($clickXPosBlocks, $clickYPosBlocks) = $self->findGridBlock(
            $clickXPosPixels,
            $clickYPosPixels
        );

        # If $self->freeClickMode isn't set to 'default', left-clicking on empty space causes
        #   something unusual to happen
        if ($clickType eq 'single' && $button eq 'left' && $self->freeClickMode ne 'default') {

            # Free click mode 1 - 'Add room at click' menu option
            # (NB If this code is altered, the equivalent code in ->enableCanvasPopupMenu must also
            #   be altered)
            if ($self->freeClickMode eq 'add_room') {

                # Only add one new room
                $self->ivPoke('freeClickMode', 'default');

                $newRoomObj = $self->createNewRoom(
                    $self->currentRegionmap,
                    $clickXPosBlocks,
                    $clickYPosBlocks,
                    $self->currentRegionmap->currentLevel,
                );

                # When using the 'Add room at block' menu item, the new room is selected to make it
                #   easier to see where it was drawn
                # To make things consistent, select this new room, too
                $self->setSelectedObj(
                    [$newRoomObj, 'room'],
                    FALSE,              # Select this object; unselect all other objects
                );

            # Free click mode 2 - 'Connect exit to click' menu option
            } elsif (
                $self->freeClickMode eq 'connect_exit'
                && $clickType eq 'single'
                && $button eq 'left'
            ) {
                # If the user has selected the 'Connect exit to click' menu option,
                #   $self->freeClickMode has been set; since this part of the grid is not occupied,
                #   we can cancel it now
                $self->ivPoke('freeClickMode', 'default');

            # Free click mode 3 - 'Add label at click' menu option
            } elsif (
                $self->freeClickMode eq 'add_label'
                && $clickType eq 'single'
                && $button eq 'left'
            ) {
                $self->addLabelAtClickCallback($clickXPosPixels, $clickYPosPixels);

                # Only add one new label
                $self->ivPoke('freeClickMode', 'default');

            # Free click mode 4 - 'Move selected rooms to  click' menu option
            } elsif (
                $self->freeClickMode eq 'move_room'
                && $clickType eq 'single'
                && $button eq 'left'
            ) {
                $self->moveRoomsToClick($clickXPosBlocks, $clickYPosBlocks);

                # Only do it once
                $self->ivPoke('freeClickMode', 'default');
            }

            return 1;
        }

        # Otherwise, see if there's a room inside the gridblock that was clicked (if there is, we
        #   will be able to detect clicks near exits)
        $roomNum = $self->currentRegionmap->fetchRoom(
            $clickXPosBlocks,
            $clickYPosBlocks,
            $self->currentRegionmap->currentLevel,
        );

        if (defined $roomNum) {

            $roomObj = $self->worldModelObj->ivShow('modelHash', $roomNum);
        }

        if (
            $roomObj
            && ($clickType eq 'double' || $clickType eq 'triple')
            && $button eq 'left'
            && $self->worldModelObj->quickPathFindFlag
        ) {
            # Don't know why double click signals are picked up by the background, but there you go.
            #   See if the click was within the room

            # Find the coordinates of the pixel at the top-left corner of the room's border
            ($borderCornerXPosPixels, $borderCornerYPosPixels) = $self->getBorderCorner(
                $clickXPosBlocks,
                $clickYPosBlocks,
            );

            # Did the user click on the room (or its border), or in the surrounding area of the
            #   gridblock?
            if (
                $clickXPosPixels >= $borderCornerXPosPixels
                && $clickYPosPixels >= $borderCornerYPosPixels
                && $clickXPosPixels
                    <= ($borderCornerXPosPixels + $self->currentRegionmap->roomWidthPixels - 1)
                && $clickYPosPixels
                    <= ($borderCornerYPosPixels + $self->currentRegionmap->roomHeightPixels - 1)
            ) {
                # User double-clicked on the room (or its border)
                # Don't do anything if the user double-clicks on the current room, or if the current
                #   room isn't known
                if (! $self->mapObj->currentRoom || $roomObj eq $self->mapObj->currentRoom) {

                    return 1;

                } else {

                    # Select the clicked room...
                    $self->setSelectedObj(
                        [$roomObj, 'room'],
                        FALSE,          # Select this object; unselect all other objects
                    );

                    # ...and go there
                    return $self->processPathCallback('send_char');
                }

            } else {

                # Ignore double clicks outside a room's border
                return 1;
            }

        } elsif ($roomObj && $self->drawnExitHash) {

            # Usually, when we click on the map on an empty pixel, all selected objects are
            #   unselected.
            # However, because exits are often drawn only 1 pixel wide, they're quite difficult to
            #   click on. This section checks whether the mouse click occured close enough to an
            #   exit
            # A left-click near an exit causes the exit to be selected/unselected. A right-click
            #   selects the exit (unselecting everything else) and opens a popup menu for that exit.
            #   If the click isn't close enough to an exit, the user is deemed to have clicked in
            #   open space
            # (NB If no exits have been drawn, don't bother checking)

            # Now we check if they clicked near an exit, or in open space
            $exitObj = $self->findClickedExit($clickXPosPixels, $clickYPosPixels, $roomObj);
            if ($exitObj) {

                if ($button eq 'left') {

                    # If this exit (and/or its twin) is a selected exit, unselect them
                    $result = $self->unselectObj($exitObj);
                    if ($exitObj->twinExit) {

                        $twinExitObj
                            = $self->worldModelObj->ivShow('exitModelHash', $exitObj->twinExit);

                        if ($twinExitObj) {

                            $result2 = $self->unselectObj($twinExitObj);
                        }
                    }

                    if (! $result && ! $result2) {

                        # The exit wasn't already selected, so select it
                        $self->setSelectedObj(
                            [$exitObj, 'exit'],
                            # Retain other selected objects if CTRL key held down
                            $ctrlFlag,
                        );
                    }

                } elsif ($button eq 'right') {

                    # Select the exit, unselecting all other selected objects
                    $self->setSelectedObj(
                        [$exitObj, 'exit'],
                        FALSE,          # Select this object; unselect all other objects
                    );

                    # Create the popup menu
                    if ($self->selectedExit) {

                        $popupMenu = $self->enableExitsPopupMenu();
                        if ($popupMenu) {

                            $popupMenu->popup(
                                undef, undef, undef, undef,
                                $event->button,
                                $event->time,
                            );
                        }
                    }
                }

                return 1;
            }
        }

        # Otherwise, the user clicked in open space. Unselect all selected objects
        $self->setSelectedObj();

        # If it was a right-click, open a popup menu
        if ($clickType eq 'single' && $button eq 'right') {

            $popupMenu = $self->enableCanvasPopupMenu(
                $clickXPosPixels,
                $clickYPosPixels,
                $clickXPosBlocks,
                $clickYPosBlocks,
            );

            if ($popupMenu) {

                $popupMenu->popup(
                    undef, undef, undef, undef,
                    $event->button,
                    $event->time,
                );
            }
        }

        return 1;
    }

    sub canvasObjEventHandler {

        # Handles events on canvas object (i.e. clicking on a room, room tag, room guild, exit,
        #   exit tag or label)
        # The calling function, an anonymous sub defined in $self->setupCanvasObjEvent, filters out
        #   the signals we don't want
        # At the moment, the signals let through the filter are:
        #   button_press, 2button_press,
        #
        # Expected arguments
        #   $objType    - 'room', 'room_tag', 'room_guild', 'exit', 'exit_tag' or 'label'
        #   $canvasObj  - The canvas object which intercepted an event signal
        #   $modelObj   - The GA::ModelObj::Room, GA::Obj::Exit or GA::Obj::MapLabel which is
        #                   represented by this canvas object
        #   $event      - The Gtk2::Gdk::Event that caused the signal
        #
        # Return values
        #   'undef' on improper arguments or if the signal $event is one that this function doesn't
        #       handle
        #   1 otherwise

        my ($self, $objType, $canvasObj, $modelObj, $event, $check) = @_;

        # Local variables
        my (
            $clickType, $button, $shiftFlag, $ctrlFlag, $otherRoomObj, $result, $result2,
            $twinExitObj, $startX, $stopX, $startY, $stopY, $popupMenu,
        );

        # Check for improper arguments
        if (
            ! defined $objType || ! defined $canvasObj || ! defined $modelObj || ! defined $event
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->canvasObjEventHandler', @_);
        }

        # In case the previous click on the canvas was a right-click on an exit, we no longer need
        #   the coordinates of the click
        $self->ivUndef('exitClickXPosn');
        $self->ivUndef('exitClickYPosn');

        # If $self->freeClickMode has been set to 'add_room' or 'add_label' by the 'Add room at
        #   click' or 'Add label at click' menu options, since this part of the grid is already
        #   occupied, we can go back to normal
        if ($self->freeClickMode eq 'add_room' || $self->freeClickMode eq 'add_label') {

            $self->ivPoke('freeClickMode', 'default');
        }

        # For mouse button clicks, get the click type (single- or double-click) and whether or not
        #   the SHIFT and/or CTRL keys were held down
        ($clickType, $button, $shiftFlag, $ctrlFlag) = $self->checkMouseClick($event);
        if (! $clickType) {

            # Not an event in which we're interested
            return undef;
        }

        # Process single left clicks
        if ($clickType eq 'single' && $button eq 'left') {

            # Process a left-clicked room differently, if ->freeClickMode has been set to
            #   'connect_exit' by the 'Connect to click' menu option (ignoring the SHIFT/CTRL keys)
            if ($self->freeClickMode eq 'connect_exit' && $objType eq 'room') {

                # Occasionally get an error, when there's no selected exit. $self->freeClickMode
                #   should get reset, but not in these situations
                if (! $self->selectedExit) {

                    $self->ivPoke('freeClickMode', 'default');

                } else {

                    # Get the selected exit's parent room, and the room's parent region
                    $otherRoomObj
                        = $self->worldModelObj->ivShow('modelHash', $self->selectedExit->parent);

                    # If that room and the clicked room are in the same region...
                    if ($otherRoomObj && $modelObj->parent == $otherRoomObj->parent) {

                        # The two rooms are in the same region, so it's (possibly) a broken exit
                        $self->connectExitToRoom($modelObj, 'broken');

                    } else {

                        # The two rooms are in different regions, so it's a region exit
                        $self->connectExitToRoom($modelObj, 'region');
                    }

                    # Only do it once
                    $self->ivPoke('freeClickMode', 'default');
                }

            # Process a left-clicked room differently, if ->freeClickMode has been set to
            #   'move_room' by the 'Move selected rooms to click' option (ignoring the SHIFT/CTRL
            #   keys)
            } elsif ($self->freeClickMode eq 'move_room' && $objType eq 'room') {

                $self->moveRoomsToExit($modelObj);

                # Only do it once
                $self->ivPoke('freeClickMode', 'default');

            # Process left-clicked rooms (ignoring the CTRL key, but checking for the SHIFT key)
            } elsif (
                $objType eq 'room'
                && $shiftFlag
                && ($self->selectedRoom || $self->selectedRoomHash)
            ) {
                # Find the coordinates of opposite corners (top-left and bottom-right) of the area
                #   of the grid which contains currently selected rooms
                ($startX, $startY, $stopX, $stopY) = $self->findSelectedRoomArea();

                # If there are no selected rooms on this level...
                if (! defined $startX) {

                    # Select this room, only
                    $startX = $modelObj->xPosBlocks;
                    $startY = $modelObj->yPosBlocks;
                    $stopX = $modelObj->xPosBlocks;
                    $stopY = $modelObj->yPosBlocks;

                # Otherwise, if the clicked room is selected...
                } elsif ($self->checkRoomIsSelected($modelObj)) {

                    # If the clicked room is at the top-left of the area containing selected rooms,
                    #   select only this room, and unselect all the others
                    if ($modelObj->xPosBlocks == $startX && $modelObj->yPosBlocks == $startY) {

                        $stopX = $startX;
                        $stopY = $startY;

                    # Otherwise, the clicked room is the new bottom-right of the selected area
                    } else {

                        $stopX = $modelObj->xPosBlocks;
                        $stopY = $modelObj->yPosBlocks;
                    }

                # ...but if the clicked room isn't selected...
                } else {

                    # If the clicked room's x-coordinate is to the left of the area's starting
                    #   x-coordinate, change the area's starting x co-ordinate
                    if ($modelObj->xPosBlocks < $startX) {

                        $startX = $modelObj->xPosBlocks;

                    # Likewise for the other three corners
                    } elsif ($modelObj->xPosBlocks > $stopX) {

                        $stopX = $modelObj->xPosBlocks;
                    }

                    if ($modelObj->yPosBlocks < $startY) {

                        $startY = $modelObj->yPosBlocks;

                    } elsif ($modelObj->yPosBlocks > $stopY) {

                        $stopY = $modelObj->yPosBlocks;
                    }
                }

                # Select all rooms in the (modified) area, and unselect all rooms outside it (along
                #   with any selected exits, room tags and labels)
                $self->selectRoomsInArea($startX, $startY, $stopX, $stopY);

            # Process left-clicked room tags (ignoring the SHIFT key, but checking for the CTRL key)
            } elsif ($objType eq 'room_tag') {

                # If a group of things are already selected, unselect them all and select the object
                #   that was clicked
                if (
                    ! $ctrlFlag
                    && (
                        $self->selectedRoomHash || $self->selectedRoomTagHash
                        || $self->selectedRoomGuildHash || $self->selectedExitHash
                        || $self->selectedExitTagHash || $self->selectedLabelHash
                    )
                ) {
                    # Select this room tag, unselecting all other objects
                    $self->setSelectedObj(
                        [$modelObj, 'room_tag'],
                        # Retain other selected objects if CTRL key held down
                        $ctrlFlag,
                    );

                } else {

                    # If this object is already a selected object, unselect it
                    $result = $self->unselectObj($modelObj, 'room_tag');
                    if (! $result) {

                        # The room tag wasn't already selected, so select it
                        $self->setSelectedObj(
                            [$modelObj, 'room_tag'],
                            # Retain other selected objects if CTRL key held down
                            $ctrlFlag,
                        );
                    }
                }

            # Process left-clicked room guilds (ignoring the SHIFT key, but checking for the CTRL
            #   key)
            } elsif ($objType eq 'room_guild') {

                # If a group of things are already selected, unselect them all and select the object
                #   that was clicked
                if (
                    ! $ctrlFlag
                    && (
                        $self->selectedRoomHash || $self->selectedRoomTagHash
                        || $self->selectedRoomGuildHash || $self->selectedExitHash
                        || $self->selectedExitTagHash || $self->selectedLabelHash
                    )
                ) {
                    # Select this room guild, unselecting all other objects
                    $self->setSelectedObj(
                        [$modelObj, 'room_guild'],
                        # Retain other selected objects if CTRL key held down
                        $ctrlFlag,
                    );

                } else {

                    # If this object is already a selected object, unselect it
                    $result = $self->unselectObj($modelObj, 'room_guild');
                    if (! $result) {

                        # The room guild wasn't already selected, so select it
                        $self->setSelectedObj(
                            [$modelObj, 'room_guild'],
                            # Retain other selected objects if CTRL key held down
                            $ctrlFlag,
                        );
                    }
                }

            # Process left-clicked exits (ignoring the SHIFT key, but checking for the CTRL key)
            } elsif ($objType eq 'exit') {

                # For twin exits - which share a canvas object - use the exit whose parent room is
                #   closest to the click
                $modelObj = $self->chooseClickedExit($modelObj, int($event->x), int($event->y));

                # If a group of things are already selected, unselect them all and select the object
                #   that was clicked
                if (
                    ! $ctrlFlag
                    && (
                        $self->selectedRoomHash || $self->selectedRoomTagHash
                        || $self->selectedRoomGuildHash || $self->selectedExitHash
                        || $self->selectedExitTagHash || $self->selectedLabelHash
                    )
                ) {
                    # Select this exit, unselecting all other objects
                    $self->setSelectedObj(
                        [$modelObj, 'exit'],
                        # Retain other selected objects if CTRL key held down
                        $ctrlFlag,
                    );

                } else {

                    # If this exit (and/or its twin) is a selected exit, unselect them
                    $result = $self->unselectObj($modelObj);
                    if ($modelObj->twinExit) {

                        $twinExitObj
                            = $self->worldModelObj->ivShow('exitModelHash', $modelObj->twinExit);

                        if ($twinExitObj) {

                            $result2 = $self->unselectObj($twinExitObj);
                        }
                    }

                    if (! $result && ! $result2) {

                        # The exit wasn't already selected, so select it
                        $self->setSelectedObj(
                            [$modelObj, 'exit'],
                            # Retain other selected objects if CTRL key held down
                            $ctrlFlag,
                        );
                    }
                }

            # Process left-clicked exit tags (ignoring the SHIFT key, but checking for the CTRL key)
            } elsif ($objType eq 'exit_tag') {

                # If a group of things are already selected, unselect them all and select the object
                #   that was clicked
                if (
                    ! $ctrlFlag
                    && (
                        $self->selectedRoomHash || $self->selectedRoomTagHash
                        || $self->selectedRoomGuildHash || $self->selectedExitHash
                        || $self->selectedExitTagHash || $self->selectedLabelHash
                    )
                ) {
                    # Select this exit tag, unselecting all other objects
                    $self->setSelectedObj(
                        [$modelObj, 'exit_tag'],
                        # Retain other selected objects if CTRL key held down
                        $ctrlFlag,
                    );

                } else {

                    # If this object is already a selected object, unselect it
                    $result = $self->unselectObj($modelObj, 'exit_tag');
                    if (! $result) {

                        # The exit tag wasn't already selected, so select it
                        $self->setSelectedObj(
                            [$modelObj, 'exit_tag'],
                            # Retain other selected objects if CTRL key held down
                            $ctrlFlag,
                        );
                    }
                }

            # Process other kinds of left-click
            } else {

                # If a group of things are already selected, unselect them all and select the object
                #   that was clicked
                if (
                    ! $ctrlFlag
                    && (
                        $self->selectedRoomHash || $self->selectedRoomTagHash
                        || $self->selectedRoomGuildHash || $self->selectedExitHash
                        || $self->selectedExitTagHash || $self->selectedLabelHash
                    )
                ) {
                    # Select this room/label, unselecting all other objects
                    $self->setSelectedObj(
                        [$modelObj, $objType],
                        # Retain other selected objects if CTRL key held down
                        $ctrlFlag,
                    );

                } else {

                    # If this object is already a selected object, unselect it
                    $result = $self->unselectObj($modelObj);
                    if (! $result) {

                        # The room or label wasn't already selected, so select it
                        $self->setSelectedObj(
                            [$modelObj, $objType],
                            # Retain other selected objects if CTRL key held down
                            $ctrlFlag,
                        );
                    }
                }
            }

        # Process right-clicks
        } elsif ($clickType eq 'single' && $button eq 'right') {

            if ($objType eq 'exit') {

                # For twin exits - which share a canvas object - use the exit whose parent room is
                #   closest to the click
                $modelObj = $self->chooseClickedExit($modelObj, int($event->x), int($event->y));
            }

            # If a group of things are already selected, unselect them all and select the object
            #   that was clicked
            if (
                $self->selectedRoomHash || $self->selectedRoomTagHash
                || $self->selectedRoomGuildHash || $self->selectedExitHash
                || $self->selectedExitTagHash || $self->selectedLabelHash
            ) {
                # Select this room/label, unselecting all other objects
                $self->setSelectedObj(
                    [$modelObj, $objType],
                    # Retain other selected objects if CTRL key held down
                    $ctrlFlag,
                );

            } else {

                # If this object isn't already selected, select it (but don't unselect something
                #   as we would for a left-click)
                if ($objType eq 'room_tag') {

                    $self->setSelectedObj(
                        [$modelObj, 'room_tag'],
                        FALSE,          # Select this object; unselect all other objects
                    );

                } elsif ($objType eq 'room_guild') {

                    $self->setSelectedObj(
                        [$modelObj, 'room_guild'],
                        FALSE,          # Select this object; unselect all other objects
                    );

                } elsif ($objType eq 'exit_tag') {

                    $self->setSelectedObj(
                        [$modelObj, 'exit_tag'],
                        FALSE,          # Select this object; unselect all other objects
                    );

                } else {

                    $self->setSelectedObj(
                        [$modelObj, $objType],
                        FALSE,          # Select this object; unselect all other objects
                    );
                }
            }

            # Create the popup menu
            if ($objType eq 'room' && $self->selectedRoom) {
                $popupMenu = $self->enableRoomsPopupMenu();
            } elsif ($objType eq 'room_tag' && $self->selectedRoomTag) {
                $popupMenu = $self->enableRoomTagsPopupMenu();
            } elsif ($objType eq 'room_guild' && $self->selectedRoomGuild) {
                $popupMenu = $self->enableRoomGuildsPopupMenu();
            } elsif ($objType eq 'exit_tag' && $self->selectedExitTag) {
                $popupMenu = $self->enableExitTagsPopupMenu();
            } elsif ($objType eq 'exit' && $self->selectedExit) {

                # Store the position of the right-click, in case the user wants to add a bend from
                #   the popup menu
                $self->ivPoke('exitClickXPosn', int($event->x));
                $self->ivPoke('exitClickYPosn', int($event->y));
                # Now we can open the poup menu
                $popupMenu = $self->enableExitsPopupMenu();

            } elsif ($objType eq 'label' && $self->selectedLabel) {

                $popupMenu = $self->enableLabelsPopupMenu();
            }

            if ($popupMenu) {

                $popupMenu->popup(undef, undef, undef, undef, $event->button, $event->time);
            }
        }

        return 1;
    }

    sub startDrag {

        # Called by $self->setupCanvasObjEvent at the start of a drag operation
        # Grabs the clicked canvas object and sets up IVs
        #
        # Expected arguments
        #   $type           - What type of canvas object this is - 'room', 'room_tag', 'room_guild',
        #                       'exit', 'exit_tag' or 'label'
        #   $canvasObj      - The Gnome2::Canvas::Item on which the user clicked
        #   $modelObj       - The GA::ModelObj::Room, GA::Obj::Exit or GA::Obj::MapLabel which
        #                       corresponds to the canvas object $canvasObj
        #   $event          - The mouse click event (a Gtk::Gdk::Event)
        #   $xPos, $yPos    - The coordinates of the click on the canvas
        #
        # Return values
        #   'undef' on improper arguments or if a dragging operation has already started
        #   1 otherwise

        my ($self, $type, $canvasObj, $modelObj, $event, $xPos, $yPos, $check) = @_;

        # Local variables
        my (
            $mode, $posn, $listRef, $fakeRoomObj, $twinExitObj, $bendNum, $bendIndex, $twinBendNum,
            $twinBendIndex,
            @offsetList,
        );

        # Check for improper arguments
        if (
            ! defined $type || ! defined $canvasObj || ! defined $modelObj || ! defined $event
            || ! defined $xPos || ! defined $yPos || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->startDrag', @_);
        }

        # Gtk2 can return fractional values for $xPos, $yPos. We definitely want only integers
        $xPos = int($xPos);
        $yPos = int($yPos);

        # Double-clicking on a canvas object can cause this function to be called twice; the second
        #   time, don't do anything
        if ($self->dragFlag) {

            return undef;
        }

        # If the tooltips are visible, hide them
        $self->hideTooltips();

        if ($type eq 'room') {

            # If the room has been drawn emphasised (a normal box with a border, plus an extra
            #   square just outside it, in the same colour as the border, giving the impression of a
            #   thicker border), we need to re-draw it without emphasis - otherwise the extra
            #   square will be left behind on the canvas, while the room is being dragged around
            if ($self->worldModelObj->drawExitMode eq 'ask_regionmap') {
                $mode = $self->currentRegionmap->drawExitMode;
            } else {
                $mode = $self->worldModelObj->drawExitMode;
            }

            # The TRUE argument means 'don't draw an emphasised room'
            $self->drawRoom($modelObj, $mode, TRUE);

            # Get the canvas object which has just been drawn
            $posn = $modelObj->xPosBlocks . '_' . $modelObj->yPosBlocks . '_'
                        . $modelObj->zPosBlocks;
            $listRef = $self->ivShow('drawnRoomHash', $posn);
            $canvasObj = $$listRef[0];

            # Raise the canvas object above others so that, while we're dragging it around, it
            #   doesn't disappear under exits (and so on)
            $canvasObj->raise_to_top();

            # Draw a fake room at the same position, so that $modelObj's exits don't look odd
            $fakeRoomObj = $self->drawFakeRoomBox($modelObj);
            if ($fakeRoomObj) {

                $self->ivPoke('dragFakeRoomObj', $fakeRoomObj);
                # Update Gtk2's events queue
                $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->startDrag');
            }

        } elsif ($type eq 'exit') {

            if ($modelObj->twinExit) {

                $twinExitObj = $self->worldModelObj->ivShow('exitModelHash', $modelObj->twinExit);
            }

            # See if the click was near a bend
            $bendNum = $self->findExitBend($modelObj, $xPos, $yPos);
            if (defined $bendNum) {

                # Set IVs to monitor the bend's position, relative to its position right now
                $self->ivPoke('dragBendNum', $bendNum);
                # (The first bend, $bendNum = 0, occupies the first two items in ->bendOffsetList)
                $bendIndex = $bendNum * 2;

                $self->ivPoke('dragBendInitXPos', $modelObj->ivIndex('bendOffsetList', $bendIndex));
                $self->ivPoke(
                    'dragBendInitYPos',
                    $modelObj->ivIndex('bendOffsetList', ($bendIndex + 1)),
                );

                if ($self->worldModelObj->drawExitMode eq 'ask_regionmap') {
                    $self->ivPoke('dragExitDrawMode', $self->currentRegionmap->drawExitMode);
                } else {
                    $self->ivPoke('dragExitDrawMode', $self->worldModelObj->drawExitMode);
                }

                # If there's a twin exit, set IVs for the corresponding bend in the twin
                if ($twinExitObj) {

                    $twinBendNum = ((scalar $twinExitObj->bendOffsetList / 2) - $bendNum - 1);
                    $self->ivPoke('dragBendTwinNum', $twinBendNum);
                    # (The 2nd bend, $bendNum = 1, occupies the 2nd two items in ->bendOffsetList)
                    $twinBendIndex = $twinBendNum * 2;
                    $self->ivPoke(
                        'dragBendTwinInitXPos',
                        $twinExitObj->ivIndex('bendOffsetList', $twinBendIndex),
                    );

                    $self->ivPoke(
                        'dragBendTwinInitYPos',
                        $twinExitObj->ivIndex('bendOffsetList', ($twinBendIndex + 1)),
                    );
                }

            } else {

                # Destroy the existing canvas object
                $self->deleteCanvasObj('exit', $modelObj);
                # The canvas objects for the exit may have been drawn associated with $exitObj, or
                #   with its twin exit (if any); make sure those canvas objects are destroyed, too
                #   (except for normal broken exits, region exits and impassable exits)
                if (
                    $twinExitObj
                    && (
                        (! $twinExitObj->brokenFlag || $twinExitObj->bentFlag)
                        && ! $twinExitObj->regionFlag
                        && ! $twinExitObj->impassFlag
                    )
                ) {
                    $self->deleteCanvasObj('exit', $twinExitObj);
                }

                # Draw a draggable exit, starting from $exitObj's normal start position, and ending
                #   at the position of the mouse click
                $canvasObj = $self->drawDraggableExit($modelObj, $xPos, $yPos);
                # Update Gtk2's events queue
                $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->startDrag');
            }

        } else {

            # For room tags, room guilds, exit tags and labels, just raise the canvas object above
            #   others
            $canvasObj->raise_to_top();
        }

        # Grab the canvas object
        $canvasObj->grab(
            [qw/pointer-motion-mask button-release-mask/],
            Gtk2::Gdk::Cursor->new('fleur'),
            $event->time,
        );

        # Mark the drag as started, and store the canvas object's initial configuration
        $self->ivPoke('dragFlag', TRUE);
        $self->ivPoke('dragCanvasObj', $canvasObj);
        $self->ivPoke('dragModelObj', $modelObj);
        $self->ivPoke('dragModelObjType', $type);
        $self->ivPoke('dragInitXPos', $xPos);
        $self->ivPoke('dragInitYPos', $yPos);
        $self->ivPoke('dragCurrentXPos', $xPos);
        $self->ivPoke('dragCurrentYPos', $yPos);

        return 1;
    }

    sub continueDrag {

        # Called by $self->setupCanvasObjEvent in the middle of a drag operation
        # Redraws canvas object(s) on the canvas and updates IVs
        #
        # Expected arguments
        #   $event          - The mouse click event (a Gtk::Gdk::Event)
        #   $xPos, $yPos    - The coordinates of the mouse above the canvas
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $event, $xPos, $yPos, $check) = @_;

        # Local variables
        my ($listRef, $canvasObj, $twinExitObj);

        # Check for improper arguments
        if (! defined $event || ! defined $xPos || ! defined $yPos || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->continueDrag', @_);
        }

        # Gtk2 can return fractional values for $xPos, $yPos. We definitely want only integers
        $xPos = int($xPos);
        $yPos = int($yPos);

        # For everything except exits, move the canvas object
        if ($self->dragModelObjType ne 'exit') {

            $self->dragCanvasObj->move(
                ($xPos - $self->dragCurrentXPos),
                ($yPos - $self->dragCurrentYPos),
            );

        } else {

            # Ungrab the canvas object
            $self->dragCanvasObj->ungrab($event->time);

            # If dragging an exit bend...
            if (defined $self->dragBendNum) {

                # Update the exit's list of bend positions
                $self->worldModelObj->adjustExitBend(
                    $self->dragModelObj,
                    $self->dragBendNum,
                    $self->dragBendInitXPos + ($xPos - $self->dragInitXPos),
                    $self->dragBendInitYPos + ($yPos - $self->dragInitYPos),
                );

                # Adjust the corresponding bend in the twin exit, if there is one
                if (defined $self->dragBendTwinNum) {

                    $twinExitObj = $self->worldModelObj->ivShow(
                        'exitModelHash',
                        $self->dragModelObj->twinExit,
                    );

                    $self->worldModelObj->adjustExitBend(
                        $twinExitObj,
                        $self->dragBendTwinNum,
                        $self->dragBendTwinInitXPos + ($xPos - $self->dragInitXPos),
                        $self->dragBendTwinInitYPos + ($yPos - $self->dragInitYPos),
                    );
                }

                # Destroy the bending exit's existing canvas objects
                $self->deleteCanvasObj('exit', $self->dragModelObj);
                # Redraw the bending exit, with the dragged bend in its new position
                $self->drawBentExit(
                    $self->worldModelObj->ivShow('modelHash', $self->dragModelObj->parent),
                    $self->dragModelObj,
                    $self->dragExitDrawMode,
                    $twinExitObj,
                );

                # Get the new canvas object to grab. Since the bending exit consists of several
                #   canvas objects, use the first one
                $listRef = $self->ivShow('drawnExitHash', $self->dragModelObj->number);
                $canvasObj = $$listRef[0];

            # If dragging a draggable exit...
            } else {

                # Destroy the old canvas object
                $self->dragCanvasObj->destroy();

                # Replace it with a new one draggable exit at the current mouse position
                $canvasObj = $self->drawDraggableExit($self->dragModelObj, $xPos, $yPos);
            }

            # Grab the canvas object
            $canvasObj->grab(
                [qw/pointer-motion-mask button-release-mask/],
                Gtk2::Gdk::Cursor->new('fleur'),
                $event->time,
            );

            $self->ivPoke('dragCanvasObj', $canvasObj);
        }

        # Show the canvas object in its new position
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->continueDrag');

        # Update IVs
        $self->ivPoke('dragCurrentXPos', $xPos);
        $self->ivPoke('dragCurrentYPos', $yPos);

        return 1;
    }

    sub stopDrag {

        # Called by $self->setupCanvasObjEvent at the end of a drag operation
        #
        # Expected arguments
        #   $event          - The mouse click event (a Gtk::Gdk::Event)
        #   $xPos, $yPos    - The coordinates of the click on the canvas
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $event, $xPos, $yPos, $check) = @_;

        # Local variables
        my ($destRoomObj, $newXPos, $newYPos);

        # Check for improper arguments
        if (! defined $event || ! defined $xPos || ! defined $yPos || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->stopDrag', @_);
        }

        # Gtk2 can return fractional values for $xPos, $yPos. We definitely want only integers
        $xPos = int($xPos);
        $yPos = int($yPos);

        # Ungrab the canvas object
        $self->dragCanvasObj->ungrab($event->time);

        # Mark the drag operation as finished
        $self->ivPoke('dragFlag', FALSE);

        # Respond to the drag & drop
        if ($self->dragModelObjType eq 'room') {

            # Destroy the fake room canvas object at the original position (if there is one)
            if ($self->dragFakeRoomObj) {

                $self->dragFakeRoomObj->destroy();
            }

            # Calculate the coordinate of the room's new gridblock
            $newXPos = int ($self->dragCurrentXPos / $self->currentRegionmap->blockWidthPixels);
            $newYPos = int ($self->dragCurrentYPos / $self->currentRegionmap->blockHeightPixels);

            # Check that the new gridblock isn't occupied
            if (
                $self->currentRegionmap->fetchRoom(
                    $newXPos,
                    $newYPos,
                    $self->currentRegionmap->currentLevel,
                )
            ) {
                # The room has been dragged to the same gridblock, or the new gridblock is occupied;
                #   in both cases, don't move the room; just redraw it at its original position
                $self->doDraw('room', $self->dragModelObj);

            } else {

                # Select the room
                $self->setSelectedObj(
                    [$self->dragModelObj, 'room'],
                    FALSE,                      # Select this object; unselect all other objects
                );

                # Move the (selected) room to its new location
                $self->moveSelectedObjs(
                    ($newXPos - $self->dragModelObj->xPosBlocks),
                    ($newYPos - $self->dragModelObj->yPosBlocks),
                    0,      # Room doesn't change level
                );
            }

        } elsif ($self->dragModelObjType eq 'exit') {

            # Don't need to do anything at the end of a drag operation, if we're dragging an exit
            #   bend
            if (! defined $self->dragBendNum) {

                # Destroy the draggable exit
                $self->dragCanvasObj->destroy();

                # Work out whether the end of the draggable exit (the coordinates are $xPos, $yPos)
                #   was over a room that wasn't the exit's parent room or its existing destination
                #   room
                $destRoomObj = $self->findMouseOverRoom($xPos, $yPos, $self->dragModelObj);
                if (! $destRoomObj) {

                    # No connection to make. Redraw the original exit, at its original size and
                    #   position
                   $self->doDraw('exit', $self->dragModelObj);

                } else {

                    # Connect the exit to the room (it's in the same region as the exit's parent
                    #   room, so it's potentially a broken exit, and definitely not a region exit)
                    $self->connectExitToRoom($destRoomObj, 'broken', $self->dragModelObj);
                }
            }

        } else {

            # Work out the difference between the new position and the original position
            $newXPos = int ($self->dragCurrentXPos - $self->dragInitXPos);
            $newYPos = int ($self->dragCurrentYPos - $self->dragInitYPos);

            # Move the object and instruct the world model to update its Automapper windows
            $self->worldModelObj->moveOtherObjs(
                TRUE,       # Update Automapper windows immediately
                $self->dragModelObjType,
                $self->dragModelObj,
                $newXPos,
                $newYPos,
            );
        }

        # Reset other IVs
        $self->ivUndef('dragCanvasObj');
        $self->ivUndef('dragModelObj');
        $self->ivUndef('dragModelObjType');
        $self->ivUndef('dragInitXPos');
        $self->ivUndef('dragInitYPos');
        $self->ivUndef('dragCurrentXPos');
        $self->ivUndef('dragCurrentYPos');
        $self->ivUndef('dragFakeRoomObj');
        $self->ivUndef('dragBendNum');
        $self->ivUndef('dragBendInitXPos');
        $self->ivUndef('dragBendInitYPos');
        $self->ivUndef('dragBendTwinNum');
        $self->ivUndef('dragBendTwinInitXPos');
        $self->ivUndef('dragBendTwinInitYPos');
        $self->ivUndef('dragExitDrawMode');

        return 1;
    }

    sub chooseClickedExit {

        # Called by $self->startDrag when the user starts to drag an exit, and by
        #   ->canvasObjEventHandler clicks an unselected exit
        # Selects which exit to use - the exit whose canvas object was clicked, or its twin exit -
        #   and returns the exit
        #
        # Expected arguments
        #   $exitObj        - The exit object whose canvas object was clicked
        #   $xPos, $yPos    - The coordinates of the mouse click on the canvas object
        #
        # Return values
        #   'undef' on improper arguments
        #   Otherwise, returns either $exitObj or its twin exit object

        my ($self, $exitObj, $xPos, $yPos, $check) = @_;

        # Local variables
        my ($twinExitObj, $distance, $twinDistance);

        # Check for improper arguments
        if (! defined $exitObj || ! defined $xPos || ! defined $yPos || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->chooseClickedExit', @_);
        }

        if ($exitObj->twinExit) {

            # If the exit is a region exit, or a normal (not bent) broken exit, then there is no
            #   doubt that the clicked exit (not its twin) is the one to drag/select
            if (
                $exitObj->regionFlag
                || ($exitObj->brokenFlag && ! $exitObj->bentFlag)
            ) {
                return $exitObj;
            }

            # User clicked on a two-way exit, so we need to decide which exit to drag/select
            $twinExitObj = $self->worldModelObj->ivShow('exitModelHash', $exitObj->twinExit);

            # Find the distance, in pixels, between the click and the centre of the exit's parent
            #   room
            $distance = $self->findDistanceToRoom($exitObj, $xPos, $yPos);
            # Find the distance, in pixels, between the click and the centre of the twin exit's
            #   parent room
            $twinDistance = $self->findDistanceToRoom($twinExitObj, $xPos, $yPos);

            # If the distance to $exitObj's parent room is shorter, then use the twin exit as the
            #   clicked exit (otherwise, use $exitObj as the clicked exit)
            if ($distance < $twinDistance) {

                return $twinExitObj;
            }
        }

        # Otherwise, use $exitObj
        return $exitObj;
    }

    sub findDistanceToRoom {

        # Called by $self->chooseClickedExit
        # When the user clicks on an exit, finds the distance between the exit and the centre of the
        #   parent room (in pixels)
        #
        # Expected arguments
        #   $exitObj        - The exit object whose canvas object was clicked
        #   $xPos, $yPos    - The coordinates of the mouse click on the canvas object
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $exitObj, $xPos, $yPos, $check) = @_;

        # Local variables
        my ($roomObj, $roomXPos, $roomYPos, $lengthX, $lengthY);

        # Check for improper arguments
        if (! defined $exitObj || ! defined $xPos || ! defined $yPos || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->findDistanceToRoom', @_);
        }

        $roomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->parent);
        $roomXPos = ($roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels)
                            + int($self->currentRegionmap->blockWidthPixels / 2);
        $roomYPos = ($roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels)
                            + int($self->currentRegionmap->blockHeightPixels / 2);

        $lengthX = abs($roomXPos - $xPos);
        $lengthY = abs($roomYPos - $yPos);
        return (sqrt(($lengthX ** 2) + ($lengthY ** 2)));
    }

    sub findSelectedRoomArea {

        # Called by $self->canvasObjEventHandler
        # Finds the smallest area on the current level of the grid which contains all the selected
        #   rooms on this level
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   An empty list on improper arguments, or if there are no selected rooms on the current
        #       level
        #   Otherwise, returns a list containing two pairs of grid coordinates - the top-left and
        #       bottom-right gridblock of the currently selected area (which might be the same
        #       block, if there's only one selected room). The list is in the form
        #           ($startX, $startY, $stopX, $stopY)

        my ($self, $check) = @_;

        # Local variables
        my (
            $startX, $startY, $stopX, $stopY, $count,
            @emptyList,
        );

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->findSelectedRoomArea', @_);
            return @emptyList;
        }

        if ($self->selectedRoom) {

            # There is only one selected room. Is it on the current level?
            if ($self->selectedRoom->zPosBlocks == $self->currentRegionmap->currentLevel) {

                # It's on the current level
                $startX = $self->selectedRoom->xPosBlocks;
                $startY = $self->selectedRoom->yPosBlocks;
                $stopX = $self->selectedRoom->xPosBlocks;
                $stopY = $self->selectedRoom->yPosBlocks;

            } else {

                return @emptyList;
            }

        } elsif ($self->selectedRoomHash) {

            # Check every room in ->selectedRoomHash, and expand the borders of the selected area as
            #   we go
            $count = 0;
            foreach my $roomObj ($self->ivValues('selectedRoomHash')) {

                $count++;

                if ($count == 1) {

                    # This is the first room processed
                    $startX = $roomObj->xPosBlocks;
                    $startY = $roomObj->yPosBlocks;
                    $stopX = $roomObj->xPosBlocks;
                    $stopY = $roomObj->yPosBlocks;

                } else {

                    if ($roomObj->xPosBlocks < $startX) {
                        $startX = $roomObj->xPosBlocks;
                    } elsif ($roomObj->xPosBlocks > $stopX) {
                        $stopX = $roomObj->xPosBlocks;
                    }

                    if ($roomObj->yPosBlocks < $startY) {
                        $startY = $roomObj->yPosBlocks;
                    } elsif ($roomObj->yPosBlocks > $stopY) {
                        $stopY = $roomObj->yPosBlocks;
                    }
                }
            }

        } else {

            # No selected rooms at all
            return @emptyList;
        }

        # Return the coordinates of opposite corners of the area
        return ($startX, $startY, $stopX, $stopY);
    }

    sub checkRoomIsSelected {

        # Called by $self->canvasObjEventHandler
        # Checks, as quickly as possible, whether a room is selected, or not
        #
        # Expected arguments
        #   $roomObj    - The room to check
        #
        # Return values
        #   'undef' on improper arguments or if the room is not selected
        #   1 if the room is selected

        my ($self, $roomObj, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->checkRoomIsSelected', @_);
        }

        if ($self->selectedRoom && $self->selectedRoom eq $roomObj) {

            return 1;

        } elsif ( ! $self->selectedRoomHash) {

            return undef;

        } else {

            if ($self->ivExists('selectedRoomHash', $roomObj->number)) {
                return 1;
            } else {
                return undef;
            }
        }
    }

    sub selectRoomsInArea {

        # Called by $self->canvasObjEventHandler
        # Selects all the rooms on the current level of the current regionmap, within a specified
        #   area, and unselects all other rooms (on all levels)
        # (Also unselects any selected room tags, room guilds, exits, exit tags or labels)
        #
        # Expected arguments
        #   $startX, $startY, $stopX, $stopY
        #       - Grid coordinates of the top-left and bottom-right of the area, in which all rooms
        #           should be selected

        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $startX, $startY, $stopX, $stopY, $check) = @_;

        # Local variables
        my (
            $count, $lastRoomObj,
            @selectedRoomlist, @selectedRoomTagList, @selectedRoomGuildList, @selectedExitList,
            @selectedExitTagList, @selectedLabelList, @redrawList,
            %currentHash, %newHash, %selectedRoomHash,
        );

        # Check for improper arguments
        if (
            ! defined $startX || ! defined $startY || ! defined $stopX || ! defined $stopY
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->selectRoomsInArea', @_);
        }

        # The regionmap's ->gridRoomHash contains all the rooms in the current region. Import the
        #   hash
        %currentHash = $self->currentRegionmap->gridRoomHash;

        # Compile a new hash, in the same format as $self->selectedRoomHash, containing only those
        #   rooms in the selected area (and on the current level)
        $count = 0;
        foreach my $position (keys %currentHash) {

            my ($number, $roomObj);

            $number = $currentHash{$position};
            $roomObj = $self->worldModelObj->ivShow('modelHash', $number);

            if (
                $roomObj
                && $roomObj->zPosBlocks == $self->currentRegionmap->currentLevel
                && $roomObj->xPosBlocks >= $startX
                && $roomObj->xPosBlocks <= $stopX
                && $roomObj->yPosBlocks >= $startY
                && $roomObj->yPosBlocks <= $stopY
            ) {
                # Mark this room for selection
                $newHash{$number} = $roomObj;
                $count++;
                $lastRoomObj = $roomObj;
            }
        }

        # Compile a list of currently selected rooms, exits, room tags, room guilds and labels
        @selectedRoomlist = $self->compileSelectedRooms();
        @selectedRoomTagList = $self->compileSelectedRoomTags();
        @selectedRoomGuildList = $self->compileSelectedRoomGuilds();
        @selectedExitList = $self->compileSelectedExits();
        @selectedExitTagList = $self->compileSelectedExitTags();
        @selectedLabelList = $self->compileSelectedLabels();

        # Transfer the list of currently selected rooms into a hash, so that we can compare them
        #   with %newHash
        foreach my $ref (@selectedRoomlist) {

            $selectedRoomHash{$ref} = $ref;
        }

        # Check that the same room doesn't exist in %newHash and %selectedRoomHash. If so, delete
        #   the entry in %selectedRoomHash
        foreach my $obj (values %newHash) {

            if (exists $selectedRoomHash{$obj}) {

                delete $selectedRoomHash{$obj};
            }
        }

        # Set the IVs that contain all selected objects
        if ($count == 1) {

            $self->ivPoke('selectedRoom', $lastRoomObj);
            $self->ivEmpty('selectedRoomHash');

        } else {

            $self->ivUndef('selectedRoom');
            $self->ivPoke('selectedRoomHash', %newHash);
        }

        # Make sure there are no room tags, room guilds, exits, exit tags or labels selected
        $self->ivUndef('selectedRoomTag');
        $self->ivEmpty('selectedRoomTagHash');
        $self->ivUndef('selectedRoomGuild');
        $self->ivEmpty('selectedRoomGuildHash');
        $self->ivUndef('selectedExit');
        $self->ivEmpty('selectedExitHash');
        $self->ivUndef('selectedExitTag');
        $self->ivEmpty('selectedExitTagHash');
        $self->ivUndef('selectedLabel');
        $self->ivEmpty('selectedLabelHash');

        # Finally, re-draw all objects that have either been selected or unselected. Compile a list
        #   to send to ->doDraw, in the form (type, object, type, object, ...)
        foreach my $obj (values %newHash) {

            push (@redrawList, 'room', $obj);
        }

        foreach my $obj (values %selectedRoomHash) {

            push (@redrawList, 'room', $obj);
        }

        foreach my $obj (@selectedRoomTagList) {

            push (@redrawList, 'room_tag', $obj);
        }

        foreach my $obj (@selectedRoomGuildList) {

            push (@redrawList, 'room_guild', $obj);
        }

        foreach my $obj (@selectedExitList) {

            push (@redrawList, 'exit', $obj);
        }

        foreach my $obj (@selectedExitTagList) {

            push (@redrawList, 'exit_tag', $obj);
        }

        foreach my $obj (@selectedLabelList) {

            push (@redrawList, 'label', $obj);
        }

        # Actually redraw the affected objects
        $self->doDraw(@redrawList);

        # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
        $self->restrictWidgets();

        # Operation complete
        return 1;
    }

    sub checkMouseClick {

        # Called by $self->canvasEventHandler and ->canvasObjEventHandler
        # After an event caused by a mouse click, checks the event to find out whether it was a
        #   single/double/triple click, which button was used (left or right), and whether the SHIFT
        #   and/or CTRL keys were held down during the click
        #
        # Expected arguments
        #   $event  - The Gtk2::Gdk::Event caused by the mouse click
        #
        # Return values
        #   An empty list on improper arguments, or if $event wasn't cause by a single, double or
        #       triple-mouse click (but by something else - mouse motion, perhaps)
        #   Otherwise, returns a list in the form ($clickType, $button, $shiftFlag, $ctrlFlag):
        #       $clickType  - 'single', 'double' or 'triple'
        #       $button     - 'left' or 'right'
        #       $shiftFlag  - Set to TRUE if the SHIFT key was held down during the click, set to
        #                       FALSE otherwise
        #       $ctrlFlag   - Set to TRUE if the CTRL key was held down during the click, set to
        #                       FALSE otherwise

        my ($self, $event, $check) = @_;

        # Local variables
        my (
            $clickType, $button, $shiftFlag, $ctrlFlag,
            @emptyList,
        );

        # Check for improper arguments
        if (! defined $event || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->checkMouseClick', @_);
            return @emptyList;
        }

        # Set the type of click (single or double; ignore triple clicks)
        if ($event->type eq 'button-press') {

            $clickType = 'single';

        } elsif ($event->type eq '2button-press') {

            $clickType = 'double';

        } elsif ($event->type eq '3button-press') {

            $clickType = 'triple';

        } else {

            # Not an event we're interested in
            return @emptyList;
        }

        # Set the button
        if ($event->button == 1) {

            $button = 'left';

        } elsif ($event->button == 3) {

            $button = 'right';

        } else {

            # Not an event we're interested in
            return @emptyList;
        }

        # Check whether the SHIFT and/or CTRL keys were held down, when the mouse was clicked
        if ($event->state =~ m/shift-mask/) {
            $shiftFlag = TRUE;
        } else {
            $shiftFlag = FALSE;
        }

        if ($event->state =~ m/control-mask/) {
            $ctrlFlag = TRUE;
        } else {
            $ctrlFlag = FALSE;
        }

        return ($clickType, $button, $shiftFlag, $ctrlFlag);
    }

    sub deleteCanvasObj {

        # Called by numerous functions
        #
        # When a region object, room object, room tag, room guild, exit, exit tag or label is being
        #   drawn, redrawn or deleted from the world model, this function must be called
        # This function checks whether the model object is currently displayed on the map as one or
        #   more canvas objects and, if it is, destroys the canvas objects
        #
        # Expected arguments
        #   $type       - Set to 'region', 'room', 'room_tag', 'room_guild', 'exit', 'exit_tag' or
        #                   'label'
        #   $modelObj   - The GA::ModelObj::Region, GA::ModelObj::Room, GA::Obj::Exit or
        #                   GA::Obj::MapLabel being drawn /redrawn / deleted
        #
        # Optional arguments
        #   $deleteFlag - Set to TRUE if the object is being deleted from the world model, FALSE
        #                  (or 'undef') if not
        #
        # Return values
        #   'undef' on improper arguments, if there is no current regionmap or if the model object
        #       isn't currently displayed on the map
        #   1 otherwise

        my ($self, $type, $modelObj, $deleteFlag, $check) = @_;

        # Local variables
        my (
            $regionName, $regionmapObj, $posn, $listRef, $roomObj, $twinModelObj,
            @redrawList,
            %redrawHash,
        );

        # Check for improper arguments
        if (! defined $type || ! defined $modelObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->deleteCanvasObj', @_);
        }

        if ($self->currentRegionmap) {

            # Handle a region deletion
            if ($type eq 'region' && $deleteFlag) {

                # Reset the treeview, so that the deleted region is no longer visible in it
                $self->resetTreeView();

                # Because we can be sure that all of the region's children have now been deleted,
                #   we can go ahead and redraw the current regionmap right away
                $regionName = $modelObj->name;
                $regionmapObj = $self->worldModelObj->ivShow('regionmapHash', $regionName);

                if ($regionmapObj && $regionmapObj eq $self->currentRegionmap) {

                    # The currently displayed region was the one deleted. Redraw the canvas with no
                    #   map displayed
                    $self->resetMap(FALSE);

                } else {

                    # Redraw all rooms containing region exits (which automatically redraws the
                    #   exits)
                    if ($self->currentRegionmap->regionExitHash) {

                        # The same room can have more than one region exit; add affected rooms
                        #   to a hash to eliminate duplicates
                        foreach my $number ($self->currentRegionmap->ivKeys('regionExitHash')) {

                            my $exitObj = $self->worldModelObj->ivShow('exitModelHash', $number);
                            if ($exitObj) {

                                $redrawHash{$exitObj->parent} = undef;
                            }
                        }

                        # Having eliminated duplicates, compile the list of rooms to redraw
                        foreach my $number (keys %redrawHash) {

                            my $thisRoomObj = $self->worldModelObj->ivShow('modelHash', $number);
                            if ($thisRoomObj) {

                                push (@redrawList, 'room', $thisRoomObj);
                            }
                        }

                        # Redraw the affected rooms
                        $self->doDraw(@redrawList);

                        # Sensitise/desensitise menu bar/toolbar items, depending on current
                        #   conditions
                        $self->restrictWidgets();
                    }
                }

            # Handle a room draw/redraw/deletion
            } elsif ($type eq 'room') {

                if ($deleteFlag) {

                    # Unselect the room, if selected
                    $self->unselectObj(
                        $modelObj,
                        undef,          # A room, not a room tag or room guild
                        TRUE,           # No re-draw
                    );
                }

                # Get a string representing the room's position
                if (! defined $modelObj->xPosBlocks) {

                    # No canvas object to remove
                    return undef;

                } else {

                    $posn = $modelObj->xPosBlocks . '_' . $modelObj->yPosBlocks . '_'
                                . $modelObj->zPosBlocks;
                }

                # See if the room has been drawn on the current map by looking up its canvas object
                if ($self->ivExists('drawnRoomHash', $posn)) {

                    # Delete these canvas objects
                    $listRef = $self->ivShow('drawnRoomHash', $posn);
                    foreach my $canvasObj (@$listRef) {

                        $canvasObj->destroy();
                    }

                    $self->ivDelete('drawnRoomHash', $posn);
                    # (If there's an entry in the hash containing a sub-set of key-value pairs from
                    #   $self->drawnRoomHash, delete that entry, too)
                    $self->ivDelete('dummyRoomHash', $posn);
                }

                if ($self->ivExists('drawnRoomEchoHash', $posn)) {

                    # Delete these canvas objects
                    $listRef = $self->ivShow('drawnRoomEchoHash', $posn);
                    foreach my $canvasObj (@$listRef) {

                        $canvasObj->destroy();
                    }

                    $self->ivDelete('drawnRoomEchoHash', $posn);
                }

                # Delete the associated room tag, if there is one
                if ($self->ivExists('drawnRoomTagHash', $posn)) {

                    # Delete these canvas objects
                    $listRef = $self->ivShow('drawnRoomTagHash', $posn);
                    foreach my $canvasObj (@$listRef) {

                        $canvasObj->destroy();
                    }

                    $self->ivDelete('drawnRoomTagHash', $posn);
                }

                # Delete the associated room guild, if there is one
                if ($self->ivExists('drawnRoomGuildHash', $posn)) {

                    # Delete these canvas objects
                    $listRef = $self->ivShow('drawnRoomGuildHash', $posn);
                    foreach my $canvasObj (@$listRef) {

                        $canvasObj->destroy();
                    }

                    $self->ivDelete('drawnRoomGuildHash', $posn);
                }

                # Delete one or more associated room text items (the text drawn in the room's
                #   interior), if there are any
                if ($self->ivExists('drawnRoomTextHash', $posn)) {

                    $listRef = $self->ivShow('drawnRoomTextHash', $posn);
                    foreach my $canvasObj (@$listRef) {

                        $canvasObj->destroy();
                    }

                    $self->ivDelete('drawnRoomTextHash', $posn);
                }

            # Handle a room tag deletion
            } elsif ($type eq 'room_tag') {

                if ($deleteFlag) {

                    # Unselect the room tag, if selected
                    $self->unselectObj(
                        $modelObj,
                        'room_tag',
                        TRUE,           # No re-draw
                    );
                }

                # Get a string representing the room's position
                if (! defined $modelObj->xPosBlocks) {

                    # No canvas object to remove
                    return undef;

                } else {

                    $posn = $modelObj->xPosBlocks . '_' . $modelObj->yPosBlocks . '_'
                                . $modelObj->zPosBlocks;
                }

                # See if the room tag has been drawn on the current map by looking up its canvas
                #   object
                if ($self->ivExists('drawnRoomTagHash', $posn)) {

                    # Delete these canvas objects
                    $listRef = $self->ivShow('drawnRoomTagHash', $posn);
                    foreach my $canvasObj (@$listRef) {

                        $canvasObj->destroy();
                    }

                    $self->ivDelete('drawnRoomTagHash', $posn);
                }

            # Handle a room guild deletion
            } elsif ($type eq 'room_guild') {

                if ($deleteFlag) {

                    # Unselect the room guild, if selected
                    $self->unselectObj(
                        $modelObj,
                        'room_guild',
                        TRUE,           # No re-draw
                    );
                }

                # Get a string representing the room's position
                if (! defined $modelObj->xPosBlocks) {

                    # No canvas object to remove
                    return undef;

                } else {

                    $posn = $modelObj->xPosBlocks . '_' . $modelObj->yPosBlocks . '_'
                                . $modelObj->zPosBlocks;
                }

                # See if the room guild has been drawn on the current map by looking up its canvas
                #   object
                if ($self->ivExists('drawnRoomGuildHash', $posn)) {

                    # Delete these canvas objects
                    $listRef = $self->ivShow('drawnRoomGuildHash', $posn);
                    foreach my $canvasObj (@$listRef) {

                        $canvasObj->destroy();
                    }

                    $self->ivDelete('drawnRoomGuildHash', $posn);
                }

            # Handle an exit deletion
            } elsif ($type eq 'exit') {

                # Unselect the exit, if selected
                if ($deleteFlag) {

                    $self->unselectObj(
                        $modelObj,
                        undef,          # An exit, not an exit tag
                        TRUE,           # No re-draw
                    );
                }

                # See if the exit has been drawn on the current map by looking up its canvas object
                if ($self->ivExists('drawnExitHash', $modelObj->number)) {

                    # Delete these canvas objects
                    $listRef = $self->ivShow('drawnExitHash', $modelObj->number);
                    foreach my $canvasObj (@$listRef) {

                        $canvasObj->destroy();
                    }

                    $self->ivDelete('drawnExitHash', $modelObj->number);
                }

                # Delete the associated exit tag, if there is one
                if ($self->ivExists('drawnExitTagHash', $modelObj->number)) {

                    # Delete these canvas objects
                    $listRef = $self->ivShow('drawnExitTagHash', $modelObj->number);
                    foreach my $canvasObj (@$listRef) {

                        $canvasObj->destroy();
                    }

                    $self->ivDelete('drawnExitTagHash', $modelObj->number);
                }

                # Delete the associated exit ornaments, if there are any
                if ($self->ivExists('drawnOrnamentHash', $modelObj->number)) {

                    # Delete these canvas objects
                    $listRef = $self->ivShow('drawnOrnamentHash', $modelObj->number);
                    foreach my $canvasObj (@$listRef) {

                        $canvasObj->destroy();
                    }

                    $self->ivDelete('drawnOrnamentHash', $modelObj->number);
                }

            # Handle an exit tag deletion
            } elsif ($type eq 'exit_tag') {

                if ($deleteFlag) {

                    # Unselect the exit tag, if selected
                    $self->unselectObj(
                        $modelObj,
                        'exit_tag',
                        TRUE,           # No re-draw
                    );
                }

                # See if the eixt tag has been drawn on the current map by looking up its canvas
                #   object
                if ($self->ivExists('drawnExitTagHash', $modelObj->number)) {

                    # Delete these canvas objects
                    $listRef = $self->ivShow('drawnExitTagHash', $modelObj->number);
                    foreach my $canvasObj (@$listRef) {

                        $canvasObj->destroy();
                    }

                    $self->ivDelete('drawnExitTagHash', $modelObj->number);
                }

            # Handle a label deletion
            } elsif ($type eq 'label') {

                if ($deleteFlag) {

                    # Unselect the label, if selected
                    $self->unselectObj(
                        $modelObj,
                        undef,          # A label, not a room tag or room guild
                        TRUE,           # No re-draw
                    );
                }

                # See if the label has been drawn on the current map by looking up its canvas object
                if ($self->ivExists('drawnLabelHash', $modelObj->number)) {

                    # Delete these canvas objects
                    $listRef = $self->ivShow('drawnLabelHash', $modelObj->number);
                    foreach my $canvasObj (@$listRef) {

                        $canvasObj->destroy();
                    }

                    $self->ivDelete('drawnLabelHash', $modelObj->number);
                }

            } else {

                # Unrecognised object type
                return undef;
            }
        }

        return 1;
    }

    sub showTooltips {

        # Called by $self->setupCanvasObjEvent
        # Shows tooltips (assumes the GA::Obj::WorldModel->showTooltipsFlag is TRUE)
        #
        # Expected arguments
        #   $type       - What type of canvas object caused the mouse event - 'room', 'room_tag',
        #                   'room_guild', 'exit', 'exit_tag' or 'label'
        #   $canvasObj  - The canvas object itself
        #   $modelObj   - The GA::ModelObj::Room, GA::Obj::Exit or GA::Obj::MapLabel which
        #                   corresponds to the canvas object $canvasObj
        #
        # Return values
        #   'undef' on improper arguments or if the Automapper window isn't ready and active
        #   1 otherwise

        my ($self, $type, $canvasObj, $modelObj, $check) = @_;

        # Local variables
        my ($xPos, $yPos, $label);

        # Check for improper arguments
        if (! defined $type || ! defined $canvasObj || ! defined $modelObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->showTooltips', @_);
        }

        # Don't show tooltips if the Automapper window isn't ready and active
        if (! $self->canvasFrame || ! $self->winWidget->is_active()) {

            return undef;
        }

        # Get the label to draw
        $label = $self->setTooltips($type, $modelObj);
        if ($label) {

            $self->canvasFrame->set_tooltip_text($label);

            $self->ivPoke('canvasTooltipObj', $canvasObj);
            $self->ivPoke('canvasTooltipObjType', $type);
            $self->ivPoke('canvasTooltipFlag', TRUE);
        }

        return 1;
    }

    sub hideTooltips {

        # Called by $self->setupCanvasObjEvent and several other functions
        # Hides tooltips, if visible
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->hideTooltips', @_);
        }

        # Hide tooltips, if visible
        if ($self->canvasFrame && $self->canvasTooltipObj) {

            $self->canvasFrame->set_tooltip_text('');

            $self->ivUndef('canvasTooltipObj');
            $self->ivUndef('canvasTooltipObjType');
            $self->ivPoke('canvasTooltipFlag', FALSE);
        }

        return 1;
    }

    sub setTooltips {

        # Called by $self->showTooltips
        # Compiles the text to show in the tooltips window, and returns it
        #
        # Expected arguments
        #   $type       - What type of canvas object caused the mouse event - 'room', 'room_tag',
        #                   'room_guild', 'exit', 'exit_tag' or 'label'
        #   $modelObj   - The GA::ModelObj::Room, GA::Obj::Exit or GA::Obj::MapLabel which
        #                   corresponds to the canvas object $canvasObj
        #
        # Return values
        #   'undef' on improper arguments
        #   Otherwise returns the text to display in the tooltips window

        my ($self, $type, $modelObj, $check) = @_;

        # Local variables
        my (
            $label, $vNum, $name, $area, $worldX, $worldY, $worldZ, $text, $flag, $standardDir,
            $abbrevDir, $parentRoomObj, $destRoomObj, $twinExitObj, $xPos, $yPos,
        );

        # Check for improper arguments
        if (! defined $type || ! defined $modelObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setTooltips', @_);
        }

        if ($type eq 'room') {

            $label = "Room #" . $modelObj->number;
            if ($modelObj->roomTag) {

                $label .= " \'" . $modelObj->roomTag . "\'";
            }

            # Show the room's coordinates on the map
            $label .= " (" . $modelObj->xPosBlocks . ", " . $modelObj->yPosBlocks;
            $label .= ", " . $modelObj->zPosBlocks . ")";

            # Show the world's room vnum, etc (if known)
            if ($modelObj->protocolRoomHash) {

                $label .= "\nWorld:";

                $vNum = $modelObj->ivShow('protocolRoomHash', 'vNum');
                if (defined $vNum) {

                    $label .=  "#" . $vNum;
                }

                $name = $modelObj->ivShow('protocolRoomHash', 'name');
                if (defined $name) {

                    $label .= " " . $name;
                }

                $area = $modelObj->ivShow('protocolRoomHash', 'area');
                if (defined $area) {

                    $label .= " " . $area;
                }

                $worldZ = $modelObj->ivShow('protocolRoomHash', 'zpos');
                if (! defined $worldZ) {

                    # (Guard against X being defined, but Y/Z not being defined, etc
                    $worldZ = "?";
                }

                $worldY = $modelObj->ivShow('protocolRoomHash', 'ypos');
                if (! defined $worldY) {

                    $worldY = "?";
                }

                $worldX = $modelObj->ivShow('protocolRoomHash', 'xpos');
                if (defined $worldX) {

                    $label .= " $worldX-$worldY-$worldY";
                }
            }

            # Add the room title (if there is one)
            if ($modelObj->titleList) {

                # Using the first item in the list, use the whole title
                $label .= "\n(T) " . $modelObj->ivFirst('titleList');
                $flag = TRUE;
            }

            # Add a (verbose) description (if there is one)
            if ($modelObj->descripHash) {

                # Use the description matching the current light status, if it exists
                if (
                    $self->worldModelObj->lightStatus
                    && $modelObj->ivExists('descripHash', $self->worldModelObj->lightStatus)
                ) {
                    $text = $modelObj->ivShow('descripHash', $self->worldModelObj->lightStatus);

                } else {

                    # Cycle through light statuses, looking for a matching verbose description
                    OUTER: foreach my $status ($self->worldModelObj->lightStatusList) {

                        if ($modelObj->ivExists('descripHash', $status)) {

                            $text = $modelObj->ivShow('descripHash', $status);
                            last OUTER;
                        }
                    }
                }

                if ($text) {

                    # Split the text into two lines of no more than 40 characters. The TRUE
                    #   arguments tells the function to append an ellipsis, if any text is
                    #   removed
                    $text = $self->splitText($text, 2, 40, TRUE);
                    $label .= "\n(D) " . $text;

                    $flag = TRUE;
                }
            }

            # Add the room's source code path (if set)
            if ($modelObj->sourceCodePath) {

                $label .= "\n(S) " . $modelObj->sourceCodePath;
                $flag = TRUE;
            }

            # If there is no title or (verbose) description available, show an explanatory message
            if (! $flag) {

                $label .= "\n(No description available)";
            }

        } elsif ($type eq 'room_tag') {

            $label = "Room tag \'" . $modelObj->roomTag . "\'";
            $label .= "\n  Room #" . $modelObj->number;

            # Show the room's coordinates on the map
            $label .= " (" . $modelObj->xPosBlocks . ", " . $modelObj->yPosBlocks;
            $label .= ", " . $modelObj->zPosBlocks . ")";

        } elsif ($type eq 'room_guild') {

            $label = "Room guild \'" . $modelObj->roomGuild . "\'";
            $label .= "\n  Room #" . $modelObj->number;

            # Show the room's coordinates on the map
            $label .= " (" . $modelObj->xPosBlocks . ", " . $modelObj->yPosBlocks;
            $label .= ", " . $modelObj->zPosBlocks . ")";

        } elsif ($type eq 'exit') {

            $label = "Exit #" . $modelObj->number . " \'" . $modelObj->dir . "\'";

            # Get the standard form of the exit's direction so we can compare it with the exit's
            #   map direction, ->mapDir
            $standardDir = $self->session->currentDict->ivShow('combRevDirHash', $modelObj->dir);
            if (
                $standardDir
                && $modelObj->mapDir
                && $modelObj->mapDir ne $standardDir
            ) {
                # Convert the allocated map direction to its abbreviated form
                $abbrevDir = $self->session->currentDict->ivShow(
                    'primaryAbbrevHash',
                    $modelObj->mapDir,
                );

                if (! $abbrevDir) {

                    # We're forced to use the unabbreviated form
                    $abbrevDir = $modelObj->mapDir;
                }

                $label .= " (> " . $abbrevDir . ")";
            }

            $parentRoomObj = $self->worldModelObj->ivShow('modelHash', $modelObj->parent);
            $label .= "\n  Parent room #" . $parentRoomObj->number;

            if ($modelObj->destRoom) {

                $destRoomObj = $self->worldModelObj->ivShow('modelHash', $modelObj->destRoom);
                $label .= "\n  Destination room #" . $destRoomObj->number;
            }

            if ($modelObj->twinExit) {

                $twinExitObj = $self->worldModelObj->ivShow('exitModelHash', $modelObj->twinExit);
                $label .= "\n  Twin exit #" . $twinExitObj->number . " \'" . $twinExitObj->dir
                            . "\'";
            }

            if ($modelObj->info) {

                $label .= "\n  Info: " . $modelObj->info;
            }

        } elsif ($type eq 'exit_tag') {

            $label = "Exit tag \'" . $modelObj->exitTag . "\'";
            $label .= "\n  Exit #" . $modelObj->number . " \'" . $modelObj->dir . "\'";

        } elsif ($type eq 'label') {

            $label = "Label #" . $modelObj->number;
            # Convert the label's coordinates in pixels to gridblocks
            $xPos = int($modelObj->xPosPixels / $self->currentRegionmap->blockWidthPixels);
            $yPos = int($modelObj->yPosPixels / $self->currentRegionmap->blockHeightPixels);
            $label .= " (" . $xPos . ", " . $yPos . ", " . $modelObj->level . ")";

            $label .= "\n  \'" . $modelObj->name . "\'";

        } else {

            # Failsafe: empty string
            $label = "";
        }

        return $label;
    }

    sub splitText {

        # Called by $self->setTooltips
        # Splits a line of text into a specified number of rows separated by line break characters
        # Each line will be no longer than a specified number of characters, but words are not
        #   split (unless they're longer than the line)
        # Discards any extra text and optionally appends an ellipsis if doing so
        #
        # Expected arguments
        #   $line           - The line of text to split
        #   $rows           - The number of rows (minimum 1)
        #   $columns        - The maximum number of columns per line (minimum 10)
        #
        # Optional arguments
        #   $ellipsisFlag   - If set to TRUE, an ellipsis is appended if any text is discarded.
        #                       If set to FALSE (or 'undef'), no ellipsis is appended
        #
        # Return values
        #   The unmodified value of $text on improper arguments, or if $rows and/or $columns are
        #       invalid values
        #   Otherwise, returns the modified string

        my ($self, $line, $rows, $columns, $ellipsisFlag, $check) = @_;

        # Local variables
        my (
            $newLine,
            @array,
        );

        # Check for improper arguments
        if (! defined $line || ! defined $rows || ! defined $columns || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->splitText', @_);
        }

        # Check for valid values of $rows and $columns
        if ($rows < 1 || $columns < 10) {

            return $line;
        }

        # Split the text
        until ((length $line) <= $columns || scalar @array >= $rows) {

            my $i = 0;

            # The line is going to be split near character number $columns. Find the space (or tab)
            #   nearest to the end of the line
            OUTER: for ($i = $columns; $i > 0; $i--) {

                if (substr($line, $i, 1) eq " " || substr($line, $i, 1) eq "\t") {

                    last OUTER;
                }
            }

            # A space (or tab) was found. Split the line there
            if ($i > 1) {

                push @array, substr($line, 0, $i);
                $line = substr($line, ($i + 1));

            # There is no space at which the line can be split. Split a word (using a hyphen)
            } else {

                push @array, substr($line, 0, ($columns) . '-');
                $line = substr($line, $columns);
            }
        }

        # Join the split lines together with newline characters
        if ($rows > 1) {

            $newLine = join("\n", @array);
            # If we're discarding extra text, append an ellipsis (if allowed)
            if ($line && $ellipsisFlag) {

                $newLine .= '...';
            }

        } else {

            $newLine = $line;
        }

        return $newLine;
    }

    # Menu 'File' column callbacks

    sub importModelCallback {

        # Called by $self->enableFileColumn
        # Imports a world model file specified by the user and (if successful) loads it into memory
        #   (a combination of ';importfiles' and ';load -m')
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->importModelCallback', @_);
        }

        # (No standard callback checks for this function)

        # Watch out for file operation failures
        $axmud::CLIENT->set_fileFailFlag(FALSE);
        # Allow a world model, associated with a world profile with a different name, to be imported
        #   into the current world's file structures (but only if the archive file contains only a
        #   world model)
        $self->session->set_transferWorldModelFlag(TRUE);

        # Import a file, specified by the user
        if (
            $self->session->pseudoCmd('importfiles')
            && ! $axmud::CLIENT->fileFailFlag
        ) {
            # The world model data has been incorporated into Axmud's data files, but not loaded
            #   into memory. Load it into memory now
            if (
                $self->session->pseudoCmd('load -m')
                && ! $axmud::CLIENT->fileFailFlag
            ) {
                # Make sure the world model object has the right parent world set, after the file
                #   import
                $self->session->worldModelObj->{_parentWorld} = $self->session->currentWorld->name;
                # Save the world model, to make sure the file has the right parent world set, too
                $self->session->pseudoCmd('save -f -m');
            }
        }

        # Reset the flag
        $self->session->set_transferWorldModelFlag(FALSE);

        return 1;
    }

    sub exportModelCallback {

        # Called by $self->enableFileColumn
        # Saves the current world model and (if successful) exports the 'worldmodel' file to a
        #   folder specified by the user (a combination of ';save -m' and ';exportfiles -m'
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($fileObj, $choice);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->exportModelCallback', @_);
        }

        # (No standard callback checks for this function)

        # If the world model data in memory is unsaved, prompt whether to save it first
        $fileObj = $self->session->ivShow('sessionFileObjHash', 'worldmodel');
        if ($fileObj && $fileObj->modifyFlag) {

            # Watch out for file operation failures
            $axmud::CLIENT->set_fileFailFlag(FALSE);

            # Prompt the user
            $choice = $self->showMsgDialogue(
                'Unsaved world model',
                'question',
                'The world model in memory is not saved. Do you want to save it before exporting?'
                . ' (If you choose \'no\', the previously saved world model file will be exported'
                . ' instead)',
                'yes-no',
            );

            if ($choice eq 'yes') {

                # Save the world model
                $self->session->pseudoCmd('save -m', 'win_error');

                if ($axmud::CLIENT->fileFailFlag) {

                    # Something went wrong; don't attempt to export anything
                    return 1;
                }
            }
        }

        # Export the world model data file
        $self->session->pseudoCmd(
            'exportfiles -m ' . $self->session->currentWorld->name,
            'win_error',
        );

        return 1;
    }

    # Menu 'Edit' column callbacks

    sub selectAllCallback {

        # Called by $self->enableEditColumn
        # Selects rooms, exits, room tags, room guilds and labels (or everything)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $obj    - Set to 'room', 'exit', 'room_tag', 'room_guild', or 'label'. If not defined,
        #               selects everything
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $obj, $check) = @_;

        # Local variables
        my (
            $count,
            @roomList, @exitList, @roomTagList, @roomGuildList, @labelList,
            %roomHash, %exitHash, %roomTagHash, %roomGuildHash, %labelHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->selectAllCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Make sure there are no rooms, exits, room tags or labels selected
        $self->ivUndef('selectedRoom');
        $self->ivEmpty('selectedRoomHash');
        $self->ivUndef('selectedExit');
        $self->ivEmpty('selectedExitHash');
        $self->ivUndef('selectedRoomTag');
        $self->ivEmpty('selectedRoomTagHash');
        $self->ivUndef('selectedRoomGuild');
        $self->ivEmpty('selectedRoomGuildHash');
        $self->ivUndef('selectedLabel');
        $self->ivEmpty('selectedLabelHash');

        # Select all rooms, exits, room tags, room guilds and/or labels
        if (! defined $obj || $obj eq 'room') {

            # $self->currentRegionmap->gridRoomHash contains all the rooms in the regionmap
            # Get a list of world model numbers for each room
            @roomList = $self->currentRegionmap->ivValues('gridRoomHash');
        }

        if (! defined $obj || $obj eq 'exit') {

            # ->gridExitHash contains all the drawn exits
            # Get a list of exit model numbers for each exit
            @exitList = $self->currentRegionmap->ivKeys('gridExitHash');
        }

        if (! defined $obj || $obj eq 'room_tag') {

            # ->gridRoomTagHash contains all the rooms with room tags
            # Get a list of world model numbers for the rooms containing room tag
            @roomTagList = $self->currentRegionmap->ivValues('gridRoomTagHash');
        }

        if (! defined $obj || $obj eq 'room_guild') {

            # ->gridRoomGuildHash contains all the rooms with room guilds
            # Get a list of world model numbers for the roomw containing room guilds
            @roomGuildList = $self->currentRegionmap->ivValues('gridRoomGuildHash');
        }

        if (! defined $obj || $obj eq 'label') {

            # ->gridLabelHash contains all the labels
            # Get a list of blessed references to GA::Obj::MapLabel objects
            @labelList = $self->currentRegionmap->ivValues('gridLabelHash');
        }

        # The IVs that store selected objects behave differently when there is one selected object
        #   and when there is more than one. Count how many selected objects we have
        $count = (scalar @roomList) + (scalar @exitList) + (scalar @roomTagList)
                    + (scalar @roomGuildList) + (scalar @labelList);

        # Select a single object...
        if ($count == 1) {

            if (@roomList) {

                # Select the blessed reference of a GA::ModelObj::Room
                $self->ivPoke(
                    'selectedRoom',
                    $self->worldModelObj->ivShow('modelHash', $roomList[0]),
                );

            } elsif (@exitList) {

                # Select the blessed reference of a GA::Obj::Exit
                $self->ivPoke(
                    'selectedExit',
                    $self->worldModelObj->ivShow('exitModelHash', $exitList[0]),
                );

            } elsif (@roomTagList) {

                # Select the blessed reference of the GA::ModelObj::Room which contains the room
                #   tag
                $self->ivPoke(
                    'selectedRoomTag',
                    $self->worldModelObj->ivShow('modelHash', $roomTagList[0]),
                );

            } elsif (@roomGuildList) {

                # Select the blessed reference of the GA::ModelObj::Room which contains the room
                #   guild
                $self->ivPoke(
                    'selectedRoomGuild',
                    $self->worldModelObj->ivShow('modelHash', $roomGuildList[0]),
                );

            } elsif (@labelList) {

                # Select the blessed reference of a GA::Obj::MapLabel
                $self->ivPoke('selectedLabel', $labelList[0]);
            }

        # ...or select multiple objects
        } else {

            # (For speed, update local variable hashes, before storing the whole hash(es) in IVs
            foreach my $number (@roomList) {

                $roomHash{$number} = $self->worldModelObj->ivShow('modelHash', $number);
            }

            foreach my $number (@exitList) {

                $exitHash{$number} = $self->worldModelObj->ivShow('exitModelHash', $number);
            }

            foreach my $number (@roomTagList) {

                $roomTagHash{$number} = $self->worldModelObj->ivShow('modelHash', $number);
            }

            foreach my $number (@roomGuildList) {

                $roomGuildHash{$number} = $self->worldModelObj->ivShow('modelHash', $number);
            }

            foreach my $obj (@labelList) {

                $labelHash{$obj->number} = $obj;
            }

            # Update the IVs
            $self->ivPoke('selectedRoomHash', %roomHash);
            $self->ivPoke('selectedExitHash', %exitHash);
            $self->ivPoke('selectedRoomTagHash', %roomTagHash);
            $self->ivPoke('selectedRoomGuildHash', %roomGuildHash);
            $self->ivPoke('selectedLabelHash', %labelHash);
        }

        # Redraw the current level, to show all the changes
        $self->drawRegion();

        # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
        $self->restrictWidgets();

        return 1;
    }

    sub moveSelectedRoomsCallback {

        # Called by $self->enableEditColumn
        # Prompts the user to select the direction in which to move the selected rooms
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the user
        #       declines to specify a valid distance and direction or if the move operation fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $dictObj, $distance, $choice, $standardDir,
            @shortList, @longList, @dirList, @customList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->moveSelectedRoomsCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || (! $self->selectedRoom && ! $self->selectedRoomHash)) {

            return undef;
        }

        # Must reset free click mode, so 'move rooms' and 'move rooms to click' can't be combined
        #   accidentally
        $self->ivPoke('freeClickMode', 'default');

        # Import the current dictionary
        $dictObj = $self->session->currentDict;

        # Prepare a list of standard primary directions. Whether we include 'northnortheast', etc,
        #   depends on the current value of $self->worldModelObj->showAllPrimaryFlag
        @shortList = qw(north northeast east southeast south southwest west northwest up down);
        # (For convenience, put the longest directions at the end)
        @longList = qw(
            north northeast east southeast south southwest west northwest up down
            northnortheast eastnortheast eastsoutheast southsoutheast
            southsouthwest westsouthwest westnorthwest northnorthwest
        );

        if ($self->worldModelObj->showAllPrimaryFlag) {
            @dirList = @longList;
        } else {
            @dirList = @shortList;
        }

        # Get a list of (custom) primary directions, in the standard order
        foreach my $key (@dirList) {

            push (@customList, $dictObj->ivShow('primaryDirHash', $key));
        }

        # Prompt the user for a distance and a direction
        ($distance, $choice) = $self->showDoubleComboDialogue(
            'Move selected rooms',
            'Enter a distance (in gridblocks)',
            'Select the direction of movement',
            \@customList,
        );

        # If the 'cancel' button was clicked, $distance will be 'undef'. The user might also have
        #   entered the distance 0. In either case, we don't move anything
        if (! $distance) {

            # Operation cancelled
            return undef;

        } else {

            # Check that the distance is a positive integer
            if (! $axmud::CLIENT->intCheck($distance, 1)) {

                # Open a 'dialogue' window to explain the problem
                $self->showMsgDialogue(
                    'Move selected rooms',
                    'error',
                    'The distance must be a positive integer',
                    'ok',
                );

                return undef;
            }

            # $dir is a custom primary direction; convert it into the standard primary direction
            $standardDir = $dictObj->ivShow('combRevDirHash', $choice);

            # Move the selected room(s)
            return $self->moveRoomsInDir($distance, $standardDir);
        }
    }

    sub findRoomCallback {

        # Called by $self->enableEditColumn
        # Prompts the user to enter the world model number of a room, and then selects the room
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the user declines to specify a room number
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($msg, $number, $obj, $regionObj);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->findRoomCallback', @_);
        }

        # (No standard callback checks for this function)

        # Check that the world model isn't empty
        if (! $self->worldModelObj->modelHash) {

            return $self->showMsgDialogue(
                'Find room',
                'error',
                'The world model is currently empty',
                'ok',
            );
        }

        # Prompt the user for a room number
        $msg = 'Enter the room\'s number in the world model';
        if ($self->worldModelObj->modelObjCount > 1) {

            $msg .= ' (range 1-' . $self->worldModelObj->modelObjCount . ')';
        }

        $number = $self->showEntryDialogue(
            'Find room',
            $msg,
        );

        # We need a positive integer
        if (! $axmud::CLIENT->intCheck($number, 1)) {

            # Do nothing
            return undef;
        }

        # Does the corresponding world model object exist?
        if (! $self->worldModelObj->ivExists('modelHash', $number)) {

            return $self->showMsgDialogue(
                'Find room',
                'error',
                'There is no world model object #' . $number,
                'ok',
            );

        } else {

            $obj = $self->worldModelObj->ivShow('modelHash', $number);
        }

        if ($obj->category ne 'room') {

            if ($obj->category eq 'armour') {

                $msg = 'The world model object #' . $number . ' isn\'t a room (but an '
                        . $obj->category . ')';

            } else {

                $msg = 'The world model object #' . $number . ' isn\'t a room (but a '
                    . $obj->category . ')';
            }

            return $self->showMsgDialogue(
                'Find room',
                'error',
                $msg,
                'ok',
            );
        }

        if (! defined $obj->xPosBlocks) {

            # Room not in a regionmap - very unlikely, but we'll display a message anyway
            return $self->showMsgDialogue(
                'Find room',
                'error',
                'The world model object #' . $number . ' exists, but isn\'t on the map',
                'ok',
            );
        }

        # If there isn't a current regionmap, show the one containing the room
        $regionObj = $self->worldModelObj->ivShow('modelHash', $obj->parent);
        if (! $self->currentRegionmap && $regionObj) {

            $self->setCurrentRegion($regionObj->name);
        }

        # If there is (now) a current regionmap, select the room (even if it's not in the same
        #   region)
        if ($self->currentRegionmap) {

            $self->setSelectedObj(
                [$obj, 'room'],
                FALSE,          # Select this object; unselect all other objects
            );

            # Centre the map on the room
            $self->centreMapOverRoom($self->selectedRoom);
        }

        # Prepare a message to display
        $msg = "World model room #" . $number . "\n\n";

        $regionObj = $self->worldModelObj->ivShow('modelHash', $obj->parent);
        if ($regionObj) {
            $msg .= "   Region: '" . $regionObj->name . "'\n";
        } else {
            $msg .= "   Region: <none>\n";
        }

        $msg .= "   X-pos: " . $obj->xPosBlocks . "\n";
        $msg .= "   Y-pos: " . $obj->yPosBlocks . "\n";
        $msg .= "   Level: " . $obj->zPosBlocks . "\n";

        # Display info about the room
        return $self->showMsgDialogue(
            'Find room',
            'info',
            $msg,
            'ok',
        );
    }

    sub findExitCallback {

        # Called by $self->enableEditColumn
        # Prompts the user to enter the exit model number of an exit, and then selects the exit
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the user declines to specify an room number
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($msg, $number, $exitObj, $roomObj, $regionObj);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->findExitCallback', @_);
        }

        # (No standard callback checks for this function)

        # Check that the exit model isn't empty
        if (! $self->worldModelObj->exitModelHash) {

            return $self->showMsgDialogue(
                'Find exit',
                'error',
                'The exit model is currently empty',
                'ok',
            );
        }

        # Prompt the user for an exit number
        $msg = 'Enter the exit\'s number in the exit model';
        if ($self->worldModelObj->exitObjCount > 1) {

            $msg .= ' (range 1-' . $self->worldModelObj->exitObjCount . ')';
        }


        $number = $self->showEntryDialogue(
            'Find exit',
            $msg,
        );

        # We need a positive integer
        if (! $axmud::CLIENT->intCheck($number, 1)) {

            # Do nothing
            return undef;
        }

        # Does the corresponding exit model object exist?
        if (! $self->worldModelObj->ivExists('exitModelHash', $number)) {

            return $self->showMsgDialogue(
                'Find exit',
                'error',
                'There is no exit model object #' . $number,
                'ok',
            );

        } else {

            # Get the blessed reference of the exit object and its parent room object
            $exitObj = $self->worldModelObj->ivShow('exitModelHash', $number);
            $roomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->parent);
        }

        if (! defined $roomObj->xPosBlocks) {

            # Parent room not in a regionmap - rather unlikely, but we'll display a message anyway
            return $self->showMsgDialogue(
                'Find exit',
                'error',
                'The exit\'s parent room (#' . $roomObj->number . ') exists, but isn\'t on the map',
                'ok',
            );
        }

        # If there isn't a current regionmap, show the one containing the exit
        $regionObj = $self->worldModelObj->ivShow('modelHash', $roomObj->parent);
        if (! $self->currentRegionmap && $regionObj) {

            $self->setCurrentRegion($regionObj->name);
        }

        # If there is (now) a current regionmap, select the exit (even if it's not in that region)
        if ($self->currentRegionmap) {

            $self->setSelectedObj(
                [$exitObj, 'exit'],
                FALSE,          # Select this object; unselect all other objects
            );

            # Centre the map on the parent room
            $self->centreMapOverRoom($roomObj);
        }

        # Prepare a message to display
        $msg = "Exit model object #" . $number . "\n\n";
        $msg .= "   Dir: " . $exitObj->dir . "\n";
        if ($exitObj->mapDir) {
            $msg .= "   Map dir: " . $exitObj->mapDir . "\n";
        } else {
            $msg .= "   Map dir: unallocatable\n";
        }

        $msg .= "   Parent room: #" . $roomObj->number . "\n";

        if ($regionObj) {
            $msg .= "   Region: '" . $regionObj->name . "'\n";
        } else {
            $msg .= "   Region: <none>\n";
        }

        $msg .= "   X-pos: " . $roomObj->xPosBlocks . "\n";
        $msg .= "   Y-pos: " . $roomObj->yPosBlocks . "\n";
        $msg .= "   Level: " . $roomObj->zPosBlocks . "\n";

        # Display info about the exit
        return $self->showMsgDialogue(
            'Find exit',
            'info',
            $msg,
            'ok',
        );
    }

    sub resetVisitsCallback {

        # Called by $self->enableEditColumn
        # Prompts the user to ask the character(s) and region(s) in which character visit counts
        #   should be reset
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the user chooses 'cancel' in the 'dialogue' window or
        #       if no characters/regions are found
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($result, $charListRef, $regionListRef, $roomCount, $deleteCount);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->resetVisitsCallback', @_);
        }

        # (No standard callback checks for this function)

        # Prompt the user
        ($result, $charListRef, $regionListRef) = $self->promptVisits();
        if (! $result) {

            # User closed the window, or clicked the 'cancel' button
            return undef;

        } elsif (($result == 1 && ! @$charListRef) || ! @$regionListRef) {

            $self->showMsgDialogue(
                'Reset character visits',
                'error',
                'No characters and/or regions found',
                'ok',
            );

            return undef;
        }

        $roomCount = 0;
        $deleteCount = 0;

        # Deal with non-profile characters
        if ($result == 2) {

            foreach my $regionmapObj (@$regionListRef) {

                foreach my $roomNum ($regionmapObj->ivValues('gridRoomHash')) {

                    my $roomObj = $self->worldModelObj->ivShow('modelHash', $roomNum);
                    $roomCount++;

                    foreach my $char ($roomObj->ivKeys('visitHash')) {

                        my $profObj = $self->session->ivShow('profHash', $char);

                        if (! $profObj || $profObj->category ne 'char') {

                            # The character which visited this room no longer exists as a character
                            #   profile
                            $self->worldModelObj->resetVisitCount(
                                TRUE,       # Update Automapper windows now
                                $roomObj,
                                $char,
                            );

                            $deleteCount++;
                        }
                    }
                }
            }

        # Deal with profile characters
        } else {

            foreach my $regionmapObj (@$regionListRef) {

                foreach my $roomNum ($regionmapObj->ivValues('gridRoomHash')) {

                    my $roomObj = $self->worldModelObj->ivShow('modelHash', $roomNum);
                    $roomCount++;

                    foreach my $char (@$charListRef) {

                        if ($roomObj->ivExists('visitHash', $char)) {

                            # Remove this character's visits from the room
                            $self->worldModelObj->resetVisitCount(
                                TRUE,       # Update Automapper windows now
                                $roomObj,
                                $char,
                            );

                            $deleteCount++;
                        }
                    }
                }
            }
        }

        # Show confirmation
        return $self->showMsgDialogue(
            'Reset character visits',
            'info',
            'Operation complete (rooms: ' . $roomCount . ', records deleted: ' . $deleteCount . ')',
            'ok',
        );
    }

    sub resetRemoteCallback {

        # Called by $self->enableEditColumn
        # Prompts the user before resetting remote room/exit data supplied by MSDP/MXP
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the user chooses 'cancel' in the 'dialogue' window
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $choice;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->resetRemoteCallback', @_);
        }

        # (No standard callback checks for this function)

        # Prompt the user
        $choice = $self->showMsgDialogue(
            'Reset remote data',
            'question',
            "Are you sure you want ro reset room/\nexit data supplied by the world?",
            'yes-no',
        );

        if ($choice && $choice eq 'yes') {

            # TRUE to redraw maps now
            $self->worldModelObj->resetRemoteData(TRUE);

            $self->showMsgDialogue(
                'Reset remote data',
                'info',
                'Remote data has been reset',
                'ok',
            );
        }

        return 1;
    }

    # Menu 'View' column callbacks

    sub changeCharDrawnCallback {

        # Called by $self->enableViewColumn
        # In GA::Obj::WorldModel->roomInteriorMode 'visit_count', changes which character's visits
        #   are drawn
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $currentString, $choice, $redrawFlag, $choiceObj,
            @profList, @sortedList, @comboList,
            %comboHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->changeCharDrawnCallback',
                @_,
            );
        }

        # (No standard callback checks for this function)

        # Get a sorted list of character profiles, not including the current character (if any)
        foreach my $profObj ($self->session->ivValues('profHash')) {

            if (
                $profObj->category eq 'char'
                && (! $self->session->currentChar || $self->session->currentChar ne $profObj)
            ) {
                push (@profList, $profObj);
            }
        }

        @sortedList = sort {lc($a->name) cmp lc($b->name)} (@profList);

        # Prepare a list to show in a combo box. At the same time, compile a hash in the form:
        #   $hash{combo_box_string} = blessed_reference_of_equivalent_profile
        foreach my $profObj (@sortedList) {

            push (@comboList, $profObj->name);
            $comboHash{$profObj->name} = $profObj;
        }

        # Add the current character (if there is one) to top of the combo
        if ($self->session->currentChar) {

            $currentString = '<Use current character>';
            unshift (@comboList, $currentString);
        }

        # Don't prompt for a character, if there are none available
        if (! @comboList) {

            return $self->showMsgDialogue(
                'Select character',
                'error',
                'There are no character profiles available',
                'ok',
            );
        }

        # Prompt the user for a character
        $choice = $self->showComboDialogue(
            'Select character',
            'Select which character\'s visits to draw',
            FALSE,
            \@comboList,
        );

        if ($choice) {

            if ($choice eq $currentString) {

                if (defined $self->showChar) {

                    $redrawFlag = TRUE;
                }

                # Use the current character profile (this IV uses the value 'undef' to mean the
                #   current character)
                $self->ivUndef('showChar');

            } else {

                $choiceObj = $comboHash{$choice};

                if (! defined $self->showChar || $self->showChar ne $choiceObj->name) {

                    $redrawFlag = TRUE;
                }

                # Use the specified character profile
                $self->ivPoke('showChar', $choiceObj->name);
            }

            # If there is a current regionmap, and if we are drawing room interiors in mode 7,
            #   redraw the regionmap to show character visits for the selected character
            # (Don't redraw the region if the character hasn't changed)
            if (
                $redrawFlag
                && $self->currentRegionmap
                && $self->worldModelObj->roomInteriorMode eq 'visit_count'
            ) {
                $self->drawRegion();
            }
        }

        return 1;
    }

    sub zoomCallback {

        # Called by $self->enableViewColumn
        # Zooms in or out on the map, for the current region only
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $zoom   - Set to 'in' or 'out' for zoom in/zoom out, or set to a number corresponding to
        #               the new value of GA::Obj::Regionmap->magnification (e.g. 2, 1, 0.5; if set
        #               to 'undef', the user is prompted for the magnification)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if there is no
        #       current regionmap, if the standard  magnification list is empty or if the user
        #       declines to specify a magnification, when prompted
        #   1 otherwise

        my ($self, $zoom, $check) = @_;

        # Local variables
        my (
            $index, $match, $currentMag, $newMag,
            @magList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->zoomCallback', @_);
        }

        # Standard callback check. Import the standard magnification list
        @magList = $self->constMagnifyList;
        # Perform the check
        if (
            ! $self->currentRegionmap
            || (
                defined $zoom
                && (
                    ($zoom eq 'out' && $self->currentRegionmap->magnification <= $magList[0])
                    || ($zoom eq 'in' && $self->currentRegionmap->magnification >= $magList[-1])
                )
            )
        ) {
            return undef;
        }

        # If the tooltips are visible, hide them
        $self->hideTooltips();

        # Don't do anything if there is no current regionmap (possible when the user is using the
        #   mouse scroll button) or when the magnification list is empty (no reason why it should
        #   be)
        if (! $self->currentRegionmap || ! $self->constMagnifyList) {

            return undef;
        }

        # Import the current regionmap's current magnification
        $currentMag = $self->currentRegionmap->magnification;

        if (defined $zoom && ($zoom eq 'in' || $zoom eq 'out')) {

            # The map's current magnification is stored in GA::Obj::Regionmap->magnification. The
            #   default value is 1
            # $self->constMagnifyList contains a standard list of magnifications in ascending order,
            #   e.g. (0.5, 1, 2)
            # If GA::Obj::Regionmap->magnification is in the standard list, then we use the previous
            #   (or next) value in the list
            # Otherwise, we find the previous (or next) value in the list as it would be, if
            #   GA::Obj::Regionmap->magnification were in it
            #
            # Try to find GA::Obj::Regionmap->magnification in the standard list, remembering the
            #   index at which it was found
            $index = -1;
            OUTER: foreach my $item ($self->constMagnifyList) {

                $index++;
                if ($magList[$index] == $currentMag) {

                    $match = $index;
                    last OUTER;
                }
            }

            if (! defined $match) {

                # GA::Obj::Regionmap->magnification isn't a standard value. Insert it into the
                #   list as long as it's not smaller than the smallest value or bigger than the
                #   biggest value
                # Try inserting it at the beginning...
                if ($currentMag < $magList[0]) {

                    # Use index 0
                    $match = 0;

                # Or at the end...
                } elsif ($currentMag > $magList[-1]) {

                    # Use last index
                    $match = (scalar @magList) - 1;

                # Or somewhere in the middle...
                } else {

                    OUTER: for ($index = 0; $index < ((scalar @magList) - 1); $index++) {

                        if (
                            $currentMag > $magList[$index]
                            && $currentMag < $magList[($index + 1)]
                        ) {
                            splice (@magList, ($index + 1), 0, $currentMag);

                            $match = $index + 1;
                            last OUTER;
                        }
                    }
                }
            }

            # This error message should be impossible...
            if (! defined $match) {

                return $self->sesion->writeError(
                    'Error dealing with map magnifications',
                    $self->_objClass . '->zoomCallback',
                );
            }

            # Now, zoom out (or in), if possible
            if ($zoom eq 'out') {

                if ($match > 0) {

                    $match--;
                }

            } elsif ($zoom eq 'in') {

                if ($match < ((scalar @magList) - 1)) {

                    $match++;
                }
            }

            # Set the new magnification
            $newMag = $magList[$match];

        } else {

            if (! defined $zoom) {

                # Prompt the user for a zoom factor
                $zoom = $self->showEntryDialogue(
                    'Enter zoom factor',
                    'Enter an integer (e.g. 33 for 33% zoom)',
                );

                # User pressed 'cancel' button
                if (! defined $zoom) {

                    return undef;

                # The calling function has supplied a zoom factor. Make sure it's valid
                } elsif (! $axmud::CLIENT->floatCheck($zoom, 0) || $zoom == 0) {

                    return $self->showMsgDialogue(
                        'Zoom',
                        'error',
                        'Illegal magnification \'' . $zoom . '\'% - must be an integer (e.g. 100,'
                        . ' 50, 200)',
                        'ok',
                    );
                }

                # Convert the zoom factor from a percentage to a number that can be stored in
                #   GA::Obj::Regionmap->magnification (e.g. convert 133.33% to 1.33)
                $newMag = sprintf('%.2f', ($zoom / 100));

            } else {

                # $zoom is already set to the magnification
                $newMag = $zoom;
            }

            # Make sure the magnification is within limits
            if ($newMag < $magList[0] || $newMag > $magList[-1]) {

                return $self->showMsgDialogue(
                    'Zoom',
                    'error',
                    'Illegal magnification \'' . $zoom . '\' - use a number in the range '
                    . int($magList[0] * 100) . '-' . int($magList[-1] * 100). '%',
                    'ok',
                );
            }
        }

        # Set the new magnification; the called function updates every Automapper window using the
        #   current worldmodel
        $self->worldModelObj->setMagnification($self, $newMag);

        return 1;
    }

    sub changeLevelCallback {

        # Called by $self->enableViewColumn
        # Prompts the user for a new level in the current regionmap, then sets it as the currently-
        #   displayed level
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $level;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->changeLevelCallback', @_);
        }

        # (No standard callback checks for this function)

        # Prompt the user for a new level
        $level = $self->showEntryDialogue(
            'Change level',
            'Enter the new level number (\'ground\' level is 0)',
        );

        if (defined $level) {

            # Check that $level is a valid integer (positive, negative or 0)
            if (! ($level =~ m/^-?\d+$/)) {

                return $self->showMsgDialogue(
                    'Change level',
                    'error',
                    'Invalid level \'' . $level . '\' - you must use an integer',
                    'ok',
                );
            }

            # Set the new current level, which redraws the map
            $self->setCurrentLevel($level);
        }

        return 1;
    }

    # Menu 'Mode' column callbacks

    sub repaintSelectedRoomsCallback {

        # Called by $self->enableModeColumn
        # 'Repaints' the selected room(s) by copying the values of certain IVs stored in the world
        #   model's painter object (a non-model GA::ModelObj::Room) to each selected room
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (@roomList, @redrawList);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->repaintSelectedRoomsCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || (! $self->selectedRoom && ! $self->selectedRoomHash)) {

            return undef;
        }

        # Get a list of selected rooms
        @roomList = $self->compileSelectedRooms();

        foreach my $roomObj (@roomList) {

            # Repaint each selected room
            $self->paintRoom(
                $roomObj,
                FALSE,      # Don't update Automapper windows yet
            );

            push (@redrawList, 'room', $roomObj);
        }

        # Redraw all the selected rooms, so the repainting is visible
        $self->worldModelObj->updateMaps(@redrawList);

        return 1;
    }

    sub verboseCharsCallback {

        # Called by $self->enableModeColumn
        # Sets the number of characters at the beginning of a verbose description that are checked
        #   to match a world model room with the Locator's current room
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $number;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->verboseCharsCallback', @_);
        }

        # (No standard callback checks for this function)

        # Prompt for a new number of verbose characters to match
        $number = $self->showEntryDialogue(
            'Match verbose description',
            "Enter number of initial characters to match\n(0 = match whole description. Current"
            . " value: " . $self->worldModelObj->matchDescripCharCount . ")",
        );

        if (defined $number && ! ($number =~ /\D/) && $number >= 0) {

            $self->worldModelObj->set_matchDescripCharCount($number);
        }

        return 1;
    }

    # Menu 'Regions' column callbacks

    sub newRegionCallback {

        # Called by $self->enableRegionsColumn
        # Adds a new region to the world model
        #
        # Expected arguments
        #   $tempFlag   - If set to TRUE, the new region is a temporary region (that should be
        #                   deleted, the next time the world model is loaded from file)
        #
        # Return values
        #   'undef' on improper arguments, if the new model object can't be created or if the user
        #       cancels the operation
        #   1 otherwise

        my ($self, $tempFlag, $check) = @_;

        # Local variables
        my (
            $noParentString, $title, $name, $parentName, $parentNumber, $result,
            @objList, @nameList,
        );

        # Check for improper arguments
        if (! defined $tempFlag || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->newRegionCallback', @_);
        }

        # (No standard callback checks for this function)

        # Get a sorted list of region objects
        @objList = sort {lc($a->name) cmp lc($b->name)}
                    ($self->worldModelObj->ivValues('regionModelHash'));
        # Convert this list into region names
        foreach my $obj (@objList) {

            push (@nameList, $obj->name);
        }

        # The first item on the list should be an option to choose no parent at all
        $noParentString = '<no parent region>';
        unshift(@nameList, $noParentString);

        if ($tempFlag) {
            $title = 'New temporary region';
        } else {
            $title = 'New region';
        }

        # Prompt the user for a region name and, optionally, a parent region
        ($name, $parentName) = $self->showDoubleComboDialogue(
            $title,
            'Enter a name for the new region (max 32 chars)',
            '(Optional) select the parent region',
            \@nameList,
            32,
        );

        if ($name) {

            # Check the name is not already in use
            if ($self->worldModelObj->ivExists('regionmapHash', $name)) {

                $self->showMsgDialogue(
                    $title,
                    'error',
                    'There is already a region called \'' . $name . '\'',
                    'ok',
                );

                return undef;
            }

            # If parent was specified, find its world model number
            if ($parentName) {

                $parentNumber = $self->findRegion($parentName);
            }

            # Create the region object
            if (
                ! $self->worldModelObj->addRegion(
                    $self->session,
                    TRUE,       # Update Automapper windows now
                    $name,
                    $parentNumber,
                    $tempFlag,
                )
            ) {
                # Operation failed
                $self->showMsgDialogue(
                    'New region',
                    'error',
                    'Could not create the new region',
                    'ok',
                );

                return undef;

            } else {

                # Make it the selected region, and draw it on the map
                return $self->setCurrentRegion($name);
            }

        } else {

            # User cancelled the operation
            return undef;
        }
    }

    sub renameRegionCallback {

        # Called by $self->enableRegionsColumn
        # Renames a world model region (and its tied GA::Obj::Regionmap)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if a region with
        #       the specified name already exists
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $name;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->renameRegionCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Prompt the user for a new region name
        $name = $self->showEntryDialogue(
            'Change region name',
            'Enter a new name for the \'' . $self->currentRegionmap->name . '\' region (max 32'
            . ' chars)',
            32,
        );

        if ($name) {

            # Check the name is not already in use
            if ($self->worldModelObj->ivExists('regionmapHash', $name)) {

                $self->showMsgDialogue(
                    'Change region name',
                    'error',
                    'There is already a region called \'' . $name . '\'',
                    'ok',
                );

                return undef;

            } else {

                # Rename the region
                $self->worldModelObj->renameRegion($self->currentRegionmap, $name);
            }
        }

        return 1;
    }

    sub changeRegionParentCallback {

        # Called by $self->enableRegionsColumn
        # Changes a region's parent region
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the parent
        #       region can't be set
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $modelNum, $noParentString, $parent, $parentNum,
            @list, @sortedList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->changeRegionParentCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Import the world model number of the current region
        $modelNum = $self->currentRegionmap->number;

        # Get a sorted list of references to world model regions
        @list = sort {lc($a->name) cmp lc($b->name)}
                    ($self->worldModelObj->ivValues('regionModelHash'));

        # Convert this list into region names, and remove the current region
        foreach my $regionObj (@list) {

            if ($regionObj->number ne $modelNum) {

                push (@sortedList, $regionObj->name);
            }
        }

        # Put an option for 'no parent region' at the top of the list
        $noParentString = '<no parent region>';
        unshift(@sortedList, $noParentString);

        # Prompt the user for a new parent region
        $parent = $self->showComboDialogue(
            'Change parent region',
            'Select the new parent region for \'' . $self->currentRegionmap->name . '\'',
            FALSE,
            \@sortedList,
        );

        if ($parent) {

            if ($parent eq $noParentString) {

                # Set the region to have no parent
                if (!
                    $self->worldModelObj->setParent(
                        FALSE,      # No update
                        $modelNum,
                    )
                ) {
                    return undef;
                }

            } else {

                $parentNum = $self->findRegion($parent);

                # Set the new parent region
                if (
                    ! $self->worldModelObj->setParent(
                        FALSE,          # No update
                        $modelNum,
                        $parentNum,
                    )
                ) {
                    return undef;
                }
            }

            # Redraw the list of regions in the treeview. By using the current region as an
            #   argument, we make sure that it is visible in the treeview, by expanding the tree
            #   model as necessary
            $self->resetTreeView($self->currentRegionmap->name);
            # Make sure the current region is highlighted
            $self->treeViewSelectLine($self->currentRegionmap->name);
        }

        return 1;
    }

    sub identifyRegionCallback {

        # Called by $self->enableRegionsColumn
        # Identifies the currently highlighted region (in the treeview)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($text, $regionNum, $regionObj, $parentObj);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->identifyRegionCallback', @_);
        }

        # Standard callback check
        if (! $self->treeViewSelectedLine) {

            return undef;
        }

        # Prepare the text to display
        $text = 'Currently highlighted region: \'' . $self->treeViewSelectedLine . '\'';

        $regionNum = $self->findRegion($self->treeViewSelectedLine);
        $regionObj = $self->worldModelObj->ivShow('modelHash', $regionNum);
        $text .= ' (#' . $regionNum . ')';

        if ($regionObj->parent) {

            $parentObj = $self->worldModelObj->ivShow('modelHash', $regionObj->parent);
            $text .= "\nParent region: \'" . $parentObj->name . '\' (#' . $parentObj->number . ')';
        }

        # Display the 'dialogue' window
        $self->showMsgDialogue(
            'Highlighted region',
            'info',
            $text,
            'ok',
        );

        return 1;
    }

    sub editRegionCallback {

        # Called by $self->enableRegionsColumn
        # Opens a GA::EditWin::ModelObj::Region for the current region
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($number, $obj);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->editRegionCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Find the current regionmap's equivalent world model object
        $number = $self->currentRegionmap->number;
        if ($number) {

            $obj = $self->worldModelObj->ivShow('modelHash', $number);

            # Open up an 'edit' window to edit the object
            $self->createFreeWin(
                'Games::Axmud::EditWin::ModelObj::Region',
                $self,
                $self->session,
                'Edit ' . $obj->category . ' model object #' . $obj->number,
                $obj,
                FALSE,                          # Not temporary
            );
        }

        return 1;
    }

    sub visibleScreenshotCallback {

        # Called by $self->enableRegionsColumn
        # Takes a screenshot of the visible portion of the currently displayed region, at the
        #   currently displayed level, and saves it in the /screenshots directory
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($width, $height, $file, $path, $count);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->visibleScreenshotCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # The menu column is presumably still open - which will get in the way of the screenshot.
        #   Give it a chance to close
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->visibleScreenshotCallback');

        # Get the size of the canvas
        ($width, $height) = $self->canvas->window->get_size();

        # Create a blank pixbuffer to hold the image
        my $pixBuffer = Gtk2::Gdk::Pixbuf->new(
            'rgb',
            0,
            8,
            $width,
            $height,
        );

        # Take the screenshot by transfering the canvas to the pixbuffer
        $pixBuffer->get_from_drawable(
            $self->canvas->window,
            undef, 0, 0, 0, 0,
            $width, $height,
        );

        $file = $self->currentRegionmap->name . '_level_' . $self->currentRegionmap->currentLevel;
        $path = $axmud::DATA_DIR . '/screenshots/' . $file . '.jpg';

        # If the file $path already exists, add a postscript to create a filepath that doesn't yet
        #   exist
        if (-e $path) {

            $count = 0;

            do {

                $count++;

                my $newFile = $file . '_(' . $count . ')';
                $path = $axmud::DATA_DIR . '/screenshots/' . $newFile . '.jpg'

            } until (! -e $path);
        }

        # Save the file as a .jpeg
        $pixBuffer->save($path, 'jpeg', quality => 100);

        # Display a confirmation dialogue
        $self->showMsgDialogue(
            'Screenshot',
            'info',
            'Screenshot saved to ' . $path,
            'ok',
        );

        return 1;
    }

    sub regionScreenshotCallback {

        # Called by $self->enableRegionsColumn
        # Takes a screenshot of the entire regionmap (or the occupied portion of it), at the
        #   currently displayed level, and saves it in the /screenshots directory
        #
        # Expected arguments
        #   $wholeFlag  - If set to TRUE, capture the whole regionmap. If set to FALSE, capture only
        #                   the occupied part of the regionmap
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the user
        #       declines to take the screenshot after a warning
        #   1 otherwise

        my ($self, $wholeFlag, $check) = @_;

        # Local variables
        my (
            $canvasWidth, $canvasHeight, $x, $y, $viewPortWidth, $viewPortHeight, $viewPortDepth,
            $columnCount, $columnMod, $rowCount, $rowMod, $file, $path, $count, $barXPos, $barYPos,
            $msg, $result, $left, $right, $top, $bottom, $startX, $stopX, $startY, $stopY,
            $wholeWidthFlag, $wholeHeightFlag, $imageWidth, $imageHeight,
        );

        # Check for improper arguments
        if (! defined $wholeFlag || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->regionScreenshotCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # The menu column is presumably still open - which will get in the way of the screenshot.
        #   Give it a chance to close
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->regionScreenshotCallback');

        # Get the size of the canvas
        ($canvasWidth, $canvasHeight) = $self->canvas->get_size();
        # Get the dimensions of the viewport, through which the canvas is seen
        ($x, $y, $viewPortWidth, $viewPortHeight, $viewPortDepth)
            = $self->canvas->window->get_geometry();
        # Paraphrased from original code, "a hack to slide the viewport and grab each viewable area"
        $columnCount = int($canvasWidth / $viewPortWidth);
        $columnMod = $canvasWidth % $viewPortWidth;
        $rowCount = int($canvasHeight / $viewPortHeight);
        $rowMod = $canvasHeight % $viewPortHeight;

        # If $wholeFlag is TRUE, we capture the whole regionmap. If FALSE, we capture only the
        #   occupied portion of the regionmap
        if (! $wholeFlag) {

            ($left, $right, $top, $bottom) = $self->findOccupiedMap();
        }

        if ($wholeFlag || ! defined $left) {

            $startX = 0;
            $stopX = $columnCount - 1;
            $startY = 0;
            $stopY = $rowCount - 1;

            # We're using the whole map
            $wholeWidthFlag = TRUE;
            $wholeHeightFlag = TRUE;
            $imageWidth = $canvasWidth;
            $imageHeight = $canvasHeight;

        } else {

            $startX = int ($left / $viewPortWidth);
            $stopX = int ($right / $viewPortWidth);
            $startY = int ($top / $viewPortHeight);
            $stopY = int ($bottom / $viewPortHeight);

            if ($startX <= 0 && $stopX >= ($columnCount - 1)) {

                # We're using the whole width
                $wholeWidthFlag = TRUE;
                $imageWidth = $canvasWidth;

            } else {

                $imageWidth = (($stopX - $startX + 1) * $viewPortWidth);
            }

            if ($startY <= 0 && $stopY >= ($rowCount - 1)) {

                # We're using the whole height
                $wholeHeightFlag = TRUE;
                $imageHeight = $canvasHeight;

            } else {

                $imageHeight = (($stopY - $startY + 1) * $viewPortHeight);
            }
        }

        # For very large canvases (about 300x300 gridblocks, at 51 pixels per gridblock, and larger
        #   - produces a screenshot of about 5MB), display a warning before starting the operation
        if ($imageWidth * $imageHeight > 250_000_000) {

            $msg = 'This operation will produce a very large image (' . $imageWidth . 'x'
                    . $imageHeight . ' pixels). ' . 'Are you sure you want to continue?';

            $result = $self->showMsgDialogue(
                'Screenshot',
                'warning',
                $msg,
                'yes-no',
            );

            if ($result ne 'yes') {

                return undef;
            }
        }

        # Get the current position of the scrollbars, so they can be returned to their starting
        #   positions when the operation is complete
        ($barXPos, $barYPos) = $self->getMapPosn();

        # Create a blank pixbuffer to hold the (stiched) image
        my $pixBuffer = Gtk2::Gdk::Pixbuf->new(
            'rgb',
            0,
            8,
            $imageWidth,
            $imageHeight
        );

        # When there are a large number of rows and columns to capture, make the pause window
        #   visible
        if ((($stopX - $startX) * ($stopY - $startY)) > 16) {

            $self->showPauseWin();
        }

        # Grab each portion of the canvas in turn
        for (my $cols = $startX; $cols <= $stopX; $cols++) {

            # Slide the viewport to the right
            $self->canvasHAdjustment->set_value($cols * $viewPortWidth);

            for (my $rows = $startY; $rows <= $stopY; $rows++) {

                # Slide the viewport down
                $self->canvasVAdjustment->set_value($rows * $viewPortHeight);

                # Create a blank pixbuffer to hold the small image
                my $smallPixBuffer = Gtk2::Gdk::Pixbuf->new(
                    'rgb',
                    0,
                    8,
                    $viewPortWidth,
                    $viewPortHeight,
                );

                # Get an image of the visible region
                $smallPixBuffer->get_from_drawable(
                    $self->canvas->window,
                    undef, 0, 0, 0, 0,
                    $viewPortWidth, $viewPortHeight,
                );

                # Copy it into the large (stitched) image
                $smallPixBuffer->copy_area(
                    0, 0,
                    $viewPortWidth, $viewPortHeight,
                    $pixBuffer,
                    (($cols - $startX) * $viewPortWidth),
                    (($rows - $startY) * $viewPortHeight),
                );
            }
        }

        if ($wholeWidthFlag) {

            # Get the bottom row, except for the lower right corner
            for (my $cols = 0; $cols < $columnCount; $cols++) {

                # Slide the viewport
                $self->canvasHAdjustment->set_value($cols * $viewPortWidth);
                $self->canvasVAdjustment->set_value($rowCount * $viewPortHeight);

                # Create a blank pixbuffer to hold the small image
                my $smallPixBuffer = Gtk2::Gdk::Pixbuf->new(
                    'rgb',
                    0,
                    8,
                    $viewPortWidth,
                    $rowMod,
                );

                # Get an image of the visible region
                $smallPixBuffer->get_from_drawable(
                    $self->canvas->window,
                    undef, 0, 0, 0, 0,
                    $viewPortWidth, $rowMod,
                );

                # Copy it into the large (stitched) image
                $smallPixBuffer->copy_area(
                    0, 0,
                    $viewPortWidth, $rowMod,
                    $pixBuffer,
                    ($cols * $viewPortWidth), ($rowCount * $viewPortHeight),
                );
            }
        }

        if ($wholeHeightFlag) {

            # Get the right column, except for the lower right corner
            for (my $rows = 0; $rows < $rowCount; $rows++) {

                # Slide the viewport
                $self->canvasHAdjustment->set_value($columnCount * $viewPortWidth);
                $self->canvasVAdjustment->set_value($rows * $viewPortHeight);

                # Create a blank pixbuffer to hold the small image
                my $smallPixBuffer = Gtk2::Gdk::Pixbuf->new(
                    'rgb',
                    0,
                    8,
                    $columnMod,
                    $viewPortHeight,
                );

                # Get an image of the visible region
                $smallPixBuffer->get_from_drawable(
                    $self->canvas->window,
                    undef, 0, 0, 0, 0,
                    $columnMod, $viewPortHeight,
                );

                # Copy it into the large (stitched) image
                $smallPixBuffer->copy_area(
                    0, 0,
                    $columnMod, $viewPortHeight,
                    $pixBuffer,
                    ($columnCount * $viewPortWidth), ($rows * $viewPortHeight),
                );
            }
        }

        if ($wholeWidthFlag && $wholeHeightFlag) {

            # Get the bottom-right corner

            # Slide the viewport
            $self->canvasHAdjustment->set_value($columnCount * $viewPortWidth);
            $self->canvasVAdjustment->set_value($rowCount * $viewPortHeight);

            # Create a blank pixbuffer to hold the small image
            my $smallPixBuffer = Gtk2::Gdk::Pixbuf->new(
                'rgb',
                0,
                8,
                $columnMod,
                $rowMod,
            );

            # Get an image of the visible region
            $smallPixBuffer->get_from_drawable(
                $self->canvas->window,
                undef, 0, 0, 0, 0,
                $columnMod, $rowMod,
            );

            # Copy it into the large (stitched) image
            $smallPixBuffer->copy_area(
                0, 0,
                $columnMod, $rowMod,
                $pixBuffer,
                ($canvasWidth - $columnMod), ($canvasHeight - $rowMod),
            );
        }

        # Return the scrollbars to their original positions
        $self->setMapPosn($barXPos, $barYPos);

        # Save the screenshot
        $file = $self->currentRegionmap->name . '_level_' . $self->currentRegionmap->currentLevel;
        $path = $axmud::DATA_DIR . '/screenshots/' . $file . '.jpg';

        # If the file $path already exists, add a postscript to create a filepath that doesn't yet
        #   exist
        if (-e $path) {

            $count = 0;

            do {

                $count++;

                my $newFile = $file . '_(' . $count . ')';
                $path = $axmud::DATA_DIR . '/screenshots/' . $newFile . '.jpg'

            } until (! -e $path);
        }

        # Save the file as a .jpeg
        $pixBuffer->save($path, 'jpeg', quality => 100);

        # Make the pause window invisible
        $self->hidePauseWin();

        # Display a confirmation dialogue
        $self->showMsgDialogue(
            'Screenshot',
            'info',
            'Screenshot saved to ' . $path,
            'ok',
        );

        return 1;
    }

    sub removeRoomFlagsCallback {

        # Called by $self->enableRegionsColumn
        # Prompts the user to select a room flag to be removed from every room in the current region
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the user doesn't
        #       select a room flag, if the region has no rooms or if every room in the region has no
        #       room flags
        #   1 otherwise

        my ($self, $type, $check) = @_;

        # Local variables
        my (
            $choice, $msg, $count,
            @flagList,
            %flagHash, %priorityHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->removeRoomFlagsCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Check there are some rooms in the current region
        if (! $self->currentRegionmap->gridRoomHash) {

            $self->showMsgDialogue(
                'Remove room flags',
                'error',
                'There are no rooms in the current region',
                'ok',
            );

            return undef;
        }

        # Go through every room in the region, compiling a list of room flags actually in use
        foreach my $roomNum ($self->currentRegionmap->ivValues('gridRoomHash')) {

            my $roomObj = $self->worldModelObj->ivShow('modelHash', $roomNum);

            foreach my $flag ($roomObj->ivKeys('roomFlagHash')) {

                # Compile a hash containing one entry for each room flag used (regardless of whether
                #   it's used in one room or multiple rooms)
                $flagHash{$flag} = undef;
            }
        }

        if (! %flagHash) {

            $self->showMsgDialogue(
                'Remove room flags',
                'error',
                'No rooms in the current region are using room flags',
                'ok',
            );

            return undef;
        }

        # Import the world model's room flag priority hash
        %priorityHash = $self->worldModelObj->roomFlagPriorityHash;
        # Get a list of the room flags in use, sorted by priority
        @flagList = sort {$priorityHash{$a} <=> $priorityHash{$b}} (keys %flagHash);

        # Prompt the user to select one of the room flags
        $choice = $self->showComboDialogue(
            'Remove room flags',
            "Select which room flag should be removed\nfrom every room in this region",
            FALSE,
            \@flagList,
        );

        if (! $choice) {

            return undef;

        } else {

            # Remove the room flag from each room in turn
            $count = $self->worldModelObj->removeRoomFlags($self->currentRegionmap, $choice);

            # Display a confirmation message
            $msg = "Room flag \'" . $choice . "\' removed from\n";
            if ($count == 1) {
                $msg .= "1 room in this region",
            } else {
                $msg .= $count . " rooms in this region",
            }

            $self->showMsgDialogue(
                'Remove room flags',
                'info',
                $msg,
                'ok',
            );

            return 1;
        }
    }

    sub recalculatePathsCallback {

        # Called by $self->enableRegionsColumn
        # Recalculates region paths - paths between each room in the region which has a super-region
        #   exit, and every other room in the region which has a super-region exit (used for quick
        #   pathfinding across different regions)
        #
        # Expected arguments
        #   $type   - Which region to process: 'current' for the current regionmap, 'select' to
        #               prompt the user for a regionmap, 'all' to recalculate paths in all
        #               regionmaps, or 'exit' to recalculate region paths to and from the
        #               selected exit (only)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the user
        #       declines to specify a region
        #   1 otherwise

        my ($self, $type, $check) = @_;

        # Local variables
        my (
            $choice, $count, $estimate, $msg,
            @nameList, @regionmapList,
        );

        # Check for improper arguments
        if (
            ! defined $type
            || ($type ne 'current' && $type ne 'select' && $type ne 'all' && $type ne 'exit')
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->recalculatePathsCallback',
                @_,
            );
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ($type eq 'current' && ! $self->currentRegionmap->gridRoomHash)
            || ($type eq 'exit' && ! $self->selectedExit && ! $self->selectedExit->superFlag)
        ) {
            return undef;
        }

        # Recalculate paths in the current region
        if ($type eq 'current') {

            push (@regionmapList, $self->currentRegionmap);

        # Recalculate paths in a region specified by the user
        } elsif ($type eq 'select') {

            # Get a sorted list of references to world model regions
            @nameList = sort {lc($a) cmp lc($b)} ($self->worldModelObj->ivKeys('regionmapHash'));

            # Prompt the user for a region name
            $choice = $self->showComboDialogue(
                'Recalculate region paths',
                'Select the region whose paths should be recalculated',
                FALSE,
                \@nameList,
            );

            if (! $choice) {

                return undef;

            } else {

                push (@regionmapList, $self->worldModelObj->ivShow('regionmapHash', $choice));
            }

        # Recalculate paths in all regions
        } elsif ($type eq 'all') {

            # Compile a list of regionmaps
            @regionmapList = $self->worldModelObj->ivValues('regionmapHash');
        }

        if ($type ne 'exit') {

            # Work out how many region paths there are likely to be
            $estimate = 0;
            foreach my $regionmapObj (@regionmapList) {

                my $exitCount = 0;

                # Count the number of super-region exits
                foreach my $exitNum ($regionmapObj->regionExitHash) {

                    my $exitObj = $self->worldModelObj->ivShow('exitModelHash', $exitNum);

                    if ($exitObj->superFlag) {

                        $exitCount++;
                    }
                }

                # If there are ten super-region exits, each individual exit has nine region paths
                #   joining it to every other super-region exit. We then double the number, because
                #   safe region paths are stored separately. So the estimated number of region
                #   paths is ((n-1) ^ 2 ), all multiplied by 2
                $estimate += (2 * (($exitCount - 1) ** 2));
            }

            # If the estimated number of paths is above the limit set by the world model, make the
            #   pause window visible for the duration of the recalculation
            if ($estimate > $self->worldModelObj->recalculatePauseNum) {

                $self->showPauseWin();
            }

            # Recalculate region paths for each region added to our list
            $count = 0;
            foreach my $regionmapObj (@regionmapList) {

                my $number = $self->worldModelObj->recalculateRegionPaths(
                    $self->session,
                    $regionmapObj,
                );

                if ($number) {

                    $count += $number;
                }
            }

            # Make the pause window invisible
            $self->hidePauseWin();

        } else {

            # Recalculate paths to/from the selected exit.
            $count = $self->worldModelObj->recalculateSpecificPaths(
                $self->session,
                $self->currentRegionmap,
                $self->selectedExit,
            );

            # In case the called function returns 'undef', $count still needs to be an integer
            if (! $count) {

                $count = 0;
            }

            # For the message we're about to compose, @regionmapList must contain the affected
            #   regionmap
            push (@regionmapList, $self->currentRegionmap);
        }

        # Display a popup showing the results
        $msg = 'Recalculation complete: ';

        if (! $count) {
            $msg .= 'no region paths found';
        } elsif ($count == 1) {
            $msg .= '1 region path found';
        } else {
            $msg .= $count . ' region paths found';
        }

        if (@regionmapList == 1) {
            $msg .= ' in 1 region.';
        } else {
            $msg .= ' in ' . scalar @regionmapList . ' regions.';
        }

        $self->showMsgDialogue(
            'Recalculate region paths',
            'info',
            $msg,
            'ok',
        );

        return 1;
    }

    sub locateCurrentRoomCallback {

        # Called by $self->enableRegionsColumn
        # Tries to find the current room by comparing the Locator task's current room with every
        #   room in the current region, in a specified region, or in all regions
        # If there's a single matching room, that room is set as the current room. If the single
        #   matching room is in a different region or level to the current one, the map is redrawn
        # If there are multiple matching rooms, those rooms are selected. If they are all in a
        #   different region or at a different level to the current one, the map is redrawn
        #
        # Expected arguments
        #   $type   - Where to search: 'current' for the current regionmap, 'select' to prompt the
        #               user for a regionmap, or 'all' to search in all regionmaps
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if there is no
        #       current regionmap, or if there is no Locator task (or the task doesn't know the
        #       current location), if the Locator's current room is dark or unspecified or if the
        #       user declines to continue
        #   1 otherwise

        my ($self, $type, $check) = @_;

        # Local variables
        my (
            $taskObj, $msg, $regionName, $regionmapObj, $choice, $matchObj, $regionObj,
            @roomList, @list, @regionList, @selectList, @modList, @newRegionList, @sortedList,
            %regionmapHash,
        );

        # Check for improper arguments
        if (
            ! defined $type || ($type ne 'current' && $type ne 'select' && $type ne 'all')
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->locateCurrentRoomCallback',
                @_,
            );
        }

        # Standard callback check
        if ($type eq 'current' && ! $self->currentRegionmap->gridRoomHash) {
            return undef;
        }

        # Import the Locator task
        $taskObj = $self->session->locatorTask;

        # If there is no Locator task, or if it doesn't know its location, display a warning
        if (! $taskObj || ! $taskObj->roomObj) {

            $msg = 'the Locator task isn\'t ready';

        # Also display a warning if the Locator's current room is dark or unspecified
        } elsif ($taskObj->roomObj->currentlyDarkFlag) {

            $msg = 'it is dark';

        } elsif ($taskObj->roomObj->unspecifiedFlag) {

            $msg = 'it is an unspecified room';
        }

        if ($msg) {

            $self->showMsgDialogue(
                'Locate current room',
                'error',
                'Can\'t locate the current room because ' . $msg,
                'ok',
            );

            return undef;
        }

        # Compile a list of rooms to search

        # Get rooms in the current region
        if ($type eq 'current') {

            # Get a list of rooms in the current region
            @roomList = $self->currentRegionmap->ivValues('gridRoomHash');

        # Get rooms in a region specified by the user
        } elsif ($type eq 'select') {

            # Get a sorted list of references to world model regions
            @list = sort {lc($a->name) cmp lc($b->name)}
                        ($self->worldModelObj->ivValues('regionModelHash'));

            # Convert this list into region names
            foreach my $regionObj (@list) {

                push (@regionList, $regionObj->name);
            }

            # Prompt the user for a region name
            $regionName = $self->showComboDialogue(
                'Select region',
                'Select the region in which to search',
                FALSE,
                \@regionList,
            );

            if (! $regionName) {

                return undef;
            }

            # Find the matching regionmap
            if (! $self->worldModelObj->ivExists('regionmapHash', $regionName)) {

                return undef;

            } else {

                $regionmapObj = $self->worldModelObj->ivShow('regionmapHash', $regionName);
            }

            # Get a list of rooms in the specified region
            @roomList = $regionmapObj->ivValues('gridRoomHash');

        # Locate rooms in all regions
        } elsif ($type eq 'all') {

            # Get a list of rooms in all regions
            @roomList = $self->worldModelObj->ivKeys('roomModelHash');
        }

        # If a room limit is set, prompt the user for confirmation
        if (
            $self->worldModelObj->locateMaxObjects
            && $self->worldModelObj->locateMaxObjects < @roomList
        ) {
            $choice = $self->showMsgDialogue(
                'Locate current room',
                'question',
                'There are ' . scalar @roomList . ' rooms to search. Do you want to continue?',
                'yes-no',
            );

            if ($choice ne 'yes') {

                return undef;
            }
        }

        # Compare the Locator task's current room with every room in @roomList
        foreach my $roomNum (@roomList) {

            my $roomObj = $self->worldModelObj->ivShow('modelHash', $roomNum);

            if ($self->worldModelObj->locateRoom($self->session, $roomObj)) {

                push (@selectList, $roomObj);
                # Add the parent region to a hash so we can quickly check how many regions
                #   have matching rooms
                $regionmapHash{$roomObj->parent} = undef;
            }
        }

        # No matching rooms found
        if (! @selectList) {

            # Show a confirmation
            if ($type eq 'current') {
                $msg = 'No matching rooms found in the current region';
            } elsif ($type eq 'select') {
                $msg = 'No matching rooms found in the \'' . $regionmapObj->name . '\' region';
            } elsif ($type eq 'all') {
                $msg = 'No matching rooms found in any region';
            }

            $self->showMsgDialogue(
                'Locate current room',
                'error',
                $msg,
                'ok',
            );

        # A single matching room found
        } elsif (@selectList == 1) {

            # To clear a previous location attempt, in which many rooms were selected, unselect any
            #   existing selected objects
            $self->setSelectedObj();

            # Mark the matching room as the automapper's current room. If it's in a different
            #   regionmap (or on a different level), the map is redrawn
            $self->mapObj->setCurrentRoom($selectList[0]);

            # Show a confirmation
            $self->showMsgDialogue(
                'Locate current room',
                'info',
                '1 matching room found; current location set to room #'
                . $self->mapObj->currentRoom->number,
                'ok',
            );

        # Multiple matching rooms were found
        } else {

            # Unselect any existing selected objects
            $self->setSelectedObj();

            # Select all of the matching rooms. $self->setSelectedObj expects a list in the form
            #   (room_object, 'room', room_object, 'room', ...)
            foreach my $roomObj (@selectList) {

                push (@modList, $roomObj, 'room');
            }

            $self->setSelectedObj(
                \@modList,
                TRUE,       # Select multiple objects
            );

            # Get a sorted list of affected regions
            foreach my $number (keys %regionmapHash) {

                my $regionObj = $self->worldModelObj->ivShow('modelHash', $number);

                push (@newRegionList, $regionObj->name);
            }

            @sortedList = sort {lc($a) cmp lc($b)} (@newRegionList);

            # Show a confirmation
            $msg = scalar @selectList . ' matching rooms found in ';

            if ($type eq 'all') {

                if (@sortedList > 1) {

                    $msg .= scalar @sortedList . " regions:\n";

                    # Sort the region names alphabetically
                    foreach my $item (@sortedList) {

                        $msg .= '\'' . $item . '\' ';
                    }

                } else {

                    $msg .= 'the region \'' . $sortedList[0] . '\'';
                }

            } elsif ($type eq 'select') {

                $msg .= 'the region \'' . $regionName . '\'';

            } else {

                $msg .= 'this region';
            }

            $self->showMsgDialogue(
                'Locate current room',
                'info',
                $msg,
                'ok',
            );

            # Check the list of selected rooms, looking for the first one that's in the current
            #   region
            OUTER: foreach my $roomObj (@selectList) {

                if ($roomObj->parent eq $self->currentRegionmap->number) {

                    $matchObj = $roomObj;
                    last OUTER;
                }
            }

            if (! $matchObj) {

                # None of the selected rooms are in the current region. Use the first selected
                #   room...
                $matchObj = $selectList[0];
                # ...and change the current region to show that room
                $regionObj = $self->worldModelObj->ivShow('modelHash', $matchObj->parent);
                $self->setCurrentRegion($regionObj->name);
            }

            # Centre the map over the chosen selected room
            $self->centreMapOverRoom($matchObj);
        }

        return 1;
    }

    sub emptyRegionCallback {

        # Called by $self->enableRegionsColumn
        # Empties an existing region of its rooms
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the region is
        #       already empty or if the user declines to continue, when prompted
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $regionObj, $msg, $result,
            @roomList, @otherList, @labelList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->emptyRegionCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Get the region object corresponding to the current regionmap
        $regionObj = $self->worldModelObj->ivShow('modelHash', $self->currentRegionmap->number);

        # Get a list of the region's child objects, but don't include any child regions (which won't
        #   be deleted)
        foreach my $childNum ($regionObj->ivKeys('childHash')) {

            my $childObj = $self->worldModelObj->ivShow('modelHash', $childNum);

            if ($childObj->category eq 'room') {
                push (@roomList, $childObj);
            } elsif ($childObj->category ne 'region') {
                push (@otherList, $childObj);
            }
        }

        # Get a list of the regionmap's labels
        @labelList = $self->currentRegionmap->ivValues('gridLabelHash');

        if (! @roomList && ! @otherList && ! @labelList) {

            $self->showMsgDialogue(
                'Empty region',
                'error',
                'The current region doesn\'t contain any rooms, model objects or labels',
                'ok',
            );

            return undef;

        } else {

            # Give the user a chance to change their minds, before emptying the region
            $msg = "Are you sure you want to empty the\n\'" . $regionObj->name
                    . "\'? region? It contains:\n\n   Rooms: " . scalar @roomList
                    . "\n   Other model objects: " . scalar @otherList
                    . "\n   Labels: " . scalar @labelList;


            $result = $self->showMsgDialogue(
                'Empty region',
                'question',
                $msg,
                'yes-no',
            );

            if ($result ne 'yes') {

                return undef;
            }
        }

        # Empty the region
        $self->worldModelObj->emptyRegion(
            $self->session,
            TRUE,              # Update Automapper windows now
            $regionObj,
        );

        return 1;
    }

    sub deleteRegionCallback {

        # Called by $self->enableRegionsColumn
        # Deletes the current region
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the user
        #       declines to continue, when prompted
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $regionObj, $msg, $result,
            @roomList, @otherList, @labelList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->deleteRegionCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Get the region object corresponding to the current regionmap
        $regionObj = $self->worldModelObj->ivShow('modelHash', $self->currentRegionmap->number);

        # Get a list of the region's child objects, but don't include any child regions (which won't
        #   be deleted)
        foreach my $childNum ($regionObj->ivKeys('childHash')) {

            my $childObj = $self->worldModelObj->ivShow('modelHash', $childNum);

            if ($childObj->category eq 'room') {
                push (@roomList, $childObj);
            } elsif ($childObj->category ne 'region') {
                push (@otherList, $childObj);
            }
        }

        # Get a list of the regionmap's labels
        @labelList = $self->currentRegionmap->ivValues('gridLabelHash');

        if (@roomList || @otherList || @labelList) {

            # Give the user a chance to change their minds, before emptying the region
            $msg = "Are you sure you want to delete the\n\'" . $regionObj->name
                    . "\' region? It contains:\n\n   Rooms: " . scalar @roomList
                    . "\n   Other model objects: " . scalar @otherList
                    . "\n   Labels: " . scalar @labelList;


            $result = $self->showMsgDialogue(
                'Delete region',
                'question',
                $msg,
                'yes-no',
            );

            if ($result ne 'yes') {

                return undef;
            }
        }

        # Delete the region
        $self->worldModelObj->deleteRegions(
            $self->session,
            TRUE,              # Update Automapper windows now
            $regionObj,
        );

        return 1;
    }

    sub deleteTempRegionsCallback {

        # Called by $self->enableRegionsColumn
        # Deletes all temporary regions
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if there are no temporary regions or if the user declines
        #       to continue, when prompted
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $msg, $result,
            @tempList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->deleteTempRegionsCallback',
                @_,
            );
        }

        # (No standard callback checks for this function)

        # Get a list of temporary region objects
        foreach my $regionObj ($self->worldModelObj->ivValues('regionModelHash')) {

            if ($regionObj->tempRegionFlag) {

                push (@tempList, $regionObj);
            }
        }

        if (! @tempList) {

            $self->showMsgDialogue(
                'Delete temporary regions',
                'error',
                'The world model doesn\'t contain any temporary regions',
                'ok',
            );

            return undef;

        } else {

            # Give the user a chance to change their minds, before emptying the region
            if (@tempList == 1) {

                $msg = 'There is 1 temporary region in the world model. Are you sure you want to'
                            . ' delete it?';
            } else {

                $msg = 'There are ' . scalar @tempList . ' temporary regions in the world model.'
                            . ' Are you sure you want to delete them all?'
            }

            $result = $self->showMsgDialogue(
                'Delete temporary regions',
                'question',
                $msg,
                'yes-no',
            );

            if ($result ne 'yes') {

                return undef;
            }
        }

        # Delete each temporary region in turn
        $self->worldModelObj->deleteTempRegions(
            $self->session,
            TRUE,              # Update Automapper windows now
        );

        return 1;
    }

    # Menu 'Rooms' column callbacks

    sub resetLocatorCallback {

        # Called by $self->enableRoomsColumn
        # Resets the Locator task, and marks the automapper as lost
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->resetLocatorCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Reset the Locator task
        $self->session->pseudoCmd('resetlocatortask', $self->pseudoCmdMode);

        # The call to ;resetlocatortask should mark the automapper as lost - but, if it's not, do it
        #   from here
        if ($self->mapObj->currentRoom) {

            return $self->setCurrentRoom(
                undef,
                $self->_objClass . '->resetLocatorCallback',    # Character now lost
            );
        }

        # Display an explanatory message, if necessary
        if ($self->worldModelObj->explainGetLostFlag) {

            $self->session->writeText('MAP: Lost because of a Locator reset');
        }

        return 1;
    }

    sub editLocatorRoomCallback {

        # Called by $self->enableRoomsColumn
        # Opens a GA::EditWin::ModelObj::Room for the Locator task's current (non-model) room
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the Locator task doesn't know the current location
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $taskObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->editLocatorRoomCallback',
                @_,
            );
        }

        # (No standard callback checks for this function)

        # Check there's a Locator task which knows the current room
        $taskObj = $self->session->locatorTask;
        if (! $taskObj || ! $taskObj->roomObj) {

            # Show a 'dialogue' window to explain the problem
            $self->showMsgDialogue(
                'Edit Locator room',
                'error',
                'Either the Locator task isn\'t running or it doesn\'t know the current location',
                'ok',
            );

            return undef;

        } else {

            # Open up an 'edit' window to edit the object
            $self->createFreeWin(
                'Games::Axmud::EditWin::ModelObj::Room',
                $self,
                $self->session,
                'Edit ' . $taskObj->roomObj->category . ' model object #'
                . $taskObj->roomObj->number,
                $taskObj->roomObj,
                FALSE,                          # Not temporary
            );

            return 1;
        }
    }

    sub processPathCallback {

        # Called by $self->enableRoomsColumn (also called by GA::Cmd::Go->do)
        # Performs the A* algorithm to find a path between the current room and the selected room,
        #   and then does something with it
        #
        # Expected arguments
        #   $mode   - Set to one of the following:
        #       'select_room' - shows the path by selecting every room along the route
        #       'pref_win' - shows the path in a 'pref' window, allowing the user to store it as a
        #           pre-defined route (using the ';addroute' command)
        #       'send_char' - sends the character to the selected room
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if no path can be
        #       found between the current and selected rooms
        #   1 otherwise

        my ($self, $mode, $check) = @_;

        # Local variables
        my (
            $dictObj, $text, $count, $maxChars, $string, $lastExitObj, $roomListRef, $exitListRef,
            $response,
            @roomList, @exitList, @cmdList, @reverseCmdList, @highlightList,
        );

        # Check for improper arguments
        if (
            ! defined $mode
            || ($mode ne 'select_room' && $mode ne 'pref_win' && $mode ne 'send_char')
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->processPathCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->mapObj->currentRoom
            || ! $self->selectedRoom
            || $self->mapObj->currentRoom eq $self->selectedRoom
        ) {
            return undef;
        }

        # Import the current dictionary (for speed)
        $dictObj = $self->session->currentDict;

        if ($self->mapObj->currentRoom->parent eq $self->selectedRoom->parent) {

            # The rooms are in the same region
            # Use the A* algorithm to find the shortest path between the current and selected rooms
            # (It returns two list references, one containing the list of GA::ModelObj::Room objects
            #   on the path, the other containing the list of GA::Obj::Exit objects to move
            #   between them)
            ($roomListRef, $exitListRef) = $self->worldModelObj->findPath(
                $self->mapObj->currentRoom,
                $self->selectedRoom,
                $self->worldModelObj->avoidHazardsFlag,
            );

        } else {

            # The rooms are in different regions
            # Use the universal version of the A* algorithm to find a path between the current and
            #   selected rooms
            ($roomListRef, $exitListRef) = $self->worldModelObj->findUniversalPath(
                $self->session,
                $self->mapObj->currentRoom,
                $self->selectedRoom,
                $self->worldModelObj->avoidHazardsFlag,
            );
        }

        if (! defined $roomListRef || ! @$roomListRef) {

            # There is no path between the current and selected room. Notify the user with a popup
            $self->showMsgDialogue(
                'No path found',
                'warning',
                'There is no known path between the current room (#'
                . $self->mapObj->currentRoom->number . ') and the selected room (#'
                . $self->selectedRoom->number . ')',
                'ok',
            );

            return undef;
        }

        # Apply post-processing to the path to remove jagged edges (if allowed)
        if ($self->worldModelObj->postProcessingFlag) {

            ($roomListRef, $exitListRef) = $self->worldModelObj->smoothPath(
                $self->session,
                $roomListRef,
                $exitListRef,
                $self->worldModelObj->avoidHazardsFlag,
            );
        }

        # Convert the list references returned by the called functions into lists
        @roomList = @$roomListRef;
        @exitList = @$exitListRef;

        # Compile a list of commands to get from one end of the route to the other. If assisted
        #   moves are turned on, use them; otherwise, use each exit's nominal direction
        # At the same time, try to compile a list of directions that lead from the end of the
        #   route back to the start
        @cmdList = $self->worldModelObj->convertExitList($self->session, @exitList);
        # Attempt to find the reverse list of directions, if possible (but only bother in
        #   'select_room' mode)
        if ($mode eq 'pref_win') {

            @reverseCmdList = $self->worldModelObj->findPathCmds($self->session, -1, @roomList);
        }

        # 'select_room' - select each room in the path, in order to highlight the route (but don't
        #   select the current room)
        # 'pref_win' - show the route/reverse route in a 'pref' window
        if ($mode eq 'select_room' || $mode eq 'pref_win') {

            foreach my $roomObj (@roomList) {

                if ($roomObj ne $self->mapObj->currentRoom) {

                    push (@highlightList, $roomObj, 'room');
                }
            }

            $self->setSelectedObj(
                \@highlightList,
                TRUE,           # Select multiple objects, including the currently selected room
            );
        }

        # 'pref_win' - show the route/reverse route in a 'pref' window, allowing the user to store
        #   it as a pre-defined route (using the ';addroute' command)
        if ($mode eq 'pref_win') {

            # Open up a path 'pref' window to specify task settings
            $self->createFreeWin(
                'Games::Axmud::PrefWin::Path',
                $self,
                $self->session,
                # Use 'Edit path' rather than 'Path preferences'
                'Edit path',
                # No ->editObj
                undef,
                # The path itself is temporary (although can be stored as a GA::Obj::Route)
                TRUE,
                # Config
                'room_list'     => $roomListRef,
                'exit_list'     => $exitListRef,
                'cmd_list'      => \@cmdList,
                'reverse_list'  => \@reverseCmdList,
            );
        }

        # 'send_char' - Select every room on the path, so that the user can see where the path is,
        #   before moving to the destination room (don't worry about not selecting the current room,
        #   as the character is about to move to a new room anyway)
        if ($mode eq 'send_char') {

            foreach my $roomObj (@roomList) {

                push (@highlightList, $roomObj, 'room');
            }

            $self->setSelectedObj(
                \@highlightList,
                TRUE,           # Select multiple objects, including the currently selected room
            );

            # Offer the user to opportunity to change their mind. Only display one 'dialogue'
            #   window; if the user clicks the 'yes' button, go ahead and move
            if ($self->mode eq 'wait') {

                $response = $self->showMsgDialogue(
                    'Move to room',
                    'question',
                    'The automapper is in \'wait\' mode. Do you really want to move to the'
                    . ' double-clicked room?',
                    'yes-no',
                );

                if ($response ne 'yes') {

                     # Don't move anywhere
                     return 1;
                }

            } elsif ($self->session->locatorTask->moveList) {

                $response = $self->showMsgDialogue(
                    'Move to room',
                    'question',
                    'The Locator task is expecting more room statements; the room displayed'
                    . ' as the automapper\'s current room probably isn\'t the correct one.'
                    . ' Do you really want to move to the double-clicked room?',
                    'yes-no',
                );

                if ($response ne 'yes') {

                    # Don't move anywhere
                    return 1;
                }

            } elsif (
                $self->worldModelObj->pathFindStepLimit
                && $self->worldModelObj->pathFindStepLimit < scalar @cmdList
            ) {
                $response = $self->showMsgDialogue(
                    'Move to room',
                    'warning',
                    'The path contains a large number of steps (' . scalar @cmdList . '). Do you'
                    . ' really want to move to the double-clicked room?',
                    'yes-no',
                );

                if ($response ne 'yes') {

                    # Don't move anywhere
                    return 1;
                }
            }

            # Take the route, abbreviating any primary/secondary directions, if possible
            foreach my $cmd (@cmdList) {

                my $abbrevDir = $dictObj->abbrevDir($cmd);

                # (For secondary directions like 'in' with no abbreviation, ->abbrevDir returns
                #   'undef', in which case we should use the original $cmd)
                if (defined $abbrevDir) {
                    $self->session->worldCmd($abbrevDir);
                } else {
                    $self->session->worldCmd($cmd);
                }
            }

        } else {

            # Unrecognised mode
            return undef;
        }

        return 1;
    }

    sub executeScriptsCallback {

        # Called by $self->enableRoomsPopupMenu
        # Executes Axbasic scripts for the current room, as if the character had just arrived
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->executeScriptsCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->mapObj->currentRoom) {

            return undef;
        }

        # If there are no Axbasic scripts for the current room, display a warning
        if (! $self->mapObj->currentRoom->arriveScriptList) {

            return $self->showMsgDialogue(
                'Run ' . $axmud::BASIC_NAME . ' scripts',
                'warning',
                'The current room has not been assigned any ' . $axmud::BASIC_NAME . ' scripts',
                'ok',
            );
        }

        # Otherwise, execute the scripts
        foreach my $scriptName ($self->mapObj->currentRoom->arriveScriptList) {

            $self->session->pseudoCmd('runscript ' . $scriptName);
        }

        return 1;
    }

    sub addFirstRoomCallback {

        # Called by $self->enableRoomsColumn. Also called by Axbasic ADDFIRSTROOM function
        # For an empty region, draws a room in the centre of the grid and marks it as the current
        #   room
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the Locator task
        #       isn't running or if it is still expecting room statements or if the new room can't
        #       be created
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($xPosBlocks, $yPosBlocks, $zPosBlocks, $newRoomObj);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->addFirstRoomCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || $self->currentRegionmap->gridRoomHash) {

            return undef;
        }

        # If the Locator's ->moveList isn't empty, we won't be able to switch to 'update' mode.
        #   Therefore refuse to add the first room if the list isn't empty (or if the Locator task
        #   isn't running at all)
        if (! $self->session->locatorTask) {

            $self->showMsgDialogue(
                'Add first room',
                'error',
                'Can\'t add a room at the centre of the grid - the Locator task is not running',
                'ok',
            );

            return undef;

        } elsif ($self->session->locatorTask->moveList) {

            $self->showMsgDialogue(
                'Add first room',
                'error',
                'Can\'t add a room at the centre of the grid - the Locator task is not ready',
                'ok',
            );

            return undef;
        }

        # Find the coordinates of the middle of the grid
        ($xPosBlocks, $yPosBlocks, $zPosBlocks) = $self->findGridCentre();

        # Check the location to make sure there's not already a room there
        if ($self->currentRegionmap->fetchRoom($xPosBlocks, $yPosBlocks, $zPosBlocks)) {

            $self->showMsgDialogue(
                'Add first room',
                'error',
                'Can\'t add a room at the centre of the grid - the position is already occupied',
                'ok',
            );

            return undef;
        }

        # Free click mode must be reset (nothing special happens when the user clicks on the map)
        $self->ivPoke('freeClickMode', 'default');

        # Create a new room object, with this region as its parent, and update the map
        if ($self->session->locatorTask && $self->session->locatorTask->roomObj) {

            # Set the Automapper window's mode to 'update', make the new room the current location
            #   and copy properties from the Locator task's current room (where allowed)
            $newRoomObj = $self->createNewRoom(
                $self->currentRegionmap,
                $xPosBlocks,
                $yPosBlocks,
                $zPosBlocks,
                'update',
                TRUE,
                TRUE,
            );

        } else {

            # Locator task doesn't know the current location, so don't make the new room the
            #   current room, and don't change the mode
            $newRoomObj = $self->createNewRoom(
                $self->currentRegionmap,
                $xPosBlocks,
                $yPosBlocks,
                $zPosBlocks,
            );
        }

        if (! $newRoomObj) {

            # Could not create the new room (an error message has already been displayed)
            return undef;

        } else {

            # Also update the Locator with the new current room (if there is one)
            $self->mapObj->updateLocator();

            return 1;
        }
    }

    sub addRoomAtBlockCallback {

        # Called by $self->enableRoomsColumn. Also called by the Axbasic ADDROOM function
        # Prompts the user to supply a gridblock (via a 'dialogue' window) and creates a room at
        #   that location. When called by Axbasic, uses the supplied gridblock
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $xPosBlocks, $yPosBlocks, $zPosBlocks
        #       - The coordinates on the gridblock at which to draw the room
        #
        # Return values
        #   'undef' on improper arguments,if the standard callback check fails, if the user cancels
        #       the 'dialogue' window or if the new room can't be created
        #   1 otherwise

        my ($self, $xPosBlocks, $yPosBlocks, $zPosBlocks, $check) = @_;

        # Local variables
        my $roomObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->addRoomAtBlockCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Prompt the user for a gridblock, if one was not specified
        if (! defined $xPosBlocks || ! defined $yPosBlocks || ! defined $zPosBlocks) {

            ($xPosBlocks, $yPosBlocks, $zPosBlocks) = $self->promptGridBlock();
            if (! defined $xPosBlocks ) {

                # User clicked the 'cancel' button
                return undef;
            }
        }

        # Check that the specified gridblock actually exists
        if (
            ! $self->currentRegionmap->checkGridBlock(
                $xPosBlocks,
                $yPosBlocks,
                $zPosBlocks,
            )
        ) {
            $self->showMsgDialogue(
                'Add room',
                'error',
                'The gridblock x=' . $xPosBlocks . ', y=' . $yPosBlocks . ', z=' . $zPosBlocks
                . ' is invalid',
                'ok',
            );

            return undef;
        }

        # Check that the gridblock isn't occupied
        if ($self->currentRegionmap->fetchRoom($xPosBlocks, $yPosBlocks, $zPosBlocks)) {

            $self->showMsgDialogue(
                'Add room',
                'error',
                'The gridblock x=' . $xPosBlocks . ', y=' . $yPosBlocks . ', z=' . $zPosBlocks
                . ' is already occupied',
                'ok',
            );

            return undef;
        }

        # Free click mode must be reset (nothing special happens when the user clicks on the map)
        $self->ivPoke('freeClickMode', 'default');

        # Create a new room object, with this region as its parent and update the map
        $roomObj = $self->createNewRoom(
            $self->currentRegionmap,
            $xPosBlocks,
            $yPosBlocks,
            $zPosBlocks,
        );

        if (! $roomObj) {

            # Could not create the new room (an error message has already been displayed)
            return undef;

        } else {

            # To make it easier to see where the new room was drawn, make it the selected room, and
            #   centre the map on the room
            $self->setSelectedObj(
                [$roomObj, 'room'],
                FALSE,          # Select this object; unselect all other objects
            );

            $self->centreMapOverRoom($roomObj);

            return 1;
        }
    }

    sub addExitCallback {

        # Called by $self->enableRoomsColumn
        # Adds a new exit, prompting the user for its properties
        #
        # Expected arguments
        #   $hiddenFlag - If set to TRUE, a hidden exit should be created (otherwise set to FALSE)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the user clicks
        #       'cancel' on the 'dialogue' window or if the exit can't be added
        #   1 otherwise

        my ($self, $hiddenFlag, $check) = @_;

        # Local variables
        my (
            $title, $dir, $mapDir, $assistedProf, $assistedMove, $result, $exitObj, $redrawFlag,
            $roomObj,
        );

        # Check for improper arguments
        if (! defined $hiddenFlag || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->addExitCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedRoom) {

            return undef;
        }

        # Prompt the user for properties of the new exit
        if ($hiddenFlag) {
            $title = 'Add hidden exit';
        } else {
            $title = 'Add exit';
        }

        ($dir, $mapDir, $assistedProf, $assistedMove) = $self->promptNewExit(
            $self->selectedRoom,
            $title,
        );

        if (! defined $dir) {

            return undef;
        }

        # Add the exit
        $exitObj = $self->worldModelObj->addExit(
            $self->session,
            FALSE,              # Don't redraw the map yet...
            $self->selectedRoom,
            $dir,
            $mapDir,
        );

        if (! $exitObj) {

            return undef;
        }

        # Add an entry to the exit's assisted moves hash, if one was specified by the user
        if ($assistedProf && $assistedMove) {

            $self->worldModelObj->addAssistedMove($exitObj, $assistedProf, $assistedMove);
        }

        # Mark it as a hidden exit, if necessary
        if ($hiddenFlag) {

            $self->worldModelObj->setHiddenExit(
                FALSE,          # Don't redraw the map yet...
                $exitObj,
                TRUE,           # Exit is now hidden
            );
        }

        # Now, we need to check if the room has any more unallocated exits. If they've temporarily
        #   been assigned the map direction 'undef', we must reallocate them
        OUTER: foreach my $number ($self->selectedRoom->ivValues('exitNumHash')) {

            my $thisExitObj = $self->worldModelObj->ivShow('exitModelHash', $number);

            if (! defined $thisExitObj->mapDir && $thisExitObj->drawMode eq 'primary') {

                # Assign the exit object a new map direction (using one of the sixteen cardinal
                #   directions, but not 'up' and 'down'), if any are available
                $self->worldModelObj->allocateCardinalDir(
                    $self->session,
                    $self->selectedRoom,
                    $thisExitObj,
                );
            }
        }

        # Now, if there are any incoming 1-way exits whose ->mapDir is the opposite of the exit
        #   we've just added, the incoming exit should be marked as an uncertain exit
        $self->worldModelObj->modifyIncomingExits(
            $self->session,
            TRUE,               # Redraw any modified incoming exit
            $self->selectedRoom,
            $exitObj,
        );

        # Remember the (currently selected) room object that must be redrawn in every window
        $roomObj = $self->selectedRoom;
        # Make this exit the selected exit (which redraws it in this window)
        $self->setSelectedObj(
            [$exitObj, 'exit'],
            FALSE,              # Select this object; unselect all other objects
        );

        # Redraw the selected room in every window
        $self->worldModelObj->updateMaps('room', $roomObj);

        return 1;
    }

    sub addMultipleExitsCallback {

        # Called by $self->enableRoomsColumn
        # Prompts the user to select one or more map directions not already in use by the selected
        #   room, and adds exit objects for each selected direction
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the user doesn't
        #       select any map directions or if an attempt to create an exit fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my @dirList;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->addMultipleExitsCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedRoom) {

            return undef;
        }

        # Prompt the user to select some of the selected room's available primary directions
        @dirList = $self->promptMultipleExits($self->selectedRoom);
        if (@dirList) {

            OUTER: foreach my $customDir (@dirList) {

                my ($mapDir, $exitObj);

                # $customDir is a custom primary direction. Get the equivalent standard direction
                $mapDir = $self->session->currentDict->ivShow('combRevDirHash', $customDir);

                # Add the exit
                $exitObj = $self->worldModelObj->addExit(
                    $self->session,
                    FALSE,              # Don't redraw the map yet...
                    $self->selectedRoom,
                    $customDir,
                    $mapDir,
                );

                if (! $exitObj) {

                    return undef;
                }

                # Now, if there are any incoming 1-way exits whose ->mapDir is the opposite of the
                #   exit we've just added, the incoming exit should be marked as an uncertain exit
                $self->worldModelObj->modifyIncomingExits(
                    $self->session,
                    TRUE,              # Redraw any modified incoming exit
                    $self->selectedRoom,
                    $exitObj,
                );
            }

            # Redraw the selected room in every window
            $self->worldModelObj->updateMaps('room', $self->selectedRoom);

            return 1;

        } else {

            # No exits were selected
            return undef;
        }
    }

    sub addFailedExitCallback {

        # Called by $self->enableRoomsColumn
        # When the character fails to move, and it's not a recognised failed exit pattern, the map
        #   gets messed up
        # This is a convenient way to deal with it. Adds a new failed exit string to the current
        #   world profile or to the specified room, and empties the Locator's move list
        #
        # Expected arguments
        #   $worldFlag   - If set to TRUE, a failed exit pattern is added to the world profile. If
        #                   set to FALSE, the pattern is added to the room
        #
        # Optional arguments
        #   $roomObj    - If $worldFlag is FALSE, the room to which the pattern should be added.
        #                   When called by $self->enableRoomsColumn, it will be the current room;
        #                   when called by ->enableRoomsPopupMenu, it will be the selected room
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the user
        #       doesn't supply a pattern
        #   1 otherwise

        my ($self, $worldFlag, $roomObj, $check) = @_;

        # Local variables
        my (
            $pattern, $type, $worldObj, $iv, $descrip, $taskObj,
            @comboList,
        );

        # Check for improper arguments
        if (! defined $worldFlag || (! $worldFlag && ! defined $roomObj) || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->addFailedExitCallback', @_);
        }

        # Standard callback check
        if ($roomObj && (! $self->currentRegionmap || ! $self->mapObj->currentRoom)) {

            return undef;
        }

        if (! $worldFlag) {

            # Prompt the user for a new failed exit pattern to add to the room
            $pattern = $self->showEntryDialogue(
                'Add failed exit to room',
                'Enter a pattern to match the failed exit',
            );

            if (! $pattern) {

                return undef;

            } else {

                $self->worldModelObj->addExitPattern($roomObj, 'fail', $pattern);
            }

        } else {

            # Import the current world profile
            $worldObj = $self->session->currentWorld;

            # Prompt the user for a new failed exit pattern to add to the world profile
            @comboList = ('Closed door', 'Locked door', 'Other failed exit');
            ($pattern, $type) = $self->showDoubleComboDialogue(
                'Add failed exit to world',
                'Enter a pattern to match the failed exit',
                'Which kind of failed exit was it?',
                \@comboList,
            );

            if (! ($pattern && $type)) {

                return undef;

            } else {

                # Check that the pattern isn't already in the list
                if ($type eq 'Closed door') {

                    $iv = 'doorPatternList';
                    $descrip = 'a closed door pattern';

                } elsif ($type eq 'Locked door') {

                    $iv = 'lockedPatternList';
                    $descrip = 'a locked door pattern';

                } else {
                    $iv = 'failExitPatternList';
                    $descrip = 'a failed exit pattern';
                }

                if ($worldObj->ivMatch($iv, $pattern)) {

                    $self->showMsgDialogue(
                        'Add failed exit to world',
                        'error',
                        'The current world profile already has ' . $descrip . ' pattern matching \''
                        . $pattern . '\'',
                        'ok',
                    );

                    return undef;

                } else {

                    # Add the pattern
                    $worldObj->ivPush($iv, $pattern);
                }
            }
        }

        # Import the Locator task
        $taskObj = $self->session->locatorTask;
        if ($taskObj) {

            # Empty the Locator's move list IVs and update its task window
            $taskObj->resetMoveList();
        }

        return 1;
    }

    sub addInvoluntaryExitCallback {

        # Called by $self->enableRoomsColumn
        # This callback adds an involuntary exit pattern to the specified room and empties the
        #   Locator task's move list
        #
        # Expected arguments
        #   $roomObj    - The room to which the pattern should be added. When called by
        #                   $self->enableRoomsColumn, it will be the current room; when called by
        #                   ->enableRoomsPopupMenu, it will be the selected room
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the user
        #       doesn't supply a pattern
        #   1 otherwise

        my ($self, $roomObj, $check) = @_;

        # Local variables
        my ($pattern, $taskObj);

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->addInvoluntaryExitCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->mapObj->currentRoom) {

            return undef;
        }

        # Prompt the user for a new involuntary exit pattern to add to the room
        $pattern = $self->showEntryDialogue(
            'Add involuntary exit to room',
            'Enter a pattern to match the involuntary exit',
        );

        if (! $pattern) {

            return undef;

        } else {

            $self->worldModelObj->addExitPattern($roomObj, 'involuntary', $pattern);

            # Import the Locator task
            $taskObj = $self->session->locatorTask;
            if ($taskObj) {

                # Empty the Locator's move list IVs and update its task window
                $taskObj->resetMoveList();
            }
        }

        return 1;
    }

    sub addRepulseExitCallback {

        # Called by $self->enableRoomsColumn
        # This callback adds a repulse exit pattern to the specified room and empties the Locator
        #   task's move list
        #
        # Expected arguments
        #   $roomObj    - The room to which the pattern should be added. When called by
        #                   $self->enableRoomsColumn, it will be the current room; when called by
        #                   ->enableRoomsPopupMenu, it will be the selected room
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the user
        #       doesn't supply a pattern
        #   1 otherwise

        my ($self, $roomObj, $check) = @_;

        # Local variables
        my ($pattern, $taskObj);

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->addRepulseExitCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->mapObj->currentRoom) {

            return undef;
        }

        # Prompt the user for a new involuntary exit pattern to add to the room
        $pattern = $self->showEntryDialogue(
            'Add repulse exit to room',
            'Enter a pattern to match the repulse exit',
        );

        if (! $pattern) {

            return undef;

        } else {

            $self->worldModelObj->addExitPattern($roomObj, 'repulse', $pattern);

            # Import the Locator task
            $taskObj = $self->session->locatorTask;
            if ($taskObj) {

                # Empty the Locator's move list IVs and update its task window
                $taskObj->resetMoveList();
            }
        }

        return 1;
    }

    sub addSpecialDepartureCallback {

        # Called by $self->enableRoomsColumn
        # When the character moves using an exit which doesn't send a room statement upon arrival
        #   in the new room - usually after some kind of faller - the pattern sent by the world to
        #   confirm arrival (such as 'You land in a big heap!') should be interpreted by the
        #   Locator task as a special kind of room statement
        # This callback adds a special departure pattern to the specified room and empties the
        #   Locator task's move list
        #
        # Expected arguments
        #   $roomObj    - The room to which the pattern should be added. When called by
        #                   $self->enableRoomsColumn, it will be the current room; when called by
        #                   ->enableRoomsPopupMenu, it will be the selected room
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the user
        #       doesn't supply a pattern
        #   1 otherwise

        my ($self, $roomObj, $check) = @_;

        # Local variables
        my ($pattern, $taskObj);

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->addSpecialDepartureCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->mapObj->currentRoom) {

            return undef;
        }

        # Prompt the user for a new special departure pattern to add to the room
        $pattern = $self->showEntryDialogue(
            'Add special departure to room',
            'Enter a pattern to match the special departure',
        );

        if (! $pattern) {

            return undef;

        } else {

            $self->worldModelObj->addExitPattern($roomObj, 'special', $pattern);

            # Import the Locator task
            $taskObj = $self->session->locatorTask;
            if ($taskObj) {

                # Empty the Locator's move list IVs and update its task window
                $taskObj->resetMoveList();
            }
        }

        return 1;
    }

    sub selectExitCallback {

        # Called by $self->enableRoomsColumn
        # Prompts the user to select an exit manually
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if there are no
        #       exits to select, or if the user clicks 'cancel' in the 'dialogue' window
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $choice, $selectExitObj,
            @exitList, @comboList,
            %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->selectExitCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedRoom) {

            return undef;
        }

        # Get a list of the select room's exits, in the standard order
        @exitList = $self->selectedRoom->sortedExitList;

        # Compile a hash in the form
        #   $hash{'informative_string'} = blessed_reference_to_exit_object
        foreach my $dir (@exitList) {

            my ($exitNum, $exitObj, $string, $customDir);

            # Prepare a string which shows:
            #   The exit's nominal direction and its exit model number
            #   Its temporarily allocated map direction in [square brackets]
            #   Its permanently allocated map direction in <diamond brackets>
            #   An unallocatable exit in {curly brackets}
            $exitNum = $self->selectedRoom->ivShow('exitNumHash', $dir);
            $exitObj = $self->worldModelObj->ivShow('exitModelHash', $exitNum);

            $string = $exitObj->dir . ' #' . $exitObj->number;

            if ($exitObj->mapDir) {

                # Get the equivalent custom direction, so that we can compare it to $dir
                $customDir = $self->session->currentDict->ivShow(
                    'primaryDirHash',
                    $exitObj->mapDir,
                );

                if ($customDir ne $exitObj->dir) {

                    if ($exitObj->drawMode eq 'temp_alloc') {
                        $string .= ' [' . $exitObj->mapDir . ']';
                    } else {
                        $string .= ' <' . $exitObj->mapDir . '>';
                    }
                }

            } elsif ($exitObj->drawMode eq 'temp_unalloc') {

                $string .= ' {unallocatable}';
            }

            # Add an entry to the hash...
            $exitHash{$string} = $exitObj;
            # ...and another in the combo list
            push (@comboList, $string);
        }

        # Don't prompt for an object, if there are none available
        if (! @comboList) {

            return $self->showMsgDialogue(
                'Select exit',
                'error',
                'Can\'t select an exit - this room has no exits',
                'ok',
            );
        }

        # Prompt the user to choose which exit to select
        $choice = $self->showComboDialogue(
            'Select exit',
            'Choose which exit to select',
            FALSE,
            \@comboList,
        );

        if (! $choice) {

            return undef;

        } else {

            # Get the corresponding ExitObj
            $selectExitObj = $exitHash{$choice};

            # Select this exit
            $self->setSelectedObj(
                [$selectExitObj, 'exit'],
                FALSE,      # Select this object; unselect all other objects
            );

            return 1;
        }
    }

    sub identifyRoomsCallback {

        # Called by $self->enableRoomsColumn
        # Lists the current room and all the selected rooms in a 'dialogue' window (if more than 10
        #   are selected, we only list the first 10)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $limit, $msg, $parentObj, $roomName,
            @roomList, @sortedList, @reducedList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->identifyRoomsCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            && (! $self->selectedRoom && ! $self->selectedRoomHash && ! $self->mapObj->currentRoom)
        ) {
            return undef;
        }

        # Compile a list of selected rooms, sorted by world model number
        @roomList = $self->compileSelectedRooms();
        @sortedList = sort {$a->number <=> $b->number} (@roomList);

        # Reduce the size of the list to a maximum of 10
        $limit = 10;
        if (@sortedList > $limit) {
            @reducedList = @sortedList[0..($limit - 1)];
        } else {
            @reducedList = @sortedList;
        }

        # Prepare the message to show in the window
        if ($self->mapObj->currentRoom) {

            $msg = "Current room:\n";
            $msg .= "   #" . $self->mapObj->currentRoom->number . " ('";

            $parentObj
                = $self->worldModelObj->ivShow('modelHash', $self->mapObj->currentRoom->parent);
            $msg .= $parentObj->name . "' region) '";

            # '<unnamed room>' will cause a Pango error, so replace that string
            $roomName = $self->mapObj->currentRoom->name;
            if ($roomName eq '<unnamed room>') {

                $roomName = '(unnamed room)';
            }

            $msg .= substr($roomName, 0, 64) . "'\n\n";

        } else {

            $msg = '';
        }

        if (@reducedList) {

            if (scalar @sortedList != scalar @reducedList) {

                $msg .= "Selected rooms (first " . $limit . " rooms of " . scalar @sortedList
                        . ")";

            } elsif (scalar @sortedList == 1) {

                $msg .= "Selected rooms (1 room)";

            } else {

                $msg .= "Selected rooms (" . scalar @sortedList . " rooms)";
            }

            foreach my $obj (@reducedList) {

                $parentObj = $self->worldModelObj->ivShow('modelHash', $obj->parent);
                $msg .= "\n   #" . $obj->number . " (region '" . $parentObj->name . "') '";

                if ($obj->name eq '<unnamed room>') {
                    $msg .= "(unnamed room)'";
                } else {
                    $msg .= substr($obj->name, 0, 64) . "'";
                }
            }
        }

        # Display a popup to show the results
        $self->showMsgDialogue(
            'Identify rooms',
            'info',
            $msg,
            'ok',
        );

        return 1;
    }

    sub updateVisitsCallback {

        # Called by $self->enableRoomsPopupMenu (only)
        # Adjusts the number of character visits shown in the selected room (quicker than opening
        #   the room's 'edit' window, and making the changes there)
        # Normally, the current character's visits are changed. However, if $self->showChar is set,
        #   that character's visits are changed
        #
        # Expected arguments
        #   $mode   - 'increase' to increase the number of visits by one, 'decrease' to decrease the
        #               visits by one, 'manual' to let the user enter a value manually, 'reset' to
        #               reset the number to zero
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the user clicks
        #       the 'cancel' button on a 'dialogue' window or for any other error
        #   1 otherwise

        my ($self, $mode, $check) = @_;

        # Local variables
        my ($char, $current, $result);

        # Check for improper arguments
        if (
            ! defined $mode
            || ($mode ne 'increase' && $mode ne 'decrease' && $mode ne 'manual' && $mode ne 'reset')
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->updateVisitsCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedRoom) {

            return undef;
        }

        # Decide which character to use
        if ($self->showChar) {

            $char = $self->showChar;

        } elsif ($self->session->currentChar) {

            $char = $self->session->currentChar->name;

        } else {

            $self->showMsgDialogue(
                'Update character visits',
                'error',
                'Can\'t update the number of visits - there is no current character set',
                'ok',
            );

            return undef;
        }

        # Update room visits
        if ($mode eq 'increase') {

            # Increase by one
            if ($self->selectedRoom->ivExists('visitHash', $char)) {
                $self->selectedRoom->ivIncHash('visitHash', $char);
            } else {
                $self->selectedRoom->ivAdd('visitHash', $char, 1);
            }

        } elsif ($mode eq 'decrease') {

            # Decrease by one
            if ($self->selectedRoom->ivExists('visitHash', $char)) {

                $self->selectedRoom->ivDecHash('visitHash', $char);
                # If the number of visits is down to 0, remove the entry from the hash (so that we
                #   don't get -1 visits the next time)
                if (! $self->selectedRoom->ivShow('visitHash', $char)) {

                    $self->selectedRoom->ivDelete('visitHash', $char);
                }
            }

        } elsif ($mode eq 'manual') {

            # Set manually
            $current = $self->selectedRoom->ivShow('visitHash', $char);
            if (! $current) {

                # If there's no entry for this character in the room's ->visitHash, make sure the
                #   'dialogue' window displays a value of 0
                $current = 0;
            }

            $result = $self->showEntryDialogue(
                'Update character visits',
                'Enter the number of visits to this room #' . $self->selectedRoom->number
                . ' by \'' . $char . '\'',
                undef,              # No max number of characters
                $current,
            );

            if (! defined $result) {

                # User clicked 'cancel' button in the 'dialogue' window
                return undef;

            } elsif (($result =~ /\D/) || $result < 0) {

                $self->showMsgDialogue(
                    'Update character visits',
                    'error',
                    'Invalid value (' . $result . ') - must be an integer, 0 or above',
                    'ok',
                );

                return undef;

            } else {

                if ($result) {
                    $self->selectedRoom->ivAdd('visitHash', $char, $result);
                } else {
                    $self->selectedRoom->ivDelete('visitHash', $char);
                }
            }

        } else {

            # Reset to zero
            if ($self->selectedRoom->ivExists('visitHash', $char)) {

                $self->selectedRoom->ivDelete('visitHash', $char);
            }
        }

        # Mark the selected room to be re-drawn, in case the room and its character visits are
        #   currently visible
        $self->markObjs('room', $self->selectedRoom);

        # Get the new number of visits for this room...
        $current = $self->selectedRoom->ivShow('visitHash', $char);
        if (! $current) {

            $current = 0;
        }

        # ...and then show a confirmation
        $self->showMsgDialogue(
            'Update character visits',
            'info',
            'Visits by \'' . $char . '\' to room #' . $self->selectedRoom->number . ' set to '
            . $current,
            'ok',
        );

        return 1;
    }

    sub setFilePathCallback {

        # Called by $self->enableRoomsColumn
        # Sets the file path for the world's source code file (if known) for the selected room
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the user
        #       clicks the 'cancel' button on the 'dialogue' window
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($filePath, $virtualPath);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setFilePathCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedRoom) {

            return undef;
        }

        # Prompt the user for the file path, and (optionally) the virtual area path
        ($filePath, $virtualPath) = $self->promptFilePath($self->selectedRoom);
        if (! defined $filePath) {

            # User clicked 'cancel' button in the 'dialogue' window
            return undef;

        } else {

            # Modify the world model room
            $self->worldModelObj->setRoomSource($self->selectedRoom, $filePath, $virtualPath);
            return 1;
        }
    }

    sub setVirtualAreaCallback {

        # Called by $self->enableRoomsColumn
        # Sets or resets the virtual area path for the selected room(s)
        #
        # Expected arguments
        #   $setFlag    - Set to TRUE if the rooms' ->virtualAreaPath IV should be set; set to FALSE
        #                   if it should be reset (set to 'undef')
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if there are no
        #       rooms that can be modified or if the user clicks the 'cancel' button on the
        #       'dialogue' window
        #   1 otherwise

        my ($self, $setFlag, $check) = @_;

        # Local variables
        my (
            $virtualPath, $msg,
            @roomList, @useList, @ignoreList,
        );

        # Check for improper arguments
        if (! defined $setFlag || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setVirtualAreaCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || (! $self->selectedRoom && ! $self->selectedRoomHash)) {

            return undef;
        }

        # Get a list of selected room(s)
        @roomList = $self->compileSelectedRooms();

        # Check each room to make sure each has a ->sourceCodePath, eliminating those that don't
        #   (but don't bother in reset mode)
        if ($setFlag) {

            foreach my $roomObj (@roomList) {

                if ($roomObj->sourceCodePath) {
                    push (@useList, $roomObj);
                } else {
                    push (@ignoreList, $roomObj);
                }
            }

        } else {

            # When resetting, use all the selected rooms
            @useList = @roomList;
        }

        if (! @useList) {

            $self->showMsgDialogue(
                'Set virtual area',
                'error',
                'Cannot set the virtual area for these rooms (probably because no source code path'
                . ' has been set for them)',
                'ok',
            );

            return undef;
        }

        # Set the virtual area for the selected room(s)
        if ($setFlag) {

            if (@useList == 1) {
                $msg = 'Set the path to the virtual area file for one selected room';
            } else {
                $msg = 'Set the path to the virtual area file for ' . @roomList
                            . ' of the selected rooms';
            }

            # Prompt the user for the virtual area path
            $virtualPath = $self->showEntryDialogue(
                'Set virtual area',
                $msg,
                undef,              # No maximum number of characters
                $self->worldModelObj->lastVirtualAreaPath,
            );

            if (! defined $virtualPath) {

                # User clicked 'cancel' button in the 'dialogue' window
                return undef;

            } else {

                # Set the virtual area paths
                foreach my $roomObj (@useList) {

                    # (Keep the existing value of the room's ->sourceCodePath IV)
                    $self->worldModelObj->setRoomSource(
                        $roomObj,
                        $roomObj->sourceCodePath,
                        $virtualPath,
                    );
                }

                # Display a confirmation
                if (@useList == 1) {
                    $msg = 'one selected room';
                } else {
                    $msg = scalar @useList . ' of the selected rooms';
                }

                $self->showMsgDialogue(
                    'Set virtual area',
                    'info',
                    "Set the virtual area file for " . $msg . " to:\n" . $virtualPath,
                    'ok',
                );
            }

        # Reset the virtual area for the selected room(s)
        } else {

            # Reset the virtual area paths
            foreach my $roomObj (@roomList) {

                # (Keep the existing value of the room's ->sourceCodePath IV)
                $self->worldModelObj->setRoomSource(
                    $roomObj,
                    $roomObj->sourceCodePath,
                    undef,      # No virtual path
                );
            }

            # Display a confirmation
            if (@useList == 1) {
                $msg = 'the selected room';
            } else {
                $msg = scalar @roomList . ' selected rooms';
            }

            # Display a confirmation
            $self->showMsgDialogue(
                'Reset virtual area',
                'info',
                'The virtual area file for ' . $msg . ' has been reset',
                'ok',
            );
        }

        return 1;
    }

    sub editFileCallback {

        # Called by $self->enableRoomsColumn
        # Opens the mudlib file corresponding to the selected room in Axmud's external text editor
        #   (the one specified by GA::Client->textEditCmd)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $virtualFlag    - If set to TRUE, we need to edit the file stored in the room object's
        #                       ->virtualAreaPath. If set to FALSE (or 'undef'), we need to edit the
        #                       file stored in $obj->sourceCodePath
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if no external
        #       text editor is specified by the GA::Client
        #   1 otherwise

        my ($self, $virtualFlag, $check) = @_;

        # Local variables
        my ($cmd, $file);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->editFileCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->selectedRoom
            || (
                ! defined $virtualFlag
                && (! $self->selectedRoom->sourceCodePath || $self->selectedRoom->virtualAreaPath)
            ) || (defined $virtualFlag && ! $self->selectedRoom->virtualAreaPath)
        ) {
            return undef;
        }

        # Check that the GA::Client has a text editor command set, and that it is valid
        $cmd = $axmud::CLIENT->textEditCmd;
        if (! $cmd || ! ($cmd =~ m/%s/)) {

            # Show a 'dialogue' window to explain the problem
            $self->showMsgDialogue(
                'Edit source code file',
                'error',
                'Can\'t edit the file: invalid external application command \'' . $cmd . '\'',
                'ok',
            );

            return undef;
        }

        # Set the file to be opened. If the current world model defines a mudlib directory, the
        #   object's ->mudlibPath is relative to that; otherwise it's an absolute path
        if ($self->session->worldModelObj->mudlibPath) {
            $file = $self->session->worldModelObj->mudlibPath;
        } else {
            $file = '';
        }

        if ($virtualFlag) {
            $file .= $self->selectedRoom->virtualAreaPath;
        } else {
            $file .= $self->selectedRoom->sourceCodePath;
        }

        # Add the file extension, if set
        if ($self->session->worldModelObj->mudlibExtension) {

            $file .= $self->session->worldModelObj->mudlibExtension;
        }

        # Check the file exists
        if (! (-e $file)) {

            $self->showMsgDialogue(
                'Edit source code file',
                'error',
                'Can\'t find the file \'' . $file . '\'',
                'ok',
            );

            return undef;
        }

        # Open the file in the external text editor
        $cmd =~ s/%s/$file/;

        system $cmd;

        return 1;
    }

    sub addContentsCallback {

        # Called by $self->enableRoomsColumn
        # Adds a non-model object (or objects) from the Locator's current room to the world model,
        #   making them children of (and therefore contained in) the current room
        # Alternatively, prompts the user to add a string like 'two hairy orcs and an axe'. Parses
        #   the string into a list of objects, and prompts the user to choose an object from that
        #   list
        #
        # Expected arguments
        #   $parseFlag  - Set to TRUE if the user should be prompted for a sentence to parse. Set to
        #                   FALSE if the list of objects should be taken from the Locator task
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the Locator task
        #       isn't running or doesn't know the current location, if its room's temporary contents
        #       list is empty or if an attempt to parse a string fails
        #   1 otherwise

        my ($self, $parseFlag, $check) = @_;

        # Local variables
        my (
            $taskObj, $roomObj, $string, $allString, $choice,
            @tempList, @useList, @comboList, @addList,
            %comboHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->addContentsCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || (
                (! $parseFlag && ! $self->mapObj->currentRoom)
                || ($parseFlag && ! $self->selectedRoom)
            )
        ) {
            return undef;
        }

        if (! $parseFlag) {

            # The list of objects should be taken from the Locator task's current room. Import the
            #   Locator task
            $taskObj = $self->session->locatorTask;
            # Check the Locator task exists and that it knows about the character's current
            #   location
            if (! $taskObj || ! $taskObj->roomObj) {

                # Show a 'dialogue' window to explain the problem
                $self->showMsgDialogue(
                    'Add contents',
                    'error',
                    'Either the Locator task isn\'t running or it doesn\'t know the current'
                    . ' location',
                    'ok',
                );

                return undef;
            }

            # Use the automapper's current room
            $roomObj = $self->mapObj->currentRoom;

            # Import the list of temporary non-model objects from the Locator's current room
            @tempList = $taskObj->roomObj->tempObjList;
            if (! @tempList) {

                $self->showMsgDialogue(
                    'Add contents',
                    'error',
                    'The Locator task\'s current room appears to be empty',
                    'ok',
                );

                return undef;
            }

            # From this list, remove any temporary objects which have already been added to the
            #   automapper room's list of child objects during the current visit to the room
            OUTER: foreach my $tempObj (@tempList) {

                foreach my $childNum ($roomObj->ivKeys('childHash')) {

                    if ($tempObj eq $self->worldModelObj->ivShow('modelHash', $childNum)) {

                        # Don't add it again
                        next OUTER;
                    }
                }

                # $tempObj hasn't been added to the model yet
                push (@useList, $tempObj);
            }

        } else {

            # The user should be prompted for a string to parse. Use the (single) selected room
            $roomObj = $self->selectedRoom;

            # Prompt the user to enter a string to parse
            $string = $self->showEntryDialogue(
                'Add contents',
                'Enter a string to parse (e.g. \'two hairy orcs and an axe\')',
            );

            if (! defined $string) {

                # User clicked 'cancel' or closed the window
                return undef;

            } else {

                # Try to parse the string into a list of objects (parse multiples as separate
                #   objects)
                @useList = $self->worldModelObj->parseObj($self->session, FALSE, $string);
            }
        }

        # Don't prompt for an object, if there are none available
        if (! @useList) {

            return $self->showMsgDialogue(
                'Add contents',
                'error',
                'There are no objects to add',
                'ok',
            );
        }

        # Prepare a list of strings to display in a combobox
        foreach my $obj (@useList) {

            my $line;

            if ($obj->category eq 'portable' || $obj->category eq 'decoration') {
                $line = $obj->name . ' [' . $obj->category . ' - ' . $obj->type . ']';
            } else {
                $line = $obj->name . ' [' . $obj->category . ']';
            }

            push (@comboList, $line);
            $comboHash{$line} = $obj;
        }

        # If there is more than one object that could be added, create something at the top of the
        #   combobox that lets the user add them all
        if (@comboList > 1) {

            $allString = '<add all ' . scalar @comboList . ' objects>';
            unshift (@comboList, $allString);
        }

        # Prompt the user to select an object
        $choice = $self->showComboDialogue(
            'Select object',
            'Choose which object(s) to add to the world model',
            FALSE,
            \@comboList,
        );

        if ($choice) {

            if ($allString && $choice eq $allString) {

                # Add all the objects to the model (use @useList, in case @comboList contained
                #   repeating strings, because there's more than one orc, for example, in the room)
                @addList = @useList;

            } else {

                # Add a single object to the model
                push (@addList, $comboHash{$choice});
            }

            # Add the objects to the world model as children of $roomObj
            $self->worldModelObj->addRoomChildren(
                TRUE,                   # Update Automapper windows
                FALSE,                  # Children are not hidden
                $roomObj,
                undef,                  # Children are not hidden
                @addList,
            );
        }

        return 1;
    }

    sub addHiddenObjCallback {

        # Called by $self->enableRoomsColumn
        # Adds a non-model object from the Locator's current room to the world model, making it a
        #   child (and therefore contained in) the current room
        # Alternatively, prompts the user to add a string like 'two hairy orcs and an axe'. Parses
        #   the string into a list of objects, and prompts the user to choose an object from that
        #   list
        #
        # Expected arguments
        #   $parseFlag  - Set to TRUE if the user should be prompted for a sentence to parse. Set to
        #                   FALSE if the list of objects should be taken from the Locator task
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the hidden
        #       object isn't added
        #   1 otherwise

        my ($self, $parseFlag, $check) = @_;

        # Local variables
        my (
            $taskObj, $roomObj, $string, $obtainCmd, $choice,
            @tempList, @useList, @comboList,
            %comboHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->addHiddenObjCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || (
                (! $parseFlag && ! $self->mapObj->currentRoom)
                || ($parseFlag && ! $self->mapObj->selectedRoom)
            )
        ) {
            return undef;
        }

        if (! $parseFlag) {

            # The list of objects should be taken from the Locator task's current room. Import the
            #   Locator task
            $taskObj = $self->session->locatorTask;
            # Check the Locator task exists and that it knows about the character's current
            #   location
            if (! $taskObj || ! $taskObj->roomObj) {

                # Show a 'dialogue' window to explain the problem
                $self->showMsgDialogue(
                    'Add hidden object',
                    'error',
                    'Either the Locator task isn\'t running or it doesn\'t know the current'
                    . ' location',
                    'ok',
                );

                return undef;
            }

            # Use the automapper's current room
            $roomObj = $self->mapObj->currentRoom;

            # Import the list of temporary non-model objects from the Locator's current room
            @tempList = $taskObj->roomObj->tempObjList;
            if (! @tempList) {

                $self->showMsgDialogue(
                    'Add hidden object',
                    'error',
                    'The Locator task\'s current room appears to be empty',
                    'ok',
                );

                return undef;
            }

            # From this list, remove any temporary objects which have already been added to the
            #   automapper room's list of child objects during the current visit to the room
            OUTER: foreach my $tempObj (@tempList) {

                foreach my $childNum ($roomObj->ivKeys('childHash')) {

                    if ($tempObj eq $self->worldModelObj->ivShow('modelHash', $childNum)) {

                        # Don't add it again
                        next OUTER;
                    }
                }

                # $tempObj hasn't been added to the model yet
                push (@useList, $tempObj);
            }

        } else {

            # The user should be prompted for a string to parse. Use the (single) selected room
            $roomObj = $self->selectedRoom;

            # Prompt the user to enter a string to parse
            $string = $self->showEntryDialogue(
                'Add hidden object',
                'Enter a string to parse (e.g. \'two hairy orcs and an axe\')',
            );

            if (! defined $string) {

                # User clicked 'cancel' or closed the window
                return undef;

            } else {

                # Try to parse the string into a list of objects. The TRUE argument tells the
                #   function to treat 'two hairy orcs' as a single object, with its
                #   ->multiple IV set to 2, so that the same strings don't appear in the combobox
                #   more than once (hopefully)
                @useList = $self->worldModelObj->parseObj($self->session, TRUE, $string);
            }
        }

        # Don't prompt for an object, if there are none available
        if (! @useList) {

            return $self->showMsgDialogue(
                'Add hidden object',
                'error',
                'There are no objects to add',
                'ok',
            );
        }

        # Prepare a list of strings to display in a combobox
        foreach my $obj (@useList) {

            my $line;

            if ($obj->category eq 'portable' || $obj->category eq 'decoration') {
                $line = $obj->name . ' [' . $obj->category . ' - ' . $obj->type . ']';
            } else {
                $line = $obj->name . ' [' . $obj->category . ']';
            }

            push (@comboList, $line);
            $comboHash{$line} = $obj;
        }

        ($obtainCmd, $choice) = $self->showDoubleComboDialogue(
            'Select object',
            'Enter the command used to obtain the hidden object',
            'Choose which hidden object to add to the model',
            \@comboList,
        );

        if ($choice) {

            # Add the object to the world model as a (hidden) child of $roomObj
            $self->worldModelObj->addRoomChildren(
                TRUE,                   # Update Automapper windows
                TRUE,                   # Mark child as hidden
                $roomObj,
                $obtainCmd,
                $comboHash{$choice},    # The non-model object to add to the world model
            );
        }

        return 1;
    }

    sub addSearchResultCallback {

        # Called by $self->enableRoomsColumn
        # Adds the results of a 'search' command at the current location (stored in the
        #   room object's ->searchHash IV)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($term, $result);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->addSearchResultCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->mapObj->currentRoom) {

            return undef;
        }

        # Prompt the user for a search term (e.g. 'fireplace') and the result (e.g.
        #   'It's a dirty old fireplace')
        ($term, $result) = $self->showDoubleEntryDialogue(
            'Add search result',
            'Add a search term (e.g. \'fireplace\')',
            'Add the result (e.g. \'It\'s an old fireplace.\')',
        );

        if ($term && $result) {

            # Add the search term and result to the current room's search hash, replacing the entry
            #   for the same search term, if it already exists
            $self->worldModelObj->addSearchTerm($self->mapObj->currentRoom, $term, $result);
        }

        return 1;
    }

    sub setRoomTagCallback {

        # Called by $self->enableRoomsColumn
        # Sets (or resets) the selected room's room tag
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the supplied tag
        #       is invalid or if the user declines to reassign an existing room tag
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($roomObj, $tag, $oldRoomNum, $oldRoomObj, $text, $regionObj, $result);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setRoomTagCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || (! $self->selectedRoom && ! $self->selectedRoomTag)) {

            return undef;
        }

        # Decide which room to use. If there's a single selected room; use it. If there's a single
        #   selected room tag, use its parent room
        if ($self->selectedRoom) {

            $roomObj = $self->selectedRoom;

        } elsif ($self->selectedRoomTag) {

            # (The IV stores the blessed reference of the room tag's parent room)
            $roomObj = $self->selectedRoomTag;
        }

        # Prompt the user for a tag
        $tag = $self->showEntryDialogue(
            'Set room tag',
            "Enter the selected room\'s tag\n(or leave empty to delete a tag)",
            undef,                  # No maximum number of characters
            $roomObj->roomTag,
        );

        if (defined $tag) {

            if (! $tag) {

                # Reset the room's tag. The TRUE argument instructs the world model to update its
                #   Automapper windows
                $self->worldModelObj->resetRoomTag(TRUE, $roomObj);

            } else {

                # Check the tag is valid
                if (length($tag) > 16) {

                    $self->showMsgDialogue(
                        'Set room tag',
                        'error',
                        'Invalid room tag \'' . $tag . '\' - max size 16 characters',
                        'ok',
                    );

                    return undef;

                } elsif ($tag =~ m/@@@/) {

                    $self->showMsgDialogue(
                        'Set room tag',
                        'error',
                        'Invalid room tag \'' . $tag . '\' - tag must not contain \'@@@\'',
                        'ok',
                    );

                    return undef;
                }

                # If the tag already belongs to another room, it gets reassigned to this one
                # If the other room is on the map, but is not currently visible, it won't be obvious
                #   to the user that the tag has been reassigned, rather than created
                # Prompt the user before reassigning a tag from one mapped room to another (but
                #   don't prompt if the old and new room are the same!)
                $oldRoomNum = $self->worldModelObj->checkRoomTag($tag);
                if (defined $oldRoomNum && $oldRoomNum != $roomObj->number) {

                    # Prepare the text to show
                    $oldRoomObj = $self->worldModelObj->ivShow('modelHash', $oldRoomNum);

                    $text = 'The tag \'' . $oldRoomObj->roomTag . '\' is already assigned to room #'
                                . $oldRoomNum . "\n";

                    if (
                        $self->currentRegionmap
                        && $self->currentRegionmap->number eq $oldRoomObj->parent
                    ) {
                        $text .= 'in this region. ';

                    } else {

                        $regionObj = $self->worldModelObj->ivShow('modelHash', $oldRoomObj->parent);
                        $text .= 'in the region \'' . $regionObj->name . '\'. ';
                    }

                    $text .= 'Do you want to reassign it?';

                    # Prompt the user
                    $result = $self->showMsgDialogue(
                        'Reassign room tag',
                        'question',
                        $text,
                        'yes-no',
                    );

                    if ($result eq 'no') {

                        return undef;
                    }
                }

                # Set the room's tag
                $self->worldModelObj->setRoomTag(TRUE, $roomObj, $tag);

                # If the Locator task is running, update it
                $self->mapObj->updateLocator();
            }
        }

        return 1;
    }

    sub setRoomGuildCallback {

        # Called by $self->enableRoomsColumn
        # Sets a room's guild (->roomGuild)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $noGuildString, $msg, $choice, $guildName,
            @profList, @sortedList, @comboList, @selectedList, @finalList,
            %comboHash, %itemHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setRoomGuildCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || (
                ! $self->selectedRoom && ! $self->selectedRoomHash && ! $self->selectedRoomGuild
                && ! $self->selectedRoomGuildHash
            )
        ) {
            return undef;
        }

        # Compile a list of guild profiles and sort alphabetically
        foreach my $profObj ($self->session->ivValues('profHash')) {

            if ($profObj->category eq 'guild') {

                push (@profList, $profObj);
            }
        }

        @sortedList = sort {lc($a->name) cmp lc($b->name)} (@profList);

        # Prepare a list to show in a combo box. At the same time, compile a hash in the form:
        #   $hash{combo_box_string} = blessed_reference_of_corresponding_profile
        foreach my $profObj (@sortedList) {

            push (@comboList, $profObj->name);
            $comboHash{$profObj->name} = $profObj;
        }

        # Put an option to use no guild at the top of the combo list
        $noGuildString = '<room not a guild>';
        unshift (@comboList, $noGuildString);

        if ($self->selectedRoom) {

            $msg = 'selected room';
            if ($self->selectedRoom->roomGuild) {

                $msg .= "\n(currently set to \'" . $self->selectedRoom->roomGuild . "\')";
            }

        } elsif ($self->selectedRoomGuild) {

            $msg = "selected room guild\n(currently set to \'" . $self->selectedRoomGuild->roomGuild
                    . "\')";

        } else {

            $msg = 'selected rooms';
        }

        # Prompt the user for a profile
        $choice = $self->showComboDialogue(
            'Select room guild',
            'Select the guild for the ' . $msg,
            FALSE,
            \@comboList,
        );

        if ($choice) {

            # Convert $choice into a guild profile name
            if ($choice eq $noGuildString) {

                $guildName = undef;     # Room has no guild set

            } else {

                $guildName = $comboHash{$choice}->name;
            }

            # Compile a list of selected rooms and selected room guilds
            push (@selectedList, $self->compileSelectedRooms(), $self->compileSelectedRoomGuilds());

            # Combine them into a single list, @finalList, eliminating duplicate rooms
            foreach my $roomObj (@selectedList) {

                if (! exists $itemHash{$roomObj->number}) {

                    push (@finalList, $roomObj);
                    $itemHash{$roomObj->number} = undef;
                }
            }

            # Update the guild for each room
            $self->worldModelObj->setRoomGuild(
                TRUE,           # Update the Automapper windows now
                $guildName,     # Name of a guild profile
                @finalList,
            );
        }

        return 1;
    }

    sub resetRoomOffsetsCallback {

        # Called by $self->enableRoomsColumn
        # Resets the drawn positions (offsets) of the room tags and room guilds for the selected
        #   room(s)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            @roomList, @combinedList,
            %roomHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->resetRoomOffsetsCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedRoom) {

            return undef;
        }

        # Get a list of selected rooms, room tags and room guilds
        push (@roomList,
            $self->compileSelectedRooms(),
            $self->compileSelectedRoomTags(),
            $self->compileSelectedRoomGuilds(),
        );

        # Combine these lists into a single list of affected rooms, eliminating duplicates and any
        #   selected room which doesn't have a room tag or a room guild
        foreach my $roomObj (@roomList) {

            if (
                ! exists $roomHash{$roomObj->number}
                && ($roomObj->roomTag || $roomObj->roomGuild)
            ) {
                push (@combinedList, $roomObj);
                $roomHash{$roomObj->number} = undef;
            }
        }

        # Reset the position of the room tags/room guilds in each affected room (if there are any)
        #   and instruct the world model to update its Automapper windows
        $self->worldModelObj->resetRoomOffsets(
            TRUE,               # Update Automapper windows now
            0,                  # Mode 0 - reset both room tags and room guilds
            @combinedList,
        );

        return 1;
    }

    sub toggleExclusiveProfileCallback {

        # Called by $self->enableRoomsColumn
        # Toggles the exclusivity for one or more selected rooms (specifically, toggles the rooms'
        #   ->exclusiveFlag IV)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $flagSetting, $mismatchFlag, $msg,
            @roomList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->toggleExclusiveProfileCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || (! $self->selectedRoom && ! $self->selectedRoomHash)) {

            return undef;
        }

        # Get a list of selected rooms
        @roomList = $self->compileSelectedRooms();

        # Toggle their ->exclusive flags
        $self->worldModelObj->toggleRoomExclusivity(
            TRUE,           # Update Automapper windows now
            @roomList,
        );

        # Compose a message to display. Find out if every room in @roomList has its
        #   ->exclusiveFlag set to the same value
        OUTER: foreach my $roomObj (@roomList) {

            if (! defined $flagSetting) {

                # This is the first room in @roomList
                $flagSetting = $roomObj->exclusiveFlag;

            } elsif ($flagSetting != $roomObj->exclusiveFlag) {

                # The rooms in @roomList have their ->exclusiveFlag IV set to different values
                $mismatchFlag = TRUE;
                last OUTER;
            }
        }

        if ($mismatchFlag) {

            $msg = 'Toggled exclusivity for ';

            if ($self->selectedRoom) {
                $msg .= '1 room';
            } else {
                $msg .= scalar @roomList . ' rooms';
            }

        } else {

            $msg = 'Exclusivity for ';

            if ($self->selectedRoom) {
                $msg .= '1 room';
            } else {
                $msg .= scalar @roomList . ' rooms';
            }

            if ($flagSetting) {
                $msg .= ' turned on';
            } else {
                $msg .= ' turned off';
            }
        }

        $self->showMsgDialogue(
            'Toggle exclusive profiles',
            'info',
            $msg,
            'ok',
        );

        return 1;
    }

    sub addExclusiveProfileCallback {

        # Called by $self->enableRoomsColumn
        # Adds a profile to the selected room's exclusive profile hash
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $choice,
            @profList, @sortedList, @finalList, @comboList,
            %comboHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->addExclusiveProfileCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedRoom) {

            return undef;
        }

        # Get a sorted list of profiles, not including world profiles
        foreach my $profObj ($self->session->ivValues('profHash')) {

            if ($profObj->category ne 'world') {

                push (@profList, $profObj);
            }
        }

        @sortedList = sort {lc($a->name) cmp lc($b->name)} (@profList);

        # Remove any profile which is already an exclusive profile for this room
        foreach my $profObj (@sortedList) {

            if (! $self->selectedRoom->ivExists('exclusiveHash', $profObj->name)) {

                push (@finalList, $profObj);
            }
        }

        # Don't prompt for a profile, if there are none available
        if (! @finalList) {

            return $self->showMsgDialogue(
                'Select profile',
                'warning',
                'Can\'t select a profile - there are none available',
                'ok',
            );
        }

        # Prepare a list to show in a combo box. At the same time, compile a hash in the form:
        #   $hash{combo_box_string} = blessed_reference_to_corresponding_profile
        foreach my $profObj (@finalList) {

            my $string =  $profObj->name . ' [' . $profObj->category . ']';

            push (@comboList, $string);
            $comboHash{$string} = $profObj;
        }

        # Prompt the user for a profile
        $choice = $self->showComboDialogue(
            'Select profile',
            'Select a profile which has exclusive access to this room',
            FALSE,
            \@comboList,
        );

        if ($choice) {

            $self->worldModelObj->setRoomExclusiveProfile(
                TRUE,       # Update Automapper windows
                $self->selectedRoom,
                $comboHash{$choice}->name,
            );
        }

        return 1;
    }

    sub resetExclusiveProfileCallback {

        # Called by $self->enableRoomsColumn
        # Resets the list of exclusive profiles for the selected rooms
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $msg,
            @roomList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->resetExclusiveProfileCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || (! $self->selectedRoom && ! $self->selectedRoomHash)) {

            return undef;
        }

        # Get a list of selected rooms
        @roomList = $self->compileSelectedRooms();

        # Reset their lists of exclusive profiles
        $self->worldModelObj->resetExclusiveProfiles(
            TRUE,           # Update Automapper windows now
            @roomList,
        );

        # Compose a message to display
        $msg = 'Reset exclusive profiles for ';

        if ($self->selectedRoom) {
            $msg .= '1 room';
        } else {
            $msg .= scalar @roomList . ' rooms';
        }

        $self->showMsgDialogue(
            'Reset exclusive profiles',
            'info',
            $msg,
            'ok',
        );

        return 1;
    }

    # Menu 'Exits' column callbacks

    sub changeDirCallback {

        # Called by $self->enableExitsColumn
        # Changes an existing exit's direction and/or its map direction, prompting the user for the
        #   new directions
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the user clicks
        #       'cancel' on the 'dialogue' window or if the exit directions can't be changed
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($roomObj, $exitObj, $dir, $mapDir, $result);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->changeDirCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->selectedExit
            || (
                $self->selectedExit->drawMode ne 'primary'
                && $self->selectedExit->drawMode ne 'perm_alloc'
            )
        ) {
            return undef;
        }

        # Get the parent room object
        $roomObj = $self->worldModelObj->ivShow('modelHash', $self->selectedExit->parent);

        # When a user selects an exit, they may be referring either to the exit stored in
        #   $self->selectedExit, its twin exit (if there is one) or its shadow exit (if there is
        #   one). Prompt the user to find out which
        $exitObj = $self->promptSpecifyExit('Change direction for which exit?');
        if (! $exitObj) {

            # User clicked the 'cancel' button, or closed the 'dialogue' window
            return undef;
        }

        # If this exit has been allocated a shadow exit, then the 'change direction' operation
        #   merely reassigns it as an unallocated exit
        if ($exitObj->shadowExit) {

            $result = $self->worldModelObj->changeShadowExitDir(
                $self->session,
                TRUE,       # Update Automapper windows now
                $roomObj,
                $exitObj,
            );

            if (! $result) {

                $self->showMsgDialogue(
                    'Change exit direction',
                    'warning',
                    'The exit (which has a shadow exit) could not be reassigned as an unallocated'
                    . ' exit',
                    'ok',
                );

                return undef;

            } else {

                return 1;
            }

        # Otherwise, the user needs to specify the new direction
        } else {

            # Prompt the user for new directions for the exit
            ($dir, $mapDir) = $self->promptNewExit(
                $roomObj,
                'Change exit direction',
                $exitObj,       # Only display widgets to change the nominal & map directions
                'change_dir',
            );

            if (! defined $dir) {

                return undef;
            }

            # Change the exit's direction(s)
            $result = $self->worldModelObj->changeExitDir(
                $self->session,
                TRUE,           # Update Automapper windows now
                $roomObj,
                $exitObj,
                $dir,
                $mapDir,
            );

            if (! $result) {

                $self->showMsgDialogue(
                    'Change exit direction',
                    'warning',
                    'The exit\'s direction could not be changed',
                    'ok',
                );

                return undef;

            } else {

                return 1;
            }
        }
    }

    sub setAssistedMoveCallback {

        # Called by $self->enableExitsColumn
        # Adds a key-value pair to an existing exit's ->assistedMoveHash, replacing an old pair if
        #   necessary
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the user
        #       clicks 'cancel' in any of the 'dialogue' windows
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($roomObj, $exitObj, $dir, $mapDir, $assistedProf, $assistedMove);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->setAssistedMoveCallback',
                @_,
            );
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->selectedExit
            || $self->selectedExit->drawMode eq 'temp_alloc'
        ) {
            return undef;
        }

        # Get the selected exit's parent room object
        $roomObj = $self->worldModelObj->ivShow('modelHash', $self->selectedExit->parent);

        # When a user selects an exit, they may be referring either to the exit stored in
        #   $self->selectedExit, its twin exit (if there is one) or its shadow exit (if there is
        #   one). Prompt the user to find out which
        $exitObj = $self->promptSpecifyExit('Set assisted moves for which exit?');
        if (! $exitObj) {

            # User clicked the 'cancel' button or closed the 'dialogue' window
            return undef;
        }

        # Prompt the user for new assisted move (both $dir and $mapDir will be set to 'undef')
        ($dir, $mapDir, $assistedProf, $assistedMove) = $self->promptNewExit(
            $roomObj,
            'Set assisted move',
            # Only display widgets to add an assisted move
            $exitObj,
            'set_assist',
        );

        if (! defined $assistedMove) {

            return undef;
        }

        # Update the exit
        $self->worldModelObj->addAssistedMove($exitObj, $assistedProf, $assistedMove);

        return 1;
    }

    sub allocateMapDirCallback {

        # Called by $self->enableExitsColumn
        # For an unallocated exit, allocates it a map (primary) direction
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the user
        #       clicks 'cancel' in the 'dialogue' window
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $exitObj, $roomObj, $firstComboItem, $number, $extraComboItem, $choice,
            @shortList, @longList, @dirList, @comboList, @extraList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->allocateMapDirCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->selectedExit
            || (
                $self->selectedExit->drawMode ne 'temp_alloc'
                && $self->selectedExit->drawMode ne 'temp_unalloc'
            )
        ) {
            return undef;
        }

        # In a few rare circumstances, $self->selectedExit seems to get reset before the world model
        #   can be updated. Store it in a local variable to prevent this
        $exitObj = $self->selectedExit;

        # Prepare a list of standard primary directions. Whether we include 'northnortheast', etc,
        #   depends on the current value of $self->worldModelObj->showAllPrimaryFlag
        @shortList = qw(north northeast east southeast south southwest west northwest up down);
        # (For convenience, put the longest directions at the end)
        @longList = qw(
            northnortheast eastnortheast eastsoutheast southsoutheast
            southsouthwest westsouthwest westnorthwest northnorthwest
        );

        if ($self->worldModelObj->showAllPrimaryFlag) {
            @dirList = (@shortList, @longList);
        } else {
            @dirList = @shortList;
        }

        # Get the blessed reference of the selected exit's parent room
        $roomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->parent);

        # Prepare a list of primary direction exits which are still available
        @comboList = $self->getAvailableDirs($roomObj, @dirList);
        # (The exit's current allocated direction, if it was in @dirList, will be the first item in
        #   the list)
        $firstComboItem = shift @comboList;
        # (Work out how many available exits were returned)
        $number = scalar @comboList;
        if (defined $firstComboItem) {

            $number++;
        }

        if ($number <= 2 && ! $self->worldModelObj->showAllPrimaryFlag) {

            # We didn't show 'northnortheast' the first time, but there are not many primary
            #   directions from which the user can choose; in fact, @comboList probably consists of
            #   just 'up' and 'down' (which is why we test @comboList <= 2)
            # Add 'northnortheast', so the user has more choices
            @extraList = $self->getAvailableDirs($roomObj, @longList);
            $extraComboItem = shift @extraList;
            push (@comboList, @extraList);
        }

        # The exit's current allocated direction, if available, should be the first item in the
        #   combobox
        if ($firstComboItem) {
            unshift(@comboList, $firstComboItem);
        } elsif ($extraComboItem) {
            unshift(@comboList, $extraComboItem);
        }

        # Don't prompt for a direction, if there are none available
        if (! @comboList) {

            return $self->showMsgDialogue(
                'Select map direction',
                'error',
                'Can\'t allocate a map direction - no primary directions are available',
                'ok',
            );
        }

        # Prompt the user for a primary direction
        $choice = $self->showComboDialogue(
            'Select map direction',
            'Choose a primary direction for the \'' . $exitObj->dir . '\' exit',
            FALSE,
            \@comboList,
        );

        if (! $choice) {

            return undef;

        } else {

            # Update the selected exit and instruct the world model to update its Automapper windows
            $self->worldModelObj->setExitMapDir(
                $self->session,
                TRUE,                   # Update Automapper windows now
                $roomObj,
                $exitObj,
                $choice,
            );

            return 1;
        }
    }

    sub confirmTwoWayCallback {

        # Called by $self->enableExitsColumn
        # For an unallocated exit, attempts to allocate it a map (primary) direction that's the
        #   opposite of an incoming uncertain or 1-way exit, and to connect them as twin exits
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the user
        #       clicks 'cancel' in the 'dialogue' window
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $exitObj, $roomObj, $matchExitObj, $choice,
            @shortList, @longList, @dirList, @incomingList, @comboList,
            %comboHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->confirmTwoWayCallback',
                @_,
            );
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->selectedExit
            || (
                $self->selectedExit->drawMode ne 'temp_alloc'
                && $self->selectedExit->drawMode ne 'temp_unalloc'
            )
        ) {
            return undef;
        }

        # In a few rare circumstances, $self->selectedExit seems to get reset before the world
        #   model can be updated. Store it in a local variable to prevent this
        $exitObj = $self->selectedExit;

        # Prepare a list of standard primary directions. Whether we include 'northnortheast', etc,
        #   depends on the current value of $self->worldModelObj->showAllPrimaryFlag
        @shortList = qw(north northeast east southeast south southwest west northwest up down);
        # (For convenience, put the longest directions at the end)
        @longList = qw(
            northnortheast eastnortheast eastsoutheast southsoutheast
            southsouthwest westsouthwest westnorthwest northnorthwest
        );

        if ($self->worldModelObj->showAllPrimaryFlag) {
            @dirList = (@shortList, @longList);
        } else {
            @dirList = @shortList;
        }

        # Get the blessed reference of the selected exit's parent room
        $roomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->parent);

        # Check the room's list of incoming exits, looking for one which lists $exitObj as its
        #   potential opposite exit
        OUTER: foreach my $otherExit ($roomObj->ivKeys('uncertainExitHash')) {

            if ($roomObj->ivShow('uncertainExitHash', $otherExit) == $exitObj->number) {

                $matchExitObj = $self->worldModelObj->ivShow('exitModelHash', $otherExit);
                last OUTER;
            }
        }

        if (! $matchExitObj) {

            # Otherwise, we can compile a list of all the room's incoming and 1-way exits, and ask
            #   the user to select one of them
            OUTER: foreach my $otherExit ($roomObj->ivKeys('uncertainExitHash')) {

                push (@incomingList, $self->worldModelObj->ivShow('exitModelHash', $otherExit));
            }

            OUTER: foreach my $otherExit ($roomObj->ivKeys('oneWayExitHash')) {


                push (@incomingList, $self->worldModelObj->ivShow('exitModelHash', $otherExit));
            }

            if (! @incomingList) {

                return $self->showMsgDialogue(
                    'Confirm two-way exit',
                    'error',
                    'There are no incoming uncertain or one-way exits which could be connected'
                    . ' to the selected exit',
                   'ok',
                );
            }

            # Prompt the user to select an exit (or to cancel the operation)
            foreach my $incomingExitObj (@incomingList) {

                my $string
                    = 'Exit #' . $incomingExitObj->number . ' (' . $incomingExitObj->dir . ')';

                # Use a hash, so we can match the user's selected combo item against an exit number
                push (@comboList, $string);
                $comboHash{$string} = $incomingExitObj;
            }

            $choice = $self->showComboDialogue(
                'Confirm two-way exit',
                "Select which incoming exit should be\nconnected to the selected exit",
                FALSE,
                \@comboList,
            );

            if (! $choice) {

                return undef;

            } else {

                $matchExitObj = $comboHash{$choice};
            }
        }

        if ($matchExitObj) {

            # Allocate the selected exit's ->mapDir permanently
            # Update the selected exit and instruct the world model to update its Automapper windows
            $self->worldModelObj->setExitMapDir(
                $self->session,
                FALSE,                   # Don't update Automapper windows now
                $roomObj,
                $exitObj,
                $exitObj->mapDir,
            );

            # Connect the two exits together
            $self->worldModelObj->connectRooms(
                $self->session,
                TRUE,                   # Update Automapper windows now
                $roomObj,
                $self->worldModelObj->ivShow('modelHash', $matchExitObj->parent),
                $exitObj->mapDir,
                $exitObj->mapDir,
                $exitObj,
                $matchExitObj,
            );

            return 1;

        } else {

            return undef;
        }
    }

    sub allocateShadowCallback {

        # Called by $self->enableExitsColumn
        # For an unallocated exit, allocates it a shadow exit (which is drawn instead of unallocated
        #   exit)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if there are no
        #       exits which can be added as the shadow exit or if the user clicks 'cancel' in the
        #       'dialogue' window
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $selectedExitObj, $roomObj, $choice, $shadowExitObj,
            @comboList,
            %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->allocateShadowCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->selectedExit
            || (
                $self->selectedExit->drawMode ne 'temp_alloc'
                && $self->selectedExit->drawMode ne 'temp_unalloc'
            )
        ) {
            return undef;
        }

        # The currently-selected exit will be unselected before this function finishes, so store a
        #   local copy of it
        $selectedExitObj = $self->selectedExit;

        # Get the parent room's blessed reference
        $roomObj = $self->worldModelObj->ivShow('modelHash', $selectedExitObj->parent);

        # Prepare a list of exits which use either (custom) primary directions, or have been
        #   allocated to a (standard primary) map direction
        OUTER: foreach my $exitNum ($roomObj->ivValues('exitNumHash')) {

            my ($exitObj, $string, $customDir);

            $exitObj = $self->worldModelObj->ivShow('exitModelHash', $exitNum);

            # Don't include the currently selected exit, or unallocated exits
            if (
                ($selectedExitObj && $selectedExitObj eq $exitObj)
                || $exitObj->drawMode eq 'temp_alloc'
                || $exitObj->drawMode eq 'temp_unalloc'
            ) {
                next OUTER;
            }

            # Add a string to the combo
            $string = $exitObj->dir . ' #' . $exitObj->number;

            if ($exitObj->mapDir) {

                # Get the equivalent (custom) primary direction
                $customDir = $self->session->currentDict->ivShow(
                    'primaryDirHash',
                    $exitObj->mapDir,
                );

                if ($customDir ne $exitObj->dir) {

                    $string .= ' <' . $customDir . '>';
                }
            }

            # Add an entry to the hash...
            $exitHash{$string} = $exitObj;
            # ...and another in the combo list
            push (@comboList, $string);
        }

        # Don't prompt for an exit, if there are none available
        if (! @comboList) {

            return $self->showMsgDialogue(
                'Select shadow',
                'error',
                'Can\'t allocate a shadow exit - no primary directions are available',
                'ok',
            );
        }

        # Prompt the user for a shadow exit
        $choice = $self->showComboDialogue(
            'Select shadow',
            'Choose a shadow exit for \'' . $selectedExitObj->dir . '\'',
            FALSE,
            \@comboList,
        );

        if (! $choice) {

            return undef;

        } else {

            $shadowExitObj = $exitHash{$choice};

            # Update the exit, and instruct the world model to update its Automapper windows
            $self->worldModelObj->setExitShadow(
                TRUE,                   # Update Automapper windows now
                $roomObj,
                $selectedExitObj,
                $shadowExitObj,
            );

            # Remove this exit's canvas object from the map, first unselecting it
            $self->setSelectedObj();
            $self->deleteCanvasObj('exit', $selectedExitObj);

            # The new selected exit is the shadow (to make it visually clear, what has happened)
            $self->setSelectedObj(
                [$shadowExitObj, 'exit'],
                FALSE,                  # Select this object; unselect all other objects
            );

            return 1;
        }
    }

    sub connectToClickCallback {

        # Called by $self->enableExitsColumn
        # When the user wants to connect the selected exit, first check whether the exit has a twin
        #   exit. If so, prompt the user to ask which of the two should be connected;
        #   $self->selectedExit is then set to the exit specified by the user
        # In both cases, set $self->freeClickMode to 'connect_exit'. $self->canvasObjEventHandler
        #   handles the connection operation
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the user
        #       declines to specify an exit
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $comboListRef, $exitHashRef, $choice,
            @comboList,
            %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->connectToClickCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->selectedExit
            || $self->selectedExit->drawMode eq 'temp_alloc'
        ) {
            return undef;
        }

        # If the exit has a twin exit and/or a shadow exit, we need to prompt the user to ask which
        #   of them should be edited
        if ($self->selectedExit->twinExit || $self->selectedExit->shadowExit) {

            ($comboListRef, $exitHashRef) = $self->compileExitList();
            if (! defined $comboListRef) {

                return undef;
            }

            @comboList = @$comboListRef;
            %exitHash = %$exitHashRef;

            # Prompt the user to choose which exit to edit
            $choice = $self->showComboDialogue(
                'Select exit',
                'Select which exit to connect:',
                FALSE,
                \@comboList,
            );

            if (! $choice) {

                return undef;

            } else {

                # Change the selected exit to the one specified by the user
                $self->setSelectedObj(
                    [$exitHash{$choice}, 'exit'],
                    FALSE,      # Select this object; unselect all other objects
                );
            }
        }

        # Set ->freeClickMode; $self->canvasObjEventHandler will connect the exit to the room
        #   clicked on by the user
        $self->ivPoke('freeClickMode', 'connect_exit');

        return 1;
    }

    sub disconnectExitCallback {

        # Called by $self->enableExitsColumn
        # Disconnects the selected exit. If the exit has a twin exit, that is disconnected, too
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $exitObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->disconnectExitCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedExit) {

            return undef;
        }

        # Import the selected exit (for convenience)
        $exitObj = $self->selectedExit;

        if ($exitObj->destRoom) {

            if ($exitObj->twinExit) {

                # Two-way exit
                $self->worldModelObj->abandonTwinExit(
                    TRUE,           # Update Automapper windows now
                    $exitObj,
                );

            } elsif ($exitObj->retraceFlag) {

                # Retracing exit
                $self->worldModelObj->restoreRetracingExit(
                    TRUE,           # Update Automapper windows now
                    $exitObj,
                );

            } elsif ($exitObj->oneWayFlag) {

                # One-way exit
                $self->worldModelObj->abandonOneWayExit(
                    TRUE,           # Update Automapper windows now
                    $exitObj,
                );

            } elsif ($exitObj->randomType ne 'none') {

                # Random exit
                $self->worldModelObj->restoreRandomExit(
                    TRUE,           # Update Automapper windows now
                    $exitObj,
                );

            } else {

                # Uncertain exit
                $self->worldModelObj->abandonUncertainExit(
                    TRUE,           # Update Automapper windows now
                    $exitObj,
                );
            }

        } elsif ($exitObj->randomType ne 'none') {

            # Random exit
            $self->worldModelObj->restoreRandomExit(
                TRUE,           # Update Automapper windows now
                $exitObj,
            );

        } else {

            # Not a connected exit
            $self->showMsgDialogue(
                'Disconnect exit',
                'error',
                'The selected exit (#' . $exitObj->number . ') is not connected to a room',
                'ok',
            );
        }

        return 1;
    }

    sub exitOrnamentCallback {

        # Called by $self->enableExitsColumn
        # Adds an ornament to (or removes the existing ornament from) the selected exit or exits
        # When there's a single selected exit, prompts the user whether the ornament should be
        #   added to an exit or to its twin (as appropriate)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $iv - Which of the exit object's IV is used for this ornament, e.g. 'pickFlag'. If set
        #           to 'undef', no ornament is used
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if there's an error
        #       prompting the user to choose an exit or if the user cancels that prompt
        #   1 otherwise

        my ($self, $iv, $check) = @_;

        # Local variables
        my (
            $comboListRef, $exitHashRef, $choice, $bothString,
            @exitList, @comboList,
            %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->exitOrnamentCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || (! $self->selectedExit && ! $self->selectedExitHash)) {

            return undef;
        }

        # Compile a list of selected exits
        @exitList = $self->compileSelectedExits();

        # Allow the user to specify both exits, when prompted
        $bothString = 'Use both exits';

        # If there's only one exit in @exitList - i.e. one exit is selected - we need to prompt
        #   the user to ask whether the exit or its twin (if is has one) should have their ornaments
        #   set/reset (but don't bother if the flag to automatically set/reset ornaments for both is
        #   set to TRUE)
        if ($self->selectedExit && $self->selectedExit->twinExit) {

            if (! $self->worldModelObj->setTwinOrnamentFlag) {

                ($comboListRef, $exitHashRef) = $self->compileExitList();
                if (! defined $comboListRef) {

                    return undef;
                }

                @comboList = ($bothString, @$comboListRef);
                %exitHash = %$exitHashRef;

                # Prompt the user to choose which exit to use
                $choice = $self->showComboDialogue(
                    'Set ornaments',
                    'Select which exit should have its ornaments set:',
                    FALSE,
                    \@comboList,
                );

                if (! $choice) {

                    # Don't set an ornament
                    return undef;

                } elsif ($choice eq $bothString) {

                    # Add the twin exit to @exitList, so that both will have their ornaments set or
                    #   reset
                    push (
                        @exitList,
                        $self->worldModelObj->ivShow(
                            'exitModelHash',
                            $self->selectedExit->twinExit,
                        ),
                    );

                } else {

                    # Replace the only exit in @exitList with the exit selected by the user
                    @exitList = ($exitHash{$choice});

                    # Also make this the selected exit
                    $self->setSelectedObj(
                        [$exitHash{$choice}, 'exit'],
                        FALSE,      # Select this object; unselect all other objects
                    );
                }

            } else {

                # Modify both the exit and its twin
                push (
                    @exitList,
                    $self->worldModelObj->ivShow('exitModelHash', $self->selectedExit->twinExit),
                );
            }
        }

        # Update the exits and redraw their parent rooms
        $self->worldModelObj->setMultipleOrnaments(
            TRUE,       # Update Automapper windows now
            $iv,
            @exitList,
        );

        return 1;
    }

    sub hiddenExitCallback {

        # Called by $self->enableExitsColumn
        # Hides or unhides the currently selected exit. If the selected exit could be confused with
        #   others occupying (roughly) the same space, opens a 'dialogue' window so the user can
        #   choose one
        #
        # Expected arguments
        #   $hiddenFlag - If set to TRUE, the selected exit is marked as hidden. If set to FALSE,
        #                   the selected exit is marked as not hidden
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $hiddenFlag, $check) = @_;

        # Local variables
        my (
            $bothString, $allString, $stringListRef, $exitHashRef, $text, $choice, $msg,
            @stringList, @comboList, @finalList,
            %exitHash,
        );

        # Check for improper arguments
        if (! defined $hiddenFlag || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->hiddenExitCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedExit) {

            return undef;
        }

        # Possible initial combo items
        if ($hiddenFlag) {

            $bothString = '<hide both>';
            $allString = '<hide all>';

        } else {

            $bothString = '<unhide both>';
            $allString = '<unhide all>';
        }

        # If the exit has a twin exit and/or a shadow exit, we need to prompt the user to ask which
        #   of them should be hidden/unhidden
        if ($self->selectedExit->twinExit || $self->selectedExit->shadowExit) {

            ($stringListRef, $exitHashRef) = $self->compileExitList();
            if (! defined $stringListRef) {

                return undef;
            }

            @stringList = @$stringListRef;
            %exitHash = %$exitHashRef;

            # Compile the combo list
            if (@stringList == 2) {
                @comboList = ($bothString, @stringList);
            } elsif (@stringList > 2) {
                @comboList = ($allString, @stringList);
            } else {
                @comboList = @stringList;
            }

            # Prompt the user to choose which exit to hide
            if ($hiddenFlag) {
                $text = 'Select which exit to hide:';
            } else {
                $text = 'Select which exit to unhide:';
            }

            $choice = $self->showComboDialogue(
                'Select exit',
                $text,
                FALSE,
                \@comboList,
            );

            if (! $choice) {

                return undef;

            } elsif ($choice eq $bothString || $choice eq $allString) {

                @finalList = values %exitHash;

            } else {

                push (@finalList, $exitHash{$choice});

                # Change the selected exit to the one specified by the user
                $self->setSelectedObj(
                    [$exitHash{$choice}, 'exit'],
                    FALSE,      # Select this object; unselect all other objects
                );
            }

        } else {

            # The selected exit has no twin/shadow, so simply hide/undhide it
            push (@finalList, $self->selectedExit);
        }

        # Hide/unhide the exit object(s). There aren't going to be many exits in @finalList, so it's
        #   not so inefficient to make a separate call to the world model for every exit
        foreach my $exitObj (@finalList) {

            $self->worldModelObj->setHiddenExit(
                TRUE,           # Update Automapper windows now
                $exitObj,
                $hiddenFlag,
            );
        }

        # Display a confirmation, since many exits aren't visible
        if (@finalList) {

            if (scalar @finalList == 1) {

                $msg = '1 exit (#' . $finalList[0]->number . ')';

            } else {

                $msg = scalar @finalList . ' exits';
            }

            if ($hiddenFlag) {
                $msg .= ' marked \'hidden\'';
            } else {
                $msg .= ' marked \'not hidden\'';
            }

            $self->showMsgDialogue(
                'Mark hidden exit',
                'info',
                $msg,
                'ok',
            );
        }

        return 1;
    }

    sub markBrokenExitCallback {

        # Called by $self->enableExitsColumn
        # Marks the selected exit and its twin exit (if any) as broken exits (unless they are
        #   already broken exits or region exits, or if the selected exit doesn't have a destination
        #   room)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the exit(s)
        #       can't be marked as broken
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($text, $twinExitObj);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->markBrokenExitCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedExit) {

            return undef;
        }

        # Basic checks
        if ($self->selectedExit->brokenFlag) {
            $text = 'The selected exit is already a broken exit';
        } elsif ($self->selectedExit->regionFlag) {
            $text = 'Region exits can\'t be marked as broken exits';
        } elsif ($self->selectedExit->randomType ne 'none') {
            $text = 'Random exits can\'t be marked as broken exits';
        } elsif (! $self->selectedExit->destRoom) {
            $text = 'The selected exit doesn\'t have a destination room';
        }

        if ($text) {

            $self->showMsgDialogue(
                'Mark exit as broken',
                'error',
                $text,
                'ok',
            );

            return undef;
        }

        # Mark the exit as broken and instruct the world model to update its Automapper windows
        $self->worldModelObj->setBrokenExit(
            TRUE,                   # Update Automapper windows now
            $self->selectedExit,
        );

        # If the selected exit has a twin exit, that must also be marked as broken
        if ($self->selectedExit->twinExit) {

            $twinExitObj
                = $self->worldModelObj->ivShow('exitModelHash', $self->selectedExit->twinExit);

            $self->worldModelObj->setBrokenExit(
                TRUE,                   # Update Automapper windows now
                $twinExitObj,
            );
        }

        return 1;
    }

    sub restoreBrokenExitCallback {

        # Called by $self->enableExitsColumn
        # Checks whether the selected broken exit (and its twin exit, if there is one) can now
        #   be marked as unbroken. If so, redraws them
        # This might be used in conjunction with the 'mark exit as broken' operation
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the selected
        #       broken exit can't be restored
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($text, $twinExitObj);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->restoreBrokenExitCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedExit) {

            return undef;
        }

        # Basic checks
        if (! $self->selectedExit->brokenFlag) {
            $text = 'The selected exit is not marked as broken';
        } elsif (! $self->worldModelObj->checkRoomAlignment($self->session, $self->selectedExit)) {
            $text = 'The selected broken exit can\'t be restored';
        }

        if ($text) {

            $self->showMsgDialogue(
                'Restore broken exit',
                'error',
                $text,
                'ok',
            );

            return undef;
        }

        # Mark the exit as not broken
        $self->worldModelObj->restoreBrokenExit(
            $self->session,
            TRUE,               # Update Automapper windows now
            $self->selectedExit,
            TRUE,               # We've already called ->checkRoomAlignment
        );

        # If the exit has a twin exit, that must also be marked as not broken
        if ($self->selectedExit->twinExit) {

            $twinExitObj
                = $self->worldModelObj->ivShow('exitModelHash', $self->selectedExit->twinExit);

            $self->worldModelObj->restoreBrokenExit(
                $self->session,
                TRUE,           # Update Automapper windows now
                $twinExitObj,
                TRUE,           # We've already called ->checkRoomAlignment
            );
        }

        return 1;
    }

    sub markOneWayExitCallback {

        # Called by $self->enableExitsColumn
        # Marks the selected exit as a one-way exit (assuming it's currently an uncertain or
        #   two-way exit)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the exit(s)
        #       can't be marked as one-way
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $stringListRef, $exitHashRef, $choice, $exitObj, $msg,
            @stringList,
            %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->markOneWayExitCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedExit) {

            return undef;
        }

        # If the exit has a twin exit and/or a shadow exit, we need to prompt the user to ask which
        #   of them should be marked as one-way
        if ($self->selectedExit->twinExit || $self->selectedExit->shadowExit) {

            ($stringListRef, $exitHashRef) = $self->compileExitList();
            if (! defined $stringListRef) {

                return undef;
            }

            @stringList = @$stringListRef;
            %exitHash = %$exitHashRef;

            # Prompt the user to choose which exit to convert
            $choice = $self->showComboDialogue(
                'Select exit',
                'Select which exit to make one-way',
                FALSE,
                \@stringList,
            );

            if (! $choice) {

                return undef;

            } else {

                $exitObj = $exitHash{$choice};

                # Also make this the selected exit
                $self->setSelectedObj(
                    [$exitHash{$choice}, 'exit'],
                    FALSE,      # Select this object; unselect all other objects
                );
            }

        } else {

            # The selected exit has no twin/shadow
            $exitObj = $self->selectedExit;
        }

        # Basic checks
        if ($exitObj->oneWayFlag) {

            $msg = 'The selected exit is already a one-way exit';

        } elsif ($exitObj->drawMode eq 'temp_alloc' || $exitObj->drawMode eq 'temp_unalloc') {

            $msg = 'Unallocated exits can\'t be marked as one-way exits';

        } elsif (! $exitObj->destRoom && $exitObj->randomType eq 'none') {

            $msg = 'Incomplete exits (which have no destination room) can\'t be marked as one-way'
                    . ' exits';
        }

        if ($msg) {

            $self->showMsgDialogue(
                'Mark exit as one-way',
                'error',
                $msg,
                'ok',
            );

            return undef;
        }

        # Mark this exit as a one-way exit and instruct the world model to update its Automapper
        #   windows
        $self->worldModelObj->markOneWayExit(
            TRUE,       # Update Automapper windows now
            $exitObj,
        );

        # Show a confirmation dialogue
        $self->showMsgDialogue(
            'Mark exit as one-way',
            'info',
            'Exit #' . $exitObj->number . ' converted to a one-way exit',
            'ok',
        );

        return 1;
    }

    sub restoreOneWayExitCallback {

        # Called by $self->enableExitsColumn
        # Checks whether the selected one-way exit can be marked as uncertain or two-way. If so,
        #   redraws them
        # This might be used in conjunction with the 'mark exit as one-way' operation
        #
        # Expected arguments
        #   $twoWayFlag     - Set to TRUE if the one-way exit should be converted into a two-way
        #                       exit (if possible); set to FALSE if it should be converted into an
        #                       uncertain exit (if possible)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the selected
        #       exit isn't one-way or if there isn't an  exit in the opposite direction, potentially
        #       leading back from the destination room, which we need to form a two-way or uncertain
        #       exit
        #   1 otherwise

        my ($self, $twoWayFlag, $check) = @_;

        # Local variables
        my ($msg, $oppExitObj, $title, $roomObj, $oppRoomObj);

        # Check for improper arguments
        if (! defined $twoWayFlag || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->restoreOneWayExitCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedExit) {

            return undef;
        }

        # Basic checks
        if (! $self->selectedExit->oneWayFlag) {

            $msg = 'The selected exit is not marked as one-way';

        } else {

            # See if the destination room has an exit in the opposite direction to the selected
            #   one-way exit
            $oppExitObj = $self->worldModelObj->checkOppPrimary($self->selectedExit);
            if (! $oppExitObj) {

                $msg = ' because there is no exit in the opposite direction';

            } elsif (
                $oppExitObj->drawMode eq 'temp_alloc'
                || $oppExitObj->drawMode eq 'temp_unalloc'
            ) {
                $msg = ' because the exit, apparently in the opposite direction, hasn\'t yet'
                            . ' been allocated that direction permanently',

            } elsif ($oppExitObj->destRoom) {

                $msg = ' because the opposite exit already has a destination room';
            }

            if ($msg) {

                if ($twoWayFlag) {
                    $msg = 'The selected exit can\'t be marked as two-way' . $msg;
                } else {
                    $msg = 'The selected exit can\'t be marked as uncertain' . $msg;
                }
            }
        }

        if ($msg) {

            if ($twoWayFlag) {
                $title = 'Restore two-way exit';
            } else {
                $title = 'Restore uncertain exit';
            }

            $self->showMsgDialogue(
                $title,
                'error',
                $msg,
                'ok',
            );

            return undef;
        }

        # Get the parent rooms
        $roomObj = $self->worldModelObj->ivShow('modelHash', $self->selectedExit->parent);
        $oppRoomObj = $self->worldModelObj->ivShow('modelHash', $oppExitObj->parent);

        if ($twoWayFlag) {

            # Connect the opposite exit to the selected exit, thereby establishing a two-way exit
            $self->worldModelObj->connectRooms(
                $self->session,
                TRUE,           # Update Automapper windows now
                $oppRoomObj,
                $roomObj,
                $oppExitObj->dir,
                $oppExitObj->mapDir,
                $oppExitObj,
            );

        } else {

            # Convert the one-way exit into an uncertain exit
            $self->worldModelObj->convertOneWayExit(
                TRUE,           # Update Automapper windows now
                $self->selectedExit,
                $oppRoomObj,
                $oppExitObj,
            );
        }

        # Display a confirmation
        $msg = 'Exit #' . $self->selectedExit->number . ' restored to';

        if ($twoWayFlag) {

            $msg .= ' a two-way exit';
            $title = 'Restore two-way exit';

        } else {

            $msg .= ' an uncertain exit';
            $title = 'Restore uncertain exit';
        }

        $self->showMsgDialogue(
            $title,
            'info',
            $msg,
            'ok',
        );

        return 1;
    }

    sub markRetracingExitCallback {

        # Called by $self->enableExitsColumn
        # Marks the selected exit as a retracing exit (a special kind of one-way exit which leads
        #   back to the same room)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the exit(s)
        #       can't be marked as retracing
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $comboListRef, $exitHashRef, $choice, $exitObj, $roomObj, $result,
            @comboList,
            %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->markRetracingExitCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedExit) {

            return undef;
        }

        # If the selected exit has a twin exit and/or a shadow exit, we need to prompt the user to
        #   ask which of them should be modified
        if ($self->selectedExit->twinExit || $self->selectedExit->shadowExit) {

            ($comboListRef, $exitHashRef) = $self->compileExitList();
            if (! defined $comboListRef) {

                return undef;
            }

            @comboList = @$comboListRef;
            %exitHash = %$exitHashRef;

            # Prompt the user to choose which exit to edit
            $choice = $self->showComboDialogue(
                'Select exit',
                'Select which exit to modify:',
                FALSE,
                \@comboList,
            );

            if (! $choice) {

                return undef;

            } else {

                $exitObj = $exitHash{$choice};

                # Also make this the selected exit
                $self->setSelectedObj(
                    [$exitHash{$choice}, 'exit'],
                    FALSE,      # Select this object; unselect all other objects
                );
            }

        } else {

            $exitObj = $self->selectedExit;
        }

        # Basic checks
        if ($exitObj->retraceFlag) {

            $self->showMsgDialogue(
                'Mark exit as retracing',
                'error',
                'The selected exit is already a retracing exit',
                'ok',
            );

            return undef;
        }

        # Connect the selected exit to its own parent room
        $roomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->parent);

        $result = $self->worldModelObj->connectRooms(
            $self->session,
            TRUE,       # Update Automapper windows now
            $roomObj,
            $roomObj,
            $exitObj->dir,
            $exitObj->mapDir,
            $exitObj,
        );

        # Display a confirmation
        if (! $result) {

            $self->showMsgDialogue(
                'Mark exit as retracing',
                'error',
                'Could not convert exit #' . $exitObj->number . ' to a retracing exit',
                'ok',
            );

            return undef;

        } else {

            return 1;
        }
    }

    sub restoreRetracingExitCallback {

        # Called by $self->enableExitsColumn
        # Converts the selected retracing exit into an incomplete exit
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the selected
        #       exit isn't a retracing exit
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->restoreRetracingExitCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedExit) {

            return undef;
        }

        # Basic checks
        if (! $self->selectedExit->retraceFlag) {

            $self->showMsgDialogue(
                'Restore incomplete exit',
                'error',
                'The selected exit is not marked as a retracing exit',
                'ok',
            );

            return undef;

        } else {

            # Convert the exit to an incomplete exit
            $self->worldModelObj->restoreRetracingExit(
                TRUE,                   # Update Automapper windows now
                $self->selectedExit,
            );

            return 1;
        }
    }

    sub markRandomExitCallback {

        # Called by $self->enableExitsColumn
        # Marks the selected exit as a random exit (assuming it's currently an incomplete exit)
        #
        # Expected arguments
        #   $exitType   - Set to 'same_region' if the exit leads to a random location in the current
        #                   region, 'any_region' if the exit leads to a random location in any
        #                   region or 'room_list' if the exit leads to a random location in its
        #                   ->randomDestList
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the user
        #       declines to specify which of two possible exits to modify or if the specified exit
        #       is already a random exit
        #   1 otherwise

        my ($self, $exitType, $check) = @_;

        # Local variables
        my (
            $comboListRef, $exitHashRef, $choice, $exitObj,
            @comboList,
            %exitHash,
        );

        # Check for improper arguments
        if (
            ! defined $exitType
            || ($exitType ne 'same_region' && $exitType ne 'any_region' && $exitType ne 'room_list')
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->markRandomExitCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedExit) {

            return undef;
        }

        # If the selected exit has a twin exit and/or a shadow exit, we need to prompt the user to
        #   ask which of them should be modified
        if ($self->selectedExit->twinExit || $self->selectedExit->shadowExit) {

            ($comboListRef, $exitHashRef) = $self->compileExitList();
            if (! defined $comboListRef) {

                return undef;
            }

            @comboList = @$comboListRef;
            %exitHash = %$exitHashRef;

            # Prompt the user to choose which exit to edit
            $choice = $self->showComboDialogue(
                'Select exit',
                'Select which exit to modify:',
                FALSE,
                \@comboList,
            );

            if (! $choice) {

                return undef;

            } else {

                $exitObj = $exitHash{$choice};

                # Also make this the selected exit
                $self->setSelectedObj(
                    [$exitHash{$choice}, 'exit'],
                    FALSE,      # Select this object; unselect all other objects
                );
            }

        } else {

            $exitObj = $self->selectedExit;
        }

        # Basic checks
        if ($exitObj->randomType ne 'none' && $exitObj->randomType eq $exitType) {

            $self->showMsgDialogue(
                'Mark exit as random',
                'warning',
                'The selected exit is already marked as random (type \'' . $exitType . '\')',
                'ok',
            );

            return undef;

        } else {

            # Mark the exit as random, and instruct the world model to updates its Automapper
            #   windows
            $self->worldModelObj->setRandomExit(
                TRUE,       # Update Automapper windows now
                $exitObj,
                $exitType,
            );

            return 1;
        }
    }

    sub restoreRandomExitCallback {

        # Called by $self->enableExitsColumn
        # Converts the selected random exit into an incomplete exit
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the selected
        #       exit isn't a random exit
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->restoreRandomExitCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedExit) {

            return undef;
        }

        # Basic checks
        if ($self->selectedExit->randomType eq 'none') {

            $self->showMsgDialogue(
                'Restore random exit',
                'error',
                'The selected exit is not marked as a random exit',
                'ok',
            );

            return undef;

        } else {

            $self->worldModelObj->restoreRandomExit(
                TRUE,       # Update Automapper windows now
                $self->selectedExit,
            );

            return 1;
        }
    }

    sub markSuperExitCallback {

        # Called by $self->enableExitsColumn
        # Marks the selected exit as a super-region exit (assuming it's currently an ordinary
        #   region exit)
        #
        # Expected arguments
        #   $exclusiveFlag  - Set to TRUE if this should be the only super-region exit leading from
        #                       its parent region to its destination region. Set to FALSE if other
        #                       super-region exits between the two regions (if any) can be left as
        #                       super-region exits
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the user
        #       declines to specify which of two possible exits to modify or if the specified exit
        #       isn't a region exit
        #   1 otherwise

        my ($self, $exclusiveFlag, $check) = @_;

        # Local variables
        my (
            $bothString, $allString, $stringListRef, $exitHashRef, $choice, $confirmFlag, $msg,
            $title,
            @stringList, @comboList, @finalList,
            %exitHash,
        );

        # Check for improper arguments
        if (! defined $exclusiveFlag || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->markSuperExitCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->selectedExit
            || ! $self->selectedExit->regionFlag
        ) {
            return undef;
        }

        # Possible initial combo items
        $bothString = '<modify both>';
        $allString = '<modify all>';

        # Compile a list of exits which could be confused with the currently selected one
        ($stringListRef, $exitHashRef) = $self->compileExitList();
        if (! defined $stringListRef) {

            return undef;
        }

        @stringList = @$stringListRef;
        %exitHash = %$exitHashRef;

        # If there is more than one exit in the list, prompt the user to specify which one to modify
        if (scalar @stringList > 1) {

            # Compile the combo list
            if (@stringList == 2) {
                @comboList = ($bothString, @stringList);
            } elsif (@stringList > 2) {
                @comboList = ($allString, @stringList);
            } else {
                @comboList = @stringList;
            }

            # Prompt the user to choose which exit to modify
            $choice = $self->showComboDialogue(
                'Select exit',
                'Select which exit to modify:',
                FALSE,
                \@comboList,
            );

            if (! $choice) {

                return undef;

            } elsif ($choice eq $bothString || $choice eq $allString) {

                @finalList = values %exitHash;

            } else {

                push (@finalList, $exitHash{$choice});
            }

        } else {

            # There's only one exit on which to operate
            push (@finalList, $self->selectedExit);
        }

        foreach my $exitObj (@finalList) {

            # Mark the exit as a super-region exit and instruct the world model to update its
            #   Automapper windows
            $self->worldModelObj->setSuperRegionExit(
                $self->session,
                TRUE,       # Update Automapper windows now
                $exitObj,
                $exclusiveFlag,
            );
        }

        # Show a confirmation
        if ($exclusiveFlag) {

            $title = 'Mark exclusive super-region exit';

            if (@finalList > 1) {
                $msg = 'The selected region exits are now exclusive super-region exits';
            } else {
                $msg = 'The selected region exit is now an exclusive super-region exit';
            }

        } else {

            $title = 'Mark super-region exit';

            if (@finalList > 1) {
                $msg = 'The selected region exits are now super-region exits';
            } else {
                $msg = 'The selected region exit is now a super-region exit';
            }
        }

        $self->showMsgDialogue(
            $title,
            'info',
            $msg,
            'ok',
        );

        return 1;
    }

    sub restoreSuperExitCallback {

        # Called by $self->enableExitsColumn
        # Converts the selected super-region exit into a normal region exit
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the selected
        #       exit isn't a super-region exit
        #   1 otherwise

        my ($self, $check) = @_;

        my (
            $bothString, $allString, $stringListRef, $exitHashRef, $choice, $msg,
            @stringList, @comboList, @finalList,
            %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->restoreSuperExitCallback',
                @_,
            );
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->selectedExit
            || ! $self->selectedExit->regionFlag
        ) {
            return undef;
        }

        # Possible initial combo items
        $bothString = '<modify both>';
        $allString = '<modify all>';

        # Compile a list of exits which could be confused with the currently selected one
        ($stringListRef, $exitHashRef) = $self->compileExitList();
        if (! defined $stringListRef) {

            return undef;
        }

        @stringList = @$stringListRef;
        %exitHash = %$exitHashRef;

        # If there is more than one exit in the list, prompt the user to specify which one to modify
        if (scalar @stringList > 1) {

            # Compile the combo list
            if (@stringList == 2) {
                @comboList = ($bothString, @stringList);
            } elsif (@stringList > 2) {
                @comboList = ($allString, @stringList);
            } else {
                @comboList = @stringList;
            }

            # Prompt the user to choose which exit to modify
            $choice = $self->showComboDialogue(
                'Select exit',
                'Select which exit to modify:',
                FALSE,
                \@comboList,
            );

            if (! $choice) {

                return undef;

            } elsif ($choice eq $bothString || $choice eq $allString) {

                @finalList = values %exitHash;

            } else {

                push (@finalList, $exitHash{$choice});
            }

        } else {

            # There's only one exit on which to operate
            push (@finalList, $self->selectedExit);
        }

        foreach my $exitObj (@finalList) {

            # Convert the super-region exit to a normal region exit and instruct the world model to
            #   update its Automapper windows
            $self->worldModelObj->restoreSuperRegionExit(
                TRUE,       # Update Automapper windows now
                $exitObj,
            );
        }

        # Show a confirmation
        if (@finalList > 1) {
            $msg = 'The selected region exits are now normal region exits';
        } else {
            $msg = 'The selected region exit is now a normal region exit';
        }

        $self->showMsgDialogue(
            'Convert super-region exit',
            'info',
            $msg,
            'ok',
        );

        return 1;
    }

    sub setExitTwinCallback {

        # Called by $self->enableExitsColumn
        # Twins the selected exit with an exit in the destination room, which in turn leads back to
        #   the selected exit's room
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if there are no
        #       suitable exits in the selected exit's destination room, with which it can be
        #       twinned, if the user declines to select a twin exit, when prompted, or if the
        #       twinnng operation fails
        #   1 otherwise

        my ($self, $check) = @_;

        my (
            $roomObj, $destRoomObj, $choice, $twinExitObj,
            @otherExitList, @comboList,
            %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setExitTwinCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->selectedExit
            || (
                ! $self->selectedExit->oneWayFlag
                && ! (
                    $self->selectedExit->destRoom
                    && ! $self->selectedExit->twinExit
                    && ! $self->selectedExit->retraceFlag
                    && $self->selectedExit->randomType eq 'none'
                )
            )
        ) {
            return undef;
        }

        # Get the exit's parent room and its destination room
        $roomObj = $self->worldModelObj->ivShow('modelHash', $self->selectedExit->parent);
        $destRoomObj = $self->worldModelObj->ivShow('modelHash', $self->selectedExit->destRoom);
        # Get a list of exits in the destination room which lead back to the selected exit's room
        #   and which are uncertain or one-way exits
        foreach my $otherExitNum ($destRoomObj->ivValues('exitNumHash')) {

            my $otherExitObj = $self->worldModelObj->ivShow('exitModelHash', $otherExitNum);

            if (
                $otherExitObj->destRoom
                && $otherExitObj->destRoom == $roomObj->number
                && (
                    $otherExitObj->oneWayFlag
                    || (
                        ! $otherExitObj->twinExit
                        && ! $otherExitObj->retraceFlag
                        && $otherExitObj->randomType eq 'none'
                    )
                )
            ) {
                push (@otherExitList, $otherExitObj);
            }
        }

        if (! @otherExitList) {

            $self->showMsgDialogue(
                'Set twin exit',
                'error',
                'There are no exits in the selected exit\'s destination room which lead back to the'
                . ' exit\'s own parent room',
                'ok',
            );

            return undef;
        }

        # Prompt the user to confirm which exit should be twinned with the selected exit
        foreach my $exitObj (@otherExitList) {

            my $string = '#' . $exitObj->number . ' ' . $exitObj->dir;

            if ($exitObj->shadowExit) {

                $string .= ' (shadow of exit #' . $exitObj->shadowExit . ')';
            }

            push (@comboList, $string);
            $exitHash{$string} = $exitObj;
        }

        $choice = $self->showComboDialogue(
            'Set exit twin',
            "Select which uncertain/one-way exit to\ntwin with the selected exit \'"
            . $self->selectedExit->dir . '\'',
            FALSE,
            \@comboList,
        );

        if (! $choice) {

            return undef;

        } else {

            $twinExitObj = $exitHash{$choice};

            # Twin the two exits together
            if (
                ! $self->worldModelObj->convertToTwinExits(
                    TRUE,                   # Update Automapper windows
                    $self->selectedExit,
                    $twinExitObj,
                )
            ) {
                # Show confirmation
                $self->showMsgDialogue(
                    'Set twin exit',
                    'error',
                    'The twinning operation failed',
                    'ok',
                );

                return undef;

            } else {

                # No need to show a confirmation - the Automapper window has been updated
                return 1;
            }
        }
    }

    sub setIncomingDirCallback {

        # Called by $self->enableExitsColumn
        # Changes the direction in which the far end of a one-way exit (the end which touches the
        #   destination room) is drawn
        # By default, that direction is the opposite of the exit's ->mapDir, but the user can change
        #   that direction, if they want
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the user
        #       declines to select a new incoming direction, when prompted, or if the modification
        #       operation fails
        #   1 otherwise

        my ($self, $check) = @_;

        my (
            $oneWayDir, $choice,
            @shortList, @longList, @dirList, @otherList, @comboList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setIncomingDirCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->selectedExit
            || ! $self->selectedExit->oneWayFlag
        ) {
            return undef;
        }

       # Prepare a list of standard primary directions. Whether we include 'northnortheast', etc,
        #   depends on the current value of $self->worldModelObj->showAllPrimaryFlag
        @shortList = qw(north northeast east southeast south southwest west northwest up down);
        # (For convenience, put the longest directions at the end)
        @longList = qw(
            north northeast east southeast south southwest west northwest up down
            northnortheast eastnortheast eastsoutheast southsoutheast
            southsouthwest westsouthwest westnorthwest northnorthwest
        );

        if ($self->worldModelObj->showAllPrimaryFlag) {
            @dirList = @longList;
        } else {
            @dirList = @shortList;
        }

        # Move the exit's current ->oneWayDir to the top of the list
        $oneWayDir = $self->selectedExit->oneWayDir;
        foreach my $dir (@dirList) {

            if ($oneWayDir eq $dir) {
                push (@comboList, $dir);
            } else {
                push (@otherList, $dir);
            }
        }

        push (@comboList, @otherList);

        # Prompt the user for a new incoming direction
        $choice = $self->showComboDialogue(
            'Set incoming direction',
            "Select the direction in which this \'" . $self->selectedExit->dir
            . "\' exit is\ndrawn as it approaches its destination room",
            FALSE,
            \@comboList,
        );

        if (! $choice) {

            return undef;

        } else {

            # Ask the GA::Obj::WorldModel to change the exit's IV
            if (
                ! $self->worldModelObj->setExitIncomingDir(
                    TRUE,                   # Update Automapper windows
                    $self->selectedExit,
                    $choice,
                )
            ) {
                # Show confirmation
                $self->showMsgDialogue(
                    'Set incoming direction',
                    'error',
                    'The operation failed',
                    'ok',
                );

                return undef;

            } else {

                # No need to show a confirmation - the Automapper window has been updated
                return 1;
            }
        }
    }

    sub toggleExitTagCallback {

        # Called by $self->enableExitsColumn
        # Toggles the exit tag on the selected exit
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $exitObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->toggleExitTagCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || (
                (! $self->selectedExit || ! $self->selectedExit->regionFlag)
                && ! $self->selectedExitTag
            )
        ) {
            return undef;
        }

        # Get the exit to use
        if ($self->selectedExit) {
            $exitObj = $self->selectedExit;
        } else {
            $exitObj = $self->selectedExitTag;
        }

        # Toggle the exit tag
        if (! $exitObj->exitTag) {

            $self->worldModelObj->applyExitTag(
                TRUE,           # Update Automapper windows now
                $exitObj,
            );

        } else {

            $self->worldModelObj->cancelExitTag(
                TRUE,           # Update Automapper windows now
                $exitObj,
            );
        }

        # For a selected exit tag - which has now been removed - select the exit instead
        $self->setSelectedObj(
            [$exitObj, 'exit'],
            FALSE,          # Select this object; unselect all other objects
        );

        return 1;
    }

    sub viewExitDestination {

        # Called by $self->enableExitTagsPopupMenu (only)
        # For a region exit, selects the destination room and changes the currently displayed region
        #   (and level) to show it
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $roomObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->viewExitDestination', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedExitTag) {

            return undef;
        }

        # Get the exit's destination room
        $roomObj = $self->worldModelObj->ivShow('modelHash', $self->selectedExitTag->destRoom);

        # Select the destination room
        $self->setSelectedObj(
            [$roomObj, 'room'],
            FALSE,          # Select this object; unselect all other objects
        );

        # Centre the map over the selected room, changing the currently displayed region and level
        #   as necessary
        $self->centreMapOverRoom($roomObj);

        return 1;
    }

    sub editExitTagCallback {

        # Called by $self->enableLabelsColumn
        # Prompts the user to enter a new ->exitTag for the selected exit
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($exitObj, $text);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->editExitTagCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || (
                (! $self->selectedExit || ! $self->selectedExit->regionFlag)
                && ! $self->selectedExitTag
            )
        ) {
            return undef;
        }

        # Set the exit to use
        if ($self->selectedExit) {
            $exitObj = $self->selectedExit;
        } else {
            $exitObj = $self->selectedExitTag;
        }

        # Prompt the user for the new contents of the exit tag
        $text = $self->showEntryDialogue(
            'Edit exit tag',
            'Enter the new contents of the exit tag (leave empty to reset)',
            40,                 # Max chars
            $exitObj->exitTag,
        );

        if (defined $text) {

            # Change the exit tag's contents
            $self->worldModelObj->applyExitTag(
                TRUE,               # Update Automapper windows
                $exitObj,
                undef,              # Parent regionmap not known
                $text,              # Can be an empty string
                TRUE,               # Calling function is this string
            );
        }

        return 1;
    }

    sub resetExitOffsetsCallback {

        # Called by $self->enableExitsColumn
        # Resets the positions of exit tags by setting their offset IVs to zero
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            @exitList,
            %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->resetExitOffsetsCallback',
                @_,
            );
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || (
                ! $self->selectedExit && $self->selectedExitHash
                && ! $self->selectedExitTag && ! $self->selectedExitTagHash
            )
        ) {
            return undef;
        }

        # Compile a list of exits which are selected exits, or which have selected exit tags. Use a
        #   hash to eliminate duplicates
        if ($self->selectedExit) {

            $exitHash{$self->selectedExit->number} = $self->selectedExit;

        } elsif ($self->selectedExitHash) {

            %exitHash = $self->selectedExitHash;
        }

        if ($self->selectedExitTag) {

            $exitHash{$self->selectedExitTag->number} = $self->selectedExitTag;

        } elsif ($self->selectedExitTagHash) {

            foreach my $key ($self->ivKeys('selectedExitTagHash')) {

                $exitHash{$key} = $self->ivShow('selectedExitTagHash', $key);
            }
        }

        # For each exit which has an exit tag, reset its position
        @exitList = values %exitHash;
        foreach my $exitObj (@exitList) {

            if ($exitObj->exitTag) {

                $self->worldModelObj->resetExitTag(
                    TRUE,           # Update Automapper windows now
                    $exitObj,
                );
            }
        }

        return 1;
    }

    sub applyExitTagsCallback {

        # Called by $self->enableExitsColumn
        # Applies (or cancels) exit tags on all region exits in the current region
        #
        # Expected arguments
        #   $applyFlag      - If set to TRUE, exit tags are applied. If set to FALSE, exit tags are
        #                       cancelled
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $applyFlag, $check) = @_;

        # Local variables
        my (@exitNumList, @exitObjList, @drawList);

        # Check for improper arguments
        if (! defined $applyFlag || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->applyExitTagsCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Import a list of region exits
        @exitNumList = $self->currentRegionmap->ivKeys('regionExitHash');
        foreach my $exitNum (@exitNumList) {

            my $exitObj = $self->worldModelObj->ivShow('exitModelHash', $exitNum);
            push (@exitObjList, $exitObj);
            push (@drawList, 'exit', $exitObj);
        }

        # Apply or cancel exit tags for each exit in turn
        foreach my $exitObj (@exitObjList) {

            if ($applyFlag) {

                $self->worldModelObj->applyExitTag(
                    TRUE,           # Update Automapper windows now
                    $exitObj,
                );

            } else {

                $self->worldModelObj->cancelExitTag(
                    TRUE,           # Update Automapper windows now
                    $exitObj,
                );
            }
        }

        # Redraw the exits immediately
        $self->doDraw(@drawList);

        return 1;
    }

    sub identifyExitsCallback {

        # Called by $self->enableExitsColumn
        # Lists all the selected exits in a 'dialogue' window (if more than 10 are selected, we only
        #   list the first 10)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $limit, $msg,
            @sortedList, @reducedList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->identifyExitsCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || (! $self->selectedExit && ! $self->selectedExitHash)) {

            return undef;
        }

        # Compile a list of selected exits and sort by exit model number
        @sortedList = sort {$a->number <=> $b->number} ($self->compileSelectedExits());

        # Reduce the size of the list to a maximum of $limit
        $limit = 10;
        if (@sortedList > $limit) {
            @reducedList = @sortedList[0..($limit - 1)];
        } else {
            @reducedList = @sortedList;
        }

        # Prepare the message to show in the 'dialogue' window
        if (scalar @sortedList != scalar @reducedList) {

            $msg = "Selected exits (first " . $limit . " exits of " . scalar @sortedList . ")\n";

        } elsif (scalar @sortedList == 1) {

            $msg = "Selected exits (1 exit)\n";

        } else {

            $msg = "Selected exits (" . scalar @sortedList . " exits)\n";
        }

        foreach my $exitObj (@reducedList) {

            my $customDir;

            # Convert the exit's map direction, ->mapDir (a standard primary direction) into a
            #   custom primary direction, so that we can compare it with the exit's nominal
            #   direction
            $customDir = $self->session->currentDict->ivShow('primaryDirHash', $exitObj->mapDir);
            if (! $customDir || $customDir eq $exitObj->dir) {

                $msg .= "   #" . $exitObj->number . " '" . $exitObj->dir . "' (room #"
                            . $exitObj->parent . ")\n";

            } else {

                $msg .= "   #" . $exitObj->number . " '" . $exitObj->dir . "' [" . $exitObj->mapDir
                            . "] (room #" . $exitObj->parent . ")\n";
            }
        }

        # Display a popup to show the results
        $self->showMsgDialogue(
            'Identify exits',
            'info',
            $msg,
            'ok',
        );

        return 1;
    }

    sub selectExitTypeCallback {

        # Called by $self->enableExitsColumn
        # Scours the current map, looking for unallocated, unallocatable, uncertain or incomplete
        #   exits (or all four of them together)
        # Once found, selects both the exits and the parent rooms
        # Finally, displays a 'dialogue' window showing how many were found
        #
        # Expected arguments
        #   $type   - What to search for. Must be either 'unallocated', 'unallocatable, 'uncertain',
        #               'incomplete' or 'all_above'; can also be 'region' or 'super'
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $type, $check) = @_;

        # Local variables
        my (
            $obj, $number, $title, $text,
            @exitNumList, @exitObjList,
            %selectExitHash, %selectRoomHash,
        );

        # Check for improper arguments
        if (
            ! defined $type
            || (
                $type ne 'uncertain' && $type ne 'incomplete' && $type ne 'unallocated'
                && $type ne 'unallocatable' && $type ne 'all_above' && $type ne 'region'
                && $type ne 'super'
            ) || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->selectExitTypeCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Compile a list of all exit objects drawn in this map
        @exitNumList = $self->currentRegionmap->ivKeys('gridExitHash');
        foreach my $exitNum (@exitNumList) {

            push (@exitObjList, $self->worldModelObj->ivShow('exitModelHash', $exitNum));
        }

        # Check each exit in turn. If it's one of the exits for which we're looking, mark it for
        #   selection
        OUTER: foreach my $exitObj (@exitObjList) {

            if (
                (
                    ($type eq 'uncertain' || $type eq 'all_above')
                    && $exitObj->destRoom
                    && (! $exitObj->twinExit)
                    && (! $exitObj->oneWayFlag)
                    && (! $exitObj->retraceFlag)
                ) || (
                    ($type eq 'incomplete' || $type eq 'all_above')
                    && (! $exitObj->destRoom && $exitObj->randomType eq 'none')
                ) || (
                    ($type eq 'unallocated' || $type eq 'all_above')
                    && ($exitObj->drawMode eq 'temp_alloc' || $exitObj->drawMode eq 'temp_unalloc')
                ) || (
                    $type eq 'unallocatable' && $exitObj->drawMode eq 'temp_unalloc'
                ) || (
                    $type eq 'region' && $exitObj->regionFlag
                ) || (
                    $type eq 'super' && $exitObj->superFlag
                )
            ) {
                $selectExitHash{$exitObj->number} = $exitObj;
                $selectRoomHash{$exitObj->parent}
                    = $self->worldModelObj->ivShow('modelHash', $exitObj->parent);
            }
        }

        # If anything was marked for selection...
        if (%selectExitHash) {

            # Since we're going to redraw everything on the map, we'll sidestep the normal call to
            #   $self->setSelectedObj, and set IVs directly

            # Make sure there are no rooms, exits, room tags or labels selected
            $self->ivUndef('selectedRoom');
            $self->ivEmpty('selectedRoomHash');
            $self->ivUndef('selectedExit');
            $self->ivEmpty('selectedExitHash');
            $self->ivUndef('selectedRoomTag');
            $self->ivEmpty('selectedRoomTagHash');
            $self->ivUndef('selectedLabel');
            $self->ivEmpty('selectedLabelHash');

            # Select rooms and exits. (There must be at least one of each, if there are any, so we
            #   don't ever set ->selectedRoom or ->selectedExit)
            if (scalar (keys %selectRoomHash) > 1) {

                $self->ivPoke('selectedRoomHash', %selectRoomHash);

            } elsif (%selectRoomHash) {

                ($number) = keys %selectRoomHash;
                $obj = $self->worldModelObj->ivShow('modelHash', $number);
                $self->ivAdd('selectedRoomHash', $number, $obj);
            }

            if (scalar (keys %selectExitHash) > 1) {

                $self->ivPoke('selectedExitHash', %selectExitHash);

            } elsif (%selectExitHash) {

                ($number) = keys %selectExitHash;
                $obj = $self->worldModelObj->ivShow('exitModelHash', $number);
                $self->ivAdd('selectedExitHash', $number, $obj);
            }

            # Redraw the current level, to show all the changes
            $self->drawRegion();

            # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
            $self->restrictWidgets();

            # Show a confirmation of how many uncertain/incomplete exits were found
            if ($type eq 'all_above') {

                $title = 'Select exits';

                # (This will be a longer string, so spread it across two lines)
                if (scalar (keys %selectExitHash) == 1) {

                    $text = "Found 1 unallocated/uncertain/incomplete exit\n";

                } else {

                    $text = "Found " . scalar (keys %selectExitHash) . " unallocated/uncertain/"
                            . "incomplete exits\n";
                }

            } else {

                $title = 'Select ' . $type . ' exits';

                # (This will be a shorter string, so keep it on one line)
                if (scalar (keys %selectExitHash) == 1) {
                    $text = "Found 1 $type exit ";
                } else {
                    $text = "Found " . scalar (keys %selectExitHash) . " $type exits ";
                }
            }

            if (scalar (keys %selectRoomHash) == 1) {
                $text .= 'in 1 room';
            } else {
                $text .= 'spread across ' . scalar (keys %selectRoomHash) . ' rooms';
            }

            $text .= ' in this regionmap';

            $self->showMsgDialogue(
                $title,
                'info',
                $text,
                'ok',
            );

        } else {

            # Show a confirmation that there are no uncertain/incomplete exits in this map
            if ($type eq 'all_above') {

                $title = 'Select exits';
                $text = "There are no more unallocated, uncertain or\n"
                    . "incomplete exits in this regionmap";

            } else {

                $title = 'Select ' . $type . ' exits';
                $text = 'There are no more ' . $type . ' exits in this regionmap';
            }

            $self->showMsgDialogue(
                $title,
                'info',
                $text,
                'ok',
            );
        }

        return 1;
    }

    sub editExitCallback {

        # Called by $self->enableExitsColumn
        # Opens a GA::EditWin::Exit for the selected exit. If the selected exit could be confused
        #   with others occupying (roughly) the same space, opens a 'dialogue' window so the user
        #   can choose one
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $comboListRef, $exitHashRef, $choice, $exitObj,
            @comboList,
            %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->editExitCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedExit) {

            return undef;
        }

        # If the selected exit has a twin exit and/or a shadow exit, we need to prompt the user to
        #   ask which of them should be edited
        if ($self->selectedExit->twinExit || $self->selectedExit->shadowExit) {

            ($comboListRef, $exitHashRef) = $self->compileExitList();
            if (! defined $comboListRef) {

                return undef;
            }

            @comboList = @$comboListRef;
            %exitHash = %$exitHashRef;

            # Prompt the user to choose which exit to edit
            $choice = $self->showComboDialogue(
                'Select exit',
                'Select which exit to edit:',
                FALSE,
                \@comboList,
            );

            if (! $choice) {

                return undef;

            } else {

                $exitObj = $exitHash{$choice};

                # Also make this the selected exit
                $self->setSelectedObj(
                    [$exitHash{$choice}, 'exit'],
                    FALSE,      # Select this object; unselect all other objects
                );
            }

        # Otherwise, just edit the selected exit
        } else {

            $exitObj = $self->selectedExit;
        }

        # Open up an 'edit' window to edit the object
        $self->createFreeWin(
            'Games::Axmud::EditWin::Exit',
            $self,
            $self->session,
            'Edit exit model object #' . $exitObj->number,
            $exitObj,
            FALSE,                          # Not temporary
        );

        return 1;
    }

    sub completeExitsCallback {

        # Called by $self->enableExitsColumn
        # Checks all of the selected exits and exits of selected rooms
        # Converts any uncertain exits into two-way exits
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            @exitNumList, @exitObjList, @roomObjList,
            %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->completeExitsCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Compile a list of all selected rooms in the current regionmap
        @roomObjList = $self->compileSelectedRooms();

        # Compile a hash of all selected exits in the current regionmap
        foreach my $exitObj ($self->compileSelectedExits()) {

            $exitHash{$exitObj->number} = $exitObj;
        }

        # Go through the list of rooms, adding all its exits to a hash (to eliminate duplicates)
        foreach my $roomObj (@roomObjList) {

            if ($roomObj->exitNumHash) {

                @exitNumList = $roomObj->ivValues('exitNumHash');
                foreach my $exitNum (@exitNumList) {

                    if (! exists $exitHash{$exitNum}) {

                        $exitHash{$exitNum}
                            = $self->worldModelObj->ivShow('exitModelHash', $exitNum);
                    }
                }
            }
        }

        # Extract the hash into a list of exit objects, comprising all the selected exits and
        #   all exits belonging to selected rooms
        @exitObjList = values %exitHash;

        # Check each exit in turn. If it's an uncertain exit and if, in the opposite direction,
        #   there's an incomplete exit, convert the pair into two-way exits. Instruct the world
        #   model to update its Automapper windows
        $self->worldModelObj->completeExits(
            $self->session,
            TRUE,           # Update Automapper windows now
            @exitObjList,
        );

        return 1;
    }

    sub connectAdjacentCallback {

        # Called by $self->enableExitsColumn
        # Connects any selected rooms which are adjacent to each other, and have incomplete/
        #   uncertain exits which can be converted into twin exits between them
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $wmObj, $horizontalLength, $verticalLength,
            @roomObjList, @redrawList,
            %roomHash, %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->connectAdjacentCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap || (! $self->selectedRoom && ! $self->selectedRoomHash)) {

            return undef;
        }

        # Import the world model (for convenience)
        $wmObj = $self->worldModelObj;
        # Also import the standard exit lengths (for convenience)
        $horizontalLength = $wmObj->horizontalExitLengthBlocks;
        $verticalLength = $wmObj->verticalExitLengthBlocks;

        # Compile a list of all selected rooms in the current regionmap
        @roomObjList = $self->compileSelectedRooms();
        # Also compile a hash (for quick lookup)
        foreach my $roomObj (@roomObjList) {

            $roomHash{$roomObj->number} = undef;
        }

        # Go through each room in turn, checking its incomplete and uncertain exits. For both, if
        #   there's an adjacent room with an exit in the opposite direction, convert them into
        #   twin exits
        # If GA::Obj::WorldModel->horizontalExitLengthBlocks is set to 1, then an adjacent room is
        #   in the next gridblock. If it is set to 2, an adjacent room is two gridblocks away. (The
        #   same applies for ->verticalExitLengthBlocks)
        # If either value is set to 2 or more, there must be no rooms between the two 'adjacent'
        #   rooms
        OUTER: foreach my $roomObj (@roomObjList) {

            my ($regionObj, $regionmapObj);

            $regionObj = $wmObj->ivShow('modelHash', $roomObj->parent);
            $regionmapObj = $wmObj->ivShow('regionmapHash', $regionObj->name);

            INNER: foreach my $exitDir ($roomObj->ivKeys('exitNumHash')) {

                my (
                    $exitNum, $exitObj, $vectorRef, $exitLength, $xPosBlocks, $yPosBlocks,
                    $zPosBlocks, $adjacentNum, $adjacentRoomObj, $result, $oppDir, $oppExitObj,
                );

                $exitNum = $roomObj->ivShow('exitNumHash', $exitDir);
                $exitObj = $wmObj->ivShow('exitModelHash', $exitNum);

                # Discard everything besides incomplete and uncertain exits which haven't been
                #   processed yet
                # Also discard retracting, random and unallocated exits
                if (
                    exists $exitHash{$exitNum}
                    || $exitObj->retraceFlag
                    || $exitObj->randomType ne 'none'
                    || $exitObj->drawMode eq 'temp_alloc'
                    || $exitObj->drawMode eq 'temp_unalloc'
                ) {
                    next INNER;
                }

                if (! $exitObj->destRoom && $exitObj->randomType eq 'none') {

                    # Incomplete exit. Is there an adjacent room, with no rooms in between $roomObj
                    #   and the adjacent room?
                    # Work out the potential adjacent room's location on the grid
                    $vectorRef = $self->ivShow('constVectorHash', $exitObj->mapDir);

                    if ($exitObj->mapDir eq 'up' || $exitObj->mapDir eq 'down') {
                        $exitLength = $verticalLength;
                    } else {
                        $exitLength = $horizontalLength;
                    }

                    $xPosBlocks = $roomObj->xPosBlocks + ($$vectorRef[0] * $exitLength);
                    $yPosBlocks = $roomObj->yPosBlocks + ($$vectorRef[1] * $exitLength);
                    $zPosBlocks = $roomObj->zPosBlocks + ($$vectorRef[2] * $exitLength);

                    # See if there is a potential adjacent room at this location that is a selected
                    #   room
                    $adjacentNum = $regionmapObj->fetchRoom($xPosBlocks, $yPosBlocks, $zPosBlocks);
                    if (! $adjacentNum || ! exists $roomHash{$adjacentNum}) {

                        next INNER;
                    }

                    $adjacentRoomObj = $wmObj->ivShow('modelHash', $adjacentNum);

                    # If $exitLength is greater than 1, make sure there are no rooms in between
                    #   $roomObj and the adjacent room
                    if ($exitLength > 1) {

                        # We can borrow GA::Obj::WorldModel->checkRoomAlignment to perform this
                        #   check for us. Temporarily set the exit's destination room as the
                        #   adjacent room to make it work
                        $exitObj->ivPoke('destRoom', $adjacentNum);
                        $result = $wmObj->checkRoomAlignment($self->session, $exitObj);
                        $exitObj->ivUndef('destRoom');

                        if (! $result) {

                            next INNER;
                        }
                    }

                    # Finally, check that the adjacent room has a suitable exit in the opposite
                    #   direction to $exitObj
                    $oppDir = $axmud::CLIENT->ivShow('constOppDirHash', $exitObj->mapDir);
                    THISLOOP: foreach my $otherExitDir ($adjacentRoomObj->ivKeys('exitNumHash')) {

                        my ($otherExitNum, $otherExitObj);

                        $otherExitNum = $adjacentRoomObj->ivShow('exitNumHash', $otherExitDir);
                        $otherExitObj = $wmObj->ivShow('exitModelHash', $otherExitNum);

                        if (
                            ! exists $exitHash{$otherExitNum}
                            && ! $otherExitObj->destRoom
                            && ! $otherExitObj->retraceFlag
                            && $otherExitObj->randomType eq 'none'
                            && (
                                $otherExitObj->drawMode eq 'primary'
                                || $otherExitObj->drawMode eq 'perm_alloc'
                            )
                            && $otherExitObj->mapDir eq $oppDir
                        ) {
                            # This exit is suitable, and in the right direction
                            $oppExitObj = $otherExitObj;
                            last THISLOOP;
                        }
                    }

                    if ($oppExitObj) {

                        # Connect these two adjacent rooms, converting $exitObj into an uncertain
                        #   exit
                        $self->worldModelObj->connectRooms(
                            $self->session,
                            FALSE,              # Don't update Automapper windows yet
                            $roomObj,
                            $adjacentRoomObj,
                            $exitObj->dir,
                            $exitObj->mapDir,
                            $exitObj,
                        );

                        # Only process each pair of exits once
                        $exitHash{$exitObj->number} = $exitObj;
                        $exitHash{$oppExitObj->number} = $oppExitObj;
                    }

                } elsif (
                    $exitObj->destRoom
                    && ((! $exitObj->twinExit) && (! $exitObj->oneWayFlag))
                ) {
                    # Uncertain exit. Code below converts it into a two-way exit; but we mustn't
                    #   convert it if the destination room wasn't one of the selected rooms
                    if (exists $roomHash{$exitObj->destRoom}) {

                        $exitHash{$exitObj->number} = $exitObj;
                    }
                }
            }
        }

        # Check each connected exit in turn. If it's an uncertain exit, convert it and its twin
        #   into a two-way exit
        $self->worldModelObj->completeExits(
            $self->session,
            FALSE,          # Don't update Automapper windows yet
            values %exitHash,
        );

        # NOW we can update Automapper windows, using each selected room (this is hopefully faster
        #   than letting ->completeExits do it for every affected exit)
        foreach my $roomObj (@roomObjList) {

            push (@redrawList, 'room', $roomObj);
        }

        $self->worldModelObj->updateMaps(@redrawList);

        return 1;
    }

    sub setExitLengthCallback {

        # Called by $self->enableExitsColumn
        # Prompts the user for a new exit length (distance between adjacent rooms on the map, when
        #   they are added), and sets GA::Obj::WorldModel->horizontalExitLengthBlocks or
        #   ->verticalExitLengthBlocks accordingly
        #
        # Expected arguments
        #   $type   - 'horizontal' or 'vertical', corresponding to the exit length IV stored in the
        #               world model to be set
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $type, $check) = @_;

        # Local variables
        my ($range, $title, $msg, $length);

        # Check for improper arguments
        if (! defined $type || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setExitLengthCallback', @_);
        }

        # (No standard callback checks for this function)

        # Prompt the user for a new exit length
        $range = '(1-' . $self->worldModelObj->maxExitLengthBlocks . ', currently set to ';

        if ($type eq 'vertical') {

            $title = 'Set vertical exit length';
            $msg = "Set the distance between stacked rooms\n" . $range;
            $msg .= $self->worldModelObj->verticalExitLengthBlocks . ')';

        } else {

            # (In case $type was not set to 'horizontal' or 'vertical', just carry on as if it were
            #   'horizontal')
            $type = 'horizontal';
            $title = 'Set horizontal exit length';
            $msg = "Set the distance between adjacent rooms\n" . $range;
            $msg .= $self->worldModelObj->horizontalExitLengthBlocks . ')';
        }

        $length = $self->showEntryDialogue(
            $title,
            $msg,
        );

        if ($length) {

            # Check that $length is a valid integer, in the permitted range
            if (
                ! ($length =~ /\D/)
                && $length > 0
                && $length <= $self->worldModelObj->maxExitLengthBlocks
            ) {
                $self->worldModelObj->set_exitLengthBlocks($type, $length);

            } else {

                # Show an explanatory message
               $self->showMsgDialogue(
                    $title,
                    'error',
                    'Invalid value for exit length - must be an integer between 1 and '
                    . $self->maxExitLengthBlocks,
                    'ok',
                );
            }
        }

        return 1;
    }

    sub resetExitLengthCallback {

        # Called by $self->enableExitsColumn
        # Resets the horizontal and vertical exit lengths stored in the world model back to the
        #   default value of 1
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->resetExitLengthCallback',
                @_,
            );
        }

        # (No standard callback checks for this function)

        # If both exit lengths are already set to 1, there's nothing to do
        if (
            $self->worldModelObj->horizontalExitLengthBlocks == 1
            && $self->worldModelObj->verticalExitLengthBlocks == 1
        ) {
            $self->showMsgDialogue(
                'Reset exit length',
                'warning',
                'Both types of exit length were already set to 1',
                'ok',
            );

        } else {

            # Reset the exit lengths and display a confirmation
            $self->worldModelObj->set_exitLengthBlocks('horizontal', 1);
            $self->worldModelObj->set_exitLengthBlocks('vertical', 1);

            $self->showMsgDialogue(
                'Reset exit length',
                'info',
                'Both types of exit length were reset to 1',
                'ok',
            );
        }

        return 1;
    }

    sub deleteExitCallback {

        # Called by $self->enableExitsColumn
        # Deletes the currently selected exit. If the selected exit could be confused with others
        #   occupying (roughly) the same space, opens a 'dialogue' window so the user can choose one
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if there's an
        #       error
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $bothString, $allString, $stringListRef, $exitHashRef, $choice,
            @stringList, @comboList, @finalList,
            %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->deleteExitCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedExit) {

            return undef;
        }

        # Possible initial combo items
        $bothString = '<delete both>';
        $allString = '<delete all>';

        # Compile a list of exits which could be confused with the currently selected one
        ($stringListRef, $exitHashRef) = $self->compileExitList();
        if (! defined $stringListRef) {

            return undef;
        }

        @stringList = @$stringListRef;
        %exitHash = %$exitHashRef;

        # If there is more than one exit in the list, prompt the user to specify which one to delete
        if (scalar @stringList > 1) {

            # Compile the combo list
            if (@stringList == 2) {
                @comboList = ($bothString, @stringList);
            } elsif (@stringList > 2) {
                @comboList = ($allString, @stringList);
            } else {
                @comboList = @stringList;
            }

            # Prompt the user to choose which exit to delete
            $choice = $self->showComboDialogue(
                'Select exit',
                'Select which exit to delete:',
                FALSE,
                \@comboList,
            );

            if (! $choice) {

                return undef;

            } elsif ($choice eq $bothString || $choice eq $allString) {

                @finalList = values %exitHash;

            } else {

                push (@finalList, $exitHash{$choice});
            }

        } else {

            # There's only one exit on which to operate
            push (@finalList, $self->selectedExit);
        }

        # Delete the exit object(s) and instruct the world model to update its Automapper windows
        $self->worldModelObj->deleteExits(
            $self->session,
            TRUE,           # Update Automapper windows now
            @finalList,
        );

        return 1;
    }

    sub addBendCallback {

        # Called by $self->enableExitsPopupMenu (only)
        # After a right-click on an exit, when the user has selected 'add bend' in the popup menu,
        #   add a bend at the same position
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the bend is
        #       not added
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $startXPos, $startYPos, $clickXPos, $clickYPos, $stopXPos, $stopYPos, $resultType,
            $twinExitObj,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->addBendCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->selectedExit
            || (! $self->selectedExit->oneWayFlag && ! $self->selectedExit->twinExit)
            || ! $self->selectedExit->brokenFlag
            || ! defined $self->exitClickXPosn
            || ! defined $self->exitClickYPosn
        ) {
            return undef;
        }

        # Get the absolute coordinates of the start of the middle (bending) section of the
        #   exit
        # At the same time, convert the absolute coordinates of the right-mouse click on the exit,
        #   and the absolute coordinates of the end of the bending section, into coordinates
        #   relative to the start of the bending section of the eixt
        ($startXPos, $startYPos, $clickXPos, $clickYPos, $stopXPos, $stopYPos, $resultType)
            = $self->findExitClick(
                $self->selectedExit,
                $self->exitClickXPosn,
                $self->exitClickYPosn,
            );

        # If the click wasn't in the parent room's gridblock, in the destination room's gridblock
        #   or too close to an existing bend...
        if (! $resultType) {

            # Add a bend to the exit
            $self->worldModelObj->addExitBend(
                FALSE,                          # Don't update Automapper windows yet
                $self->selectedExit,
                $startXPos, $startYPos,
                $clickXPos, $clickYPos,
                $stopXPos, $stopYPos,
            );

            # Repeat the process for the selected exit's twin (if there is one)
            if ($self->selectedExit->twinExit) {

                $twinExitObj = $self->worldModelObj->ivShow(
                    'exitModelHash',
                    $self->selectedExit->twinExit,
                );

                ($startXPos, $startYPos, $clickXPos, $clickYPos, $stopXPos, $stopYPos)
                    = $self->findExitClick(
                        $twinExitObj,
                        $self->exitClickXPosn,
                        $self->exitClickYPosn,
                    );

                $self->worldModelObj->addExitBend(
                    FALSE,                          # Don't update Automapper windows yet
                    $twinExitObj,
                    $startXPos, $startYPos,
                    $clickXPos, $clickYPos,
                    $stopXPos, $stopYPos,
                );
            }

            # Now we can redraw the exit
            $self->worldModelObj->updateMapExit(
                $self->selectedExit,
                $twinExitObj,               # May be 'undef'
            );

            return 1;

        } else {

            # If the click was too close to an existing bend, show a message explaining why nothing
            #   has happened (don't bother showing a message for other values of $resultType, which
            #   probably can't be returned to this function anyway)
            if ($resultType == 3) {

                $self->showMsgDialogue(
                    'Add bend',
                    'error',
                    'Cannot add a bend - you clicked too close to an existing bend',
                    'ok',
                );
            }

            return undef;
        }
    }

    sub removeBendCallback {

        # Called by $self->enableExitsPopupMenu (only)
        # After a right-click on an exit, when the user has selected 'remove bend' in the popup
        #   menu, remove the bend closest to the clicked position
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails or if the mouse
        #       click was not near a bend
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($index, $twinExitObj);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->removeBendCallback', @_);
        }

        # Standard callback check
        if (
            ! $self->currentRegionmap
            || ! $self->selectedExit
            || (! $self->selectedExit->oneWayFlag && ! $self->selectedExit->twinExit)
            || ! $self->selectedExit->bendOffsetList
            || ! defined $self->exitClickXPosn
            || ! defined $self->exitClickYPosn
        ) {
            return undef;
        }

        # Find the number of the bend which is closest to the the clicked position
        $index = $self->findExitBend(
            $self->selectedExit,
            $self->exitClickXPosn,
            $self->exitClickYPosn,
        );

        if (! defined $index) {

            $self->showMsgDialogue(
                'Remove bend',
                'error',
                'Please right-click on the bend that you want to remove',
                'ok',
            );

            return undef;

        } else {

            # Remove this bend
            $self->worldModelObj->removeExitBend(
                TRUE,                   # Update Automapper windows now
                $self->selectedExit,
                $index,                 # Remove this bend (first bend is numbered 0)
            );

            # If there is a twin exit, remove the corresponding bend at the same time
            if ($self->selectedExit->twinExit) {

                $twinExitObj = $self->worldModelObj->ivShow(
                    'exitModelHash',
                    $self->selectedExit->twinExit,
                );

                $self->worldModelObj->removeExitBend(
                    TRUE,               # Update Automapper windows now
                    $twinExitObj,
                    ((scalar $self->selectedExit->bendOffsetList / 2) - $index - 1),
                );
            }

            return 1;
        }
    }

    # Menu 'Labels' column callbacks

    sub addLabelAtBlockCallback {

        # Called by $self->enableLabelsColumn
        # Prompts the user to supply a gridblock (via a 'dialogue' window) and creates a label at
        #   that location
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the standard callback check fails, if the user clicks
        #       the 'cancel' button on the 'dialogue' window or if the new label can't be created
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my ($xPosBlocks, $yPosBlocks, $zPosBlocks, $text);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->addLabelAtBlockCallback',
                @_,
            );
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Prompt the user for a gridblock
        ($xPosBlocks, $yPosBlocks, $zPosBlocks) = $self->promptGridBlock();
        if (! defined $xPosBlocks) {

            # User clicked the 'cancel' button
            return undef;
        }

        # Check that the specified gridblock actually exists
        if (
            ! $self->currentRegionmap->checkGridBlock(
                $xPosBlocks,
                $yPosBlocks,
                $zPosBlocks,
            )
        ) {
            $self->showMsgDialogue(
                'Add label at block',
                'error',
                'Invalid gridblock: x=' . $xPosBlocks . ', y=' . $yPosBlocks . ', z=' . $zPosBlocks,
                'ok',
            );
        }

        # Prompt the user to specify the label text
        $text = $self->showEntryDialogue(
            'Add label at block',
            'Enter the contents of the new label',
        );

        # Free click mode must be reset (nothing special happens when the user clicks on the map)
        $self->ivPoke('freeClickMode', 'default');

        if ($text || (defined $text && $text eq '0')) {        # '0' is a valid label

            # Create a new label at the specified location
            return $self->worldModelObj->addLabel(
                $self->session,
                TRUE,       # Update Automapper windows now
                $self->currentRegionmap,
                ($xPosBlocks * $self->currentRegionmap->blockWidthPixels),
                ($yPosBlocks * $self->currentRegionmap->blockHeightPixels),
                $zPosBlocks,
                $text,
            );
        }
    }

    sub addLabelAtClickCallback {

        # Called by $self->enableCanvasPopupMenu and ->canvasEventHandler
        # Adds a label at a specified location on the current level
        #
        # Expected arguments
        #   $xPosPixels, $yPosPixels
        #       - The grid coordinates at which to create the label
        #
        # Return values
        #   'undef' on improper arguments, if the user clicks the 'cancel' button on the 'dialogue'
        #       window or if the new label can't be created
        #   1 otherwise

        my ($self, $xPosPixels, $yPosPixels, $check) = @_;

        # Local variables
        my $text;

        # Check for improper arguments
        if (! defined $xPosPixels || ! defined $yPosPixels || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->addLabelAtClickCallback',
                @_,
            );
        }

        # (No standard callback checks for this function)

        # Prompt the user to specify the label text
        $text = $self->showEntryDialogue(
            'Add label at click',
            'Enter the contents of the new label',
        );

        # Free click mode must be reset (nothing special happens when the user clicks on the map)
        $self->ivPoke('freeClickMode', 'default');

        if ($text || (defined $text && $text eq '0')) {        # '0' is a valid label

            # Create a new label at the specified location
            return $self->worldModelObj->addLabel(
                $self->session,
                TRUE,       # Update Automapper windows now
                $self->currentRegionmap,
                $xPosPixels,
                $yPosPixels,
                $self->currentRegionmap->currentLevel,
                $text,
            );
        }
    }

    sub editLabelCallback {

        # Called by $self->enableLabelsColumn
        # Prompts the user to enter a new ->name for the selected label
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $text;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->editLabelCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap || ! $self->selectedLabel) {

            return undef;
        }

        # Prompt the user for the new contents of the label
        $text = $self->showEntryDialogue(
            'Edit label',
            'Enter the new contents for the label',
            undef,              # No max chars
            $self->selectedLabel->name,
        );

        if ($text) {

            # Change the label's contents
            $self->worldModelObj->setLabelName(
                TRUE,                       # Update Automapper windows
                $self->selectedLabel,
                $text,
            );
        }

        return 1;
    }

    sub selectLabelCallback {

        # Called by $self->enableLabelsColumn
        # Prompts the user to select a label, from a combobox listing all the labels in the current
        #   regionmap
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if the standard callback check fails
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $allString, $choice, $labelObj,
            @labelList, @sortedList, @comboList, @finalList,
            %comboHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->selectLabelCallback', @_);
        }

        # Standard callback check
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Get a sorted list of labels
        @labelList = $self->currentRegionmap->ivValues('gridLabelHash');
        @sortedList = sort {lc($a->name) cmp lc($b->name)} (@labelList);

        # Don't prompt for a label, if there are none available
        if (! @sortedList) {

            return $self->showMsgDialogue(
                'Select label',
                'error',
                'There are no labels in this regionmap',
                'ok',
            );
        }

        # Prepare the contents of a combobox. Those labels which aren't on the currently displayed
        #   level are marked as being on a different level
        foreach my $obj (@sortedList) {

            my $string;

            if ($obj->level == $self->currentRegionmap->currentLevel) {

                $string = $obj->name;

            } else {

                $string = $obj->name . ' (level ' . $obj->level . ')';
            }

            push (@comboList, $string);
            $comboHash{$string} = $obj;
        }

        # At the top of the list, put an option to select all labels
        $allString = '<select all labels>';
        unshift (@comboList, $allString);

        # Prompt the user for a label
        $choice = $self->showComboDialogue(
            'Select label',
            'Choose a label to mark as selected',
            FALSE,
            \@comboList,
        );

        if ($choice) {

            if ($choice eq $allString) {

                # Unselect any existing selected objects
                $self->setSelectedObj();

                # Select every label in this region
                foreach my $obj (@sortedList) {

                    push (@finalList, $obj, 'label');
                }

                # Select the labels
                $self->setSelectedObj(
                    \@finalList,
                    TRUE,       # Select multiple objects
                );

            } else {

                $labelObj = $comboHash{$choice};

                # Select the label
                $self->setSelectedObj(
                    [$labelObj, 'label'],
                    FALSE,      # Select this object; unselect all other objects
                );
            }
        }

        return 1;
    }

    # Graphical operations - window and background

    sub setWinTitle {

        # Called by GA::Task::Locator->refreshWin or by any other function
        # Sets the Automapper window's title. If there is a current region and/or a current level,
        #   both are displayed in the title bar
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $optionalText   - Text to be displayed after the usual text
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $optionalText, $check) = @_;

        # Local variables
        my ($text, $upFlag, $downFlag);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setWinTitle', @_);
        }

        if ($self->currentRegionmap) {

            if (defined $self->currentRegionmap->currentLevel) {

                # Show both the current map and the current level
                $text = 'Automapper [' . $self->currentRegionmap->name . ', level '
                        . $self->currentRegionmap->currentLevel . ']';

                # Show arrows if higher/lower levels are occupied
                if (
                    defined $self->currentRegionmap->highestLevel
                    && $self->currentRegionmap->currentLevel < $self->currentRegionmap->highestLevel
                ) {
                    $upFlag = TRUE;
                }

                if (
                    defined $self->currentRegionmap->lowestLevel
                    && $self->currentRegionmap->currentLevel > $self->currentRegionmap->lowestLevel
                ) {
                    $downFlag = TRUE;
                }

                # Use either arrows or empty space so that the text stays in roughly the same place
                if ($upFlag) {
                    $text .= ' /\\';
                } else {
                    $text .= '   ';
                }

                if ($downFlag) {
                    $text .= ' \\/';
                } else {
                    $text .= '   ';
                }

            } else {

                # Show just the current region
                $text = 'Automapper [' . $self->currentRegionmap->name . ']';
            }

        } else {

            # Don't show the current region
            $text = 'Automapper';
        }

        # Add optional text, if any was specified
        if ($optionalText) {

            $text .= ' ' . $optionalText;
        }

        # Write the text to the title
        if ($self->pseudoWinTableObj) {
            $self->pseudoWinTableObj->set_frameTitle($text);
        } else {
            $self->winWidget->set_title($text);
        }

        return 1;
    }

    sub resetMap {

        # Can be called by anything (first called by $self->enableCanvas)
        # Deletes all canvas objects drawn on the canvas, ready to redraw new ones
        # Resets the background used as the currently displayed map, changing its colour
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $mapFlag    - If set to TRUE, the background colour is set as the standard map colour
        #                   (default is cream). If set to FALSE (or 'undef'), the no-background
        #                   colour is used to show there is no map displayed (default is white)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $mapFlag, $check) = @_;

        # Local variables
        my (
            $colour, $newObj,
            @objList,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->resetMap', @_);
        }

        # If the tooltips are visible, hide them
        $self->hideTooltips();

        # Draw up a list of existing canvas objects that must be destroyed
        foreach my $listRef ($self->ivValues('drawnRoomHash')) {

            push (@objList, @$listRef);
        }

        foreach my $listRef ($self->ivValues('drawnRoomEchoHash')) {

            push (@objList, @$listRef);
        }

        foreach my $listRef ($self->ivValues('drawnRoomTagHash')) {

            push (@objList, @$listRef);
        }

        foreach my $listRef ($self->ivValues('drawnRoomGuildHash')) {

            push (@objList, @$listRef);
        }

        foreach my $listRef ($self->ivValues('drawnRoomTextHash')) {

            push (@objList, @$listRef);
        }

        foreach my $listRef ($self->ivValues('drawnExitHash')) {

            push (@objList, @$listRef);
        }

        foreach my $listRef ($self->ivValues('drawnExitTagHash')) {

            push (@objList, @$listRef);
        }

        foreach my $listRef ($self->ivValues('drawnOrnamentHash')) {

            push (@objList, @$listRef);
        }

        foreach my $listRef ($self->ivValues('drawnLabelHash')) {

            push (@objList, @$listRef);
        }

        # Also delete the background object itself, if it exists
        if ($self->canvasBackground) {

            push (@objList, $self->canvasBackground);
        }

        # Destroy each existing canvas object
        foreach my $obj (@objList) {

            $obj->destroy();
        }

        # Empty the IVs that contained these now-destroyed objects
        $self->ivEmpty('drawnRoomHash');
        $self->ivEmpty('dummyRoomHash');        # (Subset of ->drawnRoomHash)
        $self->ivEmpty('drawnRoomEchoHash');
        $self->ivEmpty('drawnRoomTagHash');
        $self->ivEmpty('drawnRoomGuildHash');
        $self->ivEmpty('drawnRoomTextHash');
        $self->ivEmpty('drawnExitHash');
        $self->ivEmpty('drawnExitTagHash');
        $self->ivEmpty('drawnOrnamentHash');
        $self->ivEmpty('drawnLabelHash');

        # Any objects that were marked to be drawn (or re-drawn) can now be removed
        $self->ivEmpty('markedRoomHash');
        $self->ivEmpty('markedRoomTagHash');
        $self->ivEmpty('markedRoomGuildHash');
        $self->ivEmpty('markedExitHash');
        $self->ivEmpty('markedExitTagHash');
        $self->ivEmpty('markedLabelHash');

        if ($mapFlag) {

            # Draw a map background (default colour: cream)
            $colour = $self->worldModelObj->backgroundColour;
            $self->canvas->set_pixels_per_unit($self->currentRegionmap->magnification);

            # Draw the canvas object
            $newObj = Gnome2::Canvas::Item->new (
                $self->canvasRoot,
                'Gnome2::Canvas::Rect',
                x1 => 0,
                y1 => 0,
                x2 => $self->currentRegionmap->mapWidthPixels,
                y2 => $self->currentRegionmap->mapHeightPixels,
                fill_color => $colour,
                outline_color => $colour,
            );

            $self->canvas->set_scroll_region(
                0,
                0,
                $self->currentRegionmap->mapWidthPixels,
                $self->currentRegionmap->mapHeightPixels,
            );
            $self->canvas->set_center_scroll_region(1);

        } else {

            # Draw a no-map background (default colour: white)
            $colour = $self->worldModelObj->noBackgroundColour;
            $self->canvas->set_pixels_per_unit(1);

            # Draw the canvas object
            $newObj = Gnome2::Canvas::Item->new (
                $self->canvasRoot,
                'Gnome2::Canvas::Rect',
                x1 => 0,
                y1 => 0,
                x2 => $self->worldModelObj->defaultMapWidthPixels,
                y2 => $self->worldModelObj->defaultMapHeightPixels,
                fill_color => $colour,
                outline_color => $colour,
            );

            $self->canvas->set_scroll_region(
                0,
                0,
                $self->worldModelObj->defaultMapWidthPixels,
                $self->worldModelObj->defaultMapHeightPixels,
            );
            $self->canvas->set_center_scroll_region(1);
        }

        # The background is lower than anything else
        $self->setMapLevel($newObj);
        # Set up the event handler for the background map
        $self->setupCanvasEvent($newObj);

        # Store the IV
        $self->ivPoke('canvasBackground', $newObj);

        return 1;
    }

    sub drawRegion {

        # Called by $self->setCurrentRegion or by any other function
        # Draws one level of the current region on the map - namely, the level specified by
        #   $self->currentRegionmap->currentLevel
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments or if there is no current regionmap
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my @drawList;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawRegion', @_);
        }

        # If the tooltips are visible, hide them
        $self->hideTooltips();

        # Don't draw anything if there is no current region
        if (! $self->currentRegionmap) {

            return undef;
        }

        # Reset the map, which empties all objects currently drawn on it, and makes sure the
        #   background is the right colour
        $self->resetMap(TRUE);

        # Compose a list of objects to draw. If there are any rooms in this region, on this level,
        #   add them (any room tags, room guilds, exits or exits tags will be automatically drawn
        #   too)
        if ($self->currentRegionmap->gridRoomHash) {

            foreach my $number ($self->currentRegionmap->ivValues('gridRoomHash')) {

                push (@drawList, 'room', $self->worldModelObj->ivShow('modelHash', $number));
            }
        }

        # If there are any labels in this region, on this level, add them to the list
        if ($self->currentRegionmap->gridLabelHash) {

            foreach my $obj ($self->currentRegionmap->ivValues('gridLabelHash')) {

                push (@drawList, 'label', $obj);
            }
        }

        # If any objects need to be drawn, draw them now
        if (@drawList) {

            $self->doDraw(@drawList);
        }

        return 1;
    }

    sub markObjs {

        # Can be called by anything
        # Adds a list of objects which need to be drawn to the ->markedRoomHash (etc) IVs; they
        #   will be drawn the next time $self->doDraw is called
        # The objects can be rooms, room tags, room guilds, exits, exit tags or labels. (If the list
        #   contains any rooms, the room's associated room tags, room guilds, exits and exit tags
        #   are automatically redrawn when ->doDraw eventually gets called)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   @drawList   - A list of objects to be drawn (or redrawn), in the form
        #                   (type, object, type, object, ...)
        #               - ...where 'type' is one of the strings 'room', 'room_tag', 'room_guild',
        #                   'exit', 'exit_tag' and 'label', and 'object' is a GA::ModelObj::Room,
        #                   GA::Obj::Exit or GA::Obj::MapLabel
        #
        # Return values
        #   'undef' if there is no current regionmap (so nothing can be drawn) or if an unrecognised
        #       'type' is specified
        #   1 otherwise

        my ($self, @drawList) = @_;

        # (No improper arguments to check)

        # If there's no current regionmap, then nothing can be drawn
        if (! $self->currentRegionmap) {

            return undef;
        }

        if (@drawList) {

            do {

                my ($type, $obj, $posn);

                $type = shift @drawList;
                $obj = shift @drawList;

                if (! $obj) {

                    # Unrecognised $type
                    return $self->session->writeError(
                        'Undefined object type in draw list',
                        $self->_objClass . '->markObjs',
                    );

                # Mark the object to be drawn
                } elsif ($type eq 'room') {
                    $self->ivAdd('markedRoomHash', $obj->number, $obj);
                } elsif ($type eq 'room_tag') {
                    $self->ivAdd('markedRoomTagHash', $obj->number, $obj);
                } elsif ($type eq 'room_guild') {
                    $self->ivAdd('markedRoomGuildHash', $obj->number, $obj);
                } elsif ($type eq 'exit') {
                    $self->ivAdd('markedExitHash', $obj->number, $obj);
                } elsif ($type eq 'exit_tag') {
                    $self->ivAdd('markedExitTagHash', $obj->number, $obj);
                } elsif ($type eq 'label') {
                    $self->ivAdd('markedLabelHash', $obj->number, $obj);
                } else {

                    # Unrecognised $type
                    return $self->session->writeError(
                        'Unrecognised object type \'' . $type . '\'',
                        $self->_objClass . '->markObjs',
                    );
                }

            } until (! @drawList);
        }

        # Operation complete
        return 1;
    }

    # Graphical operations - map objects

    sub doDraw {

        # Can be called by anything
        # All the objects which have been marked to be drawn (or re-drawn) are stored in
        #   $self->markedRoomHash, ->markedRoomTagHash, ->markedRoomGuildHash, ->markedExitHash,
        #   ->markedExitTag Hash and $self->markedLabelHash
        # In addition, a list of objects can be specified, which are added to any stored in those
        #   IVs (any duplicates are eliminated)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   @drawList   - A list of objects to be drawn (or redrawn), in the form
        #                   (type, object, type, object, ...)
        #               - ...where 'type' is one of the strings 'room', 'room_tag', 'room_guild',
        #                   'exit', 'exit_tag' and 'label', and 'object' is a GA::ModelObj::Room,
        #                   GA::Obj::Exit or GA::Obj::MapLabel
        #
        # Return values
        #   'undef' if there is no current regionmap (so nothing can be drawn), if
        #       $self->delayDrawFlag is set or if no objects have been marked for drawing
        #   1 otherwise

        my ($self, @drawList) = @_;

        # Local variables
        my (
            $drawCount, $flag, $mode,
            %markedRoomHash, %markedRoomTagHash, %markedRoomGuildHash, %markedExitHash,
            %markedExitTagHash, %markedLabelHash,
        );

        # (No improper arguments to check)

        # Don't draw anything if there is no current region
        if (! $self->currentRegionmap) {

            # Any objects which were marked to be drawn, can now be ignored (including any specified
            #   in @drawList)
            $self->ivEmpty('markedRoomHash');
            $self->ivEmpty('markedRoomTagHash');
            $self->ivEmpty('markedRoomGuildHash');
            $self->ivEmpty('markedExitHash');
            $self->ivEmpty('markedExitTagHash');
            $self->ivEmpty('markedLabelHash');

            return undef;
        }

        # Remember the number of items to be drawn (before we empty @drawList)
        $drawCount = (scalar @drawList) / 2;

        # Import the hashes of objects already marked to be drawn
        %markedRoomHash = $self->markedRoomHash;
        %markedRoomTagHash = $self->markedRoomTagHash;
        %markedRoomGuildHash = $self->markedRoomGuildHash;
        %markedExitHash = $self->markedExitHash;
        %markedExitTagHash = $self->markedExitTagHash;
        %markedLabelHash = $self->markedLabelHash;

        # If @drawList was specified, add them to the hashes
        if (@drawList) {

            do {

                my ($type, $obj, $posn);

                $type = shift @drawList;
                $obj = shift @drawList;

                if (! $obj) {

                    # Unrecognised $type
                    $self->session->writeWarning(
                        'Undefined object (type \'' . $type . '\') in draw list',
                        $self->_objClass . '->doDraw',
                    );

                # Mark the object to be drawn
                } elsif ($type eq 'room') {
                    $markedRoomHash{$obj->number} = $obj;
                } elsif ($type eq 'room_tag') {
                    $markedRoomTagHash{$obj->number} = $obj;
                } elsif ($type eq 'room_guild') {
                    $markedRoomGuildHash{$obj->number} = $obj;
                } elsif ($type eq 'exit') {
                    $markedExitHash{$obj->number} = $obj;
                } elsif ($type eq 'exit_tag') {
                    $markedExitTagHash{$obj->number} = $obj;
                } elsif ($type eq 'label') {
                    $markedLabelHash{$obj->number} = $obj;
                } else {

                    # Unrecognised $type
                    return $self->session->writeError(
                        'Unrecognised object type \'' . $type . '\'',
                        $self->_objClass . '->doDraw',
                    );
                }

            } until (! @drawList);
        }

        # If there is already a drawing cycle in progress (i.e. another call to this function), or
        #   if some other part of the code has set $self->delayDrawFlag to TRUE, store the hashes so
        #   that the objects can be drawn in the next cycle
        # Also, if nothing has been marked to be drawn, we can exit the function now (all the
        #   hashes we're about to store will be empty
        if (
            # Some other job must finish first
            $self->delayDrawFlag
            # Nothing has been marked to be drawn
            || (
                ! %markedRoomHash && ! %markedRoomTagHash && ! %markedRoomGuildHash
                && ! %markedExitHash && ! %markedExitTagHash && ! %markedLabelHash
            )
        ) {
            $self->ivPoke('markedRoomHash', %markedRoomHash);
            $self->ivPoke('markedRoomTagHash', %markedRoomTagHash);
            $self->ivPoke('markedRoomGuildHash', %markedRoomGuildHash);
            $self->ivPoke('markedExitHash', %markedExitHash);
            $self->ivPoke('markedExitTagHash', %markedExitTagHash);
            $self->ivPoke('markedLabelHash', %markedLabelHash);

            return undef;
        }

        # If the tooltips are visible, hide them
        $self->hideTooltips();

        # One call to this function represents a single drawing cycle. Once a cycle starts, it must
        #   not be interrupted by another call to this function. (We store the current value of
        #   the flag, so that we can restore its value at the end of the function)
        $flag = $self->delayDrawFlag;
        $self->ivPoke('delayDrawFlag', TRUE);

        # (We don't need two copies of the marked object hashes, so empty the IVs right away
        $self->ivEmpty('markedRoomHash');
        $self->ivEmpty('markedRoomTagHash');
        $self->ivEmpty('markedRoomGuildHash');
        $self->ivEmpty('markedExitHash');
        $self->ivEmpty('markedExitTagHash');
        $self->ivEmpty('markedLabelHash');

        # During the drawing cycle, we keep track of the exits we've drawn, so that we don't draw
        #   (for example) both an exit and its twin exit, which occupy the same space on the map
        # Empty the hash, following any previous call to this function
        $self->ivEmpty('drawCycleExitHash');

        # When thousands of objects are being drawn (default value: 500, not including exits whose
        #   rooms are also being drawn), the drawing process can take a little time. Make the pause
        #   window visible for the duration
        if ($drawCount > $self->worldModelObj->drawPauseNum) {

            $self->showPauseWin();
        }

        # Decide how exits are drawn. GA::Obj::WorldModel->drawExitMode is one of the values
        #   'ask_regionmap', 'no_exit', 'simple_exit' and 'complex_exit'. The regionmap's
        #   ->drawExitMode is any of these values except 'ask_regionmap'
        if ($self->worldModelObj->drawExitMode eq 'ask_regionmap') {
            $mode = $self->currentRegionmap->drawExitMode;
        } else {
            $mode = $self->worldModelObj->drawExitMode;
        }

        # Decide on the size of the area in which room interior text is drawn - half the width of a
        #   room, less some extra pixels so the text doesn't touch the edge of the room
        if ($mode eq 'no_exit') {

            $self->ivPoke('drawRoomTextWidth', $self->currentRegionmap->blockWidthPixels);
            $self->ivPoke('drawRoomTextHeight', $self->currentRegionmap->blockHeightPixels);

        } else {

            $self->ivPoke('drawRoomTextWidth', $self->currentRegionmap->roomWidthPixels);
            $self->ivPoke('drawRoomTextHeight', $self->currentRegionmap->roomHeightPixels);
        }

        # Decide on the size of room interior text. The value is based on the size of the room, by
        #   default; the value can be increased (or decreased) by the text size ratios stored in the
        #   world model
        $self->ivPoke(
            'drawRoomTextSize',
            (
                ($self->drawRoomTextWidth - 3) / 2
                * Gtk2::Pango->scale()
                * $self->currentRegionmap->magnification
            ),
        );

        # Decide on the size of text drawn for labels, room tags, room guilds and exit tags. The
        #   value is the size of text used by room tags, by default; the value can be increased (or
        #   decreased) by the text size ratios stored in the world model
        $self->ivPoke(
            'drawMapTextSize',
            (
                ($self->currentRegionmap->roomWidthPixels - 3) / 2
                * Gtk2::Pango->scale()
                * $self->currentRegionmap->magnification
            ),
        );

        # To save time during a single drawing cycle, we work out the position of each kind of exit
        #   in the sixteen cardinal directions, as if they were to be drawn at the top-left
        #   gridblock
        $self->preDrawPositions($mode);
#        if ($mode eq 'simple_exit' || $mode eq 'complex_exit') {
#
#            $self->preDrawExits($mode);
#        }
        # v1.0.469 Even if we're not drawing exits on this map, we still need to call ->preDrawExits
        #   because room tags (etc) are drawn at the same position as an exit
        $self->preDrawExits($mode);

        # Draw rooms
        foreach my $number (keys %markedRoomHash) {

            my $roomObj = $markedRoomHash{$number};

            $self->drawRoom($roomObj, $mode);

            # We don't need to draw the room tag and room guild (if any) for this room a second
            #   time, so we can delete the equivalent entries in those hashes
            # ($self->drawCycleExitHash prevents us from drawing the same exit twice)
            delete $markedRoomTagHash{number};
            delete $markedRoomTagHash{number};
        }

        # Draw room tags
        foreach my $roomObj (values %markedRoomTagHash) {

            $self->drawRoomTag($roomObj);
        }

        # Draw room guilds
        foreach my $roomObj (values %markedRoomGuildHash) {

            $self->drawRoomGuild($roomObj);
        }

        # Draw exits and exit tags (except in mode 'no_exit')
        if ($mode ne 'no_exit') {

            foreach my $exitObj (values %markedExitHash) {

                $self->drawExit($exitObj, $mode);
            }

            foreach my $exitObj (values %markedExitTagHash) {

                $self->drawExitTag($exitObj);
            }
        }

        # Draw labels
        foreach my $labelObj (values %markedLabelHash) {

            $self->drawLabel($labelObj);
        }

        # Make the pause window invisible
        $self->hidePauseWin();

        # Update the event queue to make the changes visible
        $axmud::CLIENT->desktopObj->updateWidgets($self->_objClass . '->doDraw');

        # Restore the delay flag's previous value, allowing the next drawing cycle to take place if
        #   it was previously set to FALSE
        $self->ivPoke('delayDrawFlag', $flag);

        # Operation complete
        return 1;
    }

    sub preDrawPositions {

        # Called by $self->doDraw, once per drawing cycle
        # Works out the coordinates of the
        #   - top-left corner of the gridblock
        #   - centre of the gridblock (and of the room)
        #   - top-left corner of the room border
        # ...relative to the gridblock, and stores the values as IVs. This cuts down on the time it
        #   takes to draw objects in the gridblock
        #
        # Expected arguments
        #   $mode   - Matches GA::Obj::WorldModel->drawExitMode or
        #               $self->currentRegionmap->drawExitMode; set to 'no_exit', 'simple_exit' or
        #               'complex_exit'
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $mode, $check) = @_;

        # Local variables
        my (
            $blockCentreXPosPixels, $blockCentreYPosPixels, $borderCornerXPosPixels,
            $borderCornerYPosPixels,
        );

        # Check for improper arguments
        if (! defined $mode || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->preDrawPositions', @_);
        }

        # Find the coordinates of the pixel occupying the centre of the block (and of the room)
        ($blockCentreXPosPixels, $blockCentreYPosPixels) = $self->getBlockCentre(
            0,      # $roomObj->xPosBlocks,
            0,      # $roomObj->yPosBlocks,
            0,      # $blockCornerXPosPixels,
            0,      # $blockCornerYPosPixels,
        );

        # Find the coordinates of the pixel at the top-left corner of the room's border
        if ($mode eq 'no_exit') {

            # Draw exit mode 'no_exit': The room takes up the whole gridblock
            ($borderCornerXPosPixels, $borderCornerYPosPixels) = (0, 0);

        } else {

            # Draw exit modes 'simple_exit'/'complex_exit': The room takes up the central part of
            #   the gridblock
            ($borderCornerXPosPixels, $borderCornerYPosPixels) = $self->getBorderCorner(
                0,  # $roomObj->xPosBlocks,
                0,  # $roomObj->yPosBlocks,
                0,  # $blockCornerXPosPixels,
                0,  # $blockCornerYPosPixels,
            );
        }

        # Store them as IVs
        $self->ivPoke('blockCornerXPosPixels', 0);
        $self->ivPoke('blockCornerYPosPixels', 0);
        $self->ivPoke('blockCentreXPosPixels', $blockCentreXPosPixels);
        $self->ivPoke('blockCentreYPosPixels', $blockCentreYPosPixels);
        $self->ivPoke('borderCornerXPosPixels', $borderCornerXPosPixels);
        $self->ivPoke('borderCornerYPosPixels', $borderCornerYPosPixels);

        return 1;
    }

    sub preDrawExits {

        # Called by $self->doDraw, once per drawing cycle, straight after the call to
        #   $self->preDrawPositions
        # Works out the position of each kind of exit relative to its gridblock, and stores the
        #   values as IVs. This cuts down on the time it takes to draw objects in the gridblock
        #
        # Expected arguments
        #   $mode   - Matches GA::Obj::WorldModel->drawExitMode or
        #               $self->currentRegionmap->drawExitMode; set to 'simple_exit' or
        #               'complex_exit'
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $mode, $check) = @_;

        # Local variables
        my (
            %vectorHash, %oppDirHash, %uncertainExitHash, %incompleteExitHash,
            %longExitHash, %squareExitHash,
        );

        # Check for improper arguments
        if (! defined $mode || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->preDrawExits', @_);
        }

        # Import $self->constVectorHash, in the form
        #    north                   => [0, -1, 0],
        #    northnortheast          => [0.5, -1, 0],
        #    northeast               => [1, -1, 0],
        #    eastnortheast           => [1, -0.5, 0],
        #    east                    => [1, 0, 0],
        #    eastsoutheast           => [1, 0.5, 0],
        #    southeast               => [1, 1, 0],
        #    southsoutheast          => [0.5, 1, 0],
        #    south                   => [0, 1, 0],
        #    southsouthwest          => [-0.5, 1, 0],
        #    southwest               => [-1, 1, 0],
        #    westsouthwest           => [-1, 0.5, 0],
        #    west                    => [-1, 0, 0],
        #    westnorthwest           => [-1, -0.5, 0],
        #    northwest               => [-1, -1, 0],
        #    northnorthwest          => [-0.5, -1, 0],
        #    up                      => [0, 0, 1],
        #    down                    => [0, 0, -1],
        %vectorHash = $self->constVectorHash;
        # Remove 'up' and 'down' - we don't need to calculate the positions of those exits
        delete $vectorHash{'up'};
        delete $vectorHash{'down'};

        # Import the GA::Client's list of standard primary directions and their standard opposite
        #   directions, in the form
        #   north                   => 'south',
        #   northnortheast          => 'southsouthwest',
        #   northeast               => 'southwest',
        #   eastnortheast           => 'westsouthwest',
        #   east                    => 'west',
        #   eastsoutheast           => 'westnorthwest',
        #   southeast               => 'northwest',
        #   southsoutheast          => 'northnorthwest',
        #   south                   => 'north',
        #   southsouthwest          => 'northnortheast',
        #   southwest               => 'northeast',
        #   westsouthwest           => 'eastnortheast',
        #   west                    => 'east',
        #   westnorthwest           => 'eastsoutheast',
        #   northwest               => 'southeast',
        #   northnorthwest          => 'southsoutheast',
        #   up                      => 'down',
        #   down                    => 'up',
        %oppDirHash = $axmud::CLIENT->constOppDirHash;
        # Remove 'up' and 'down' - we don't need to calculate the positions of those exits
        delete $oppDirHash{'up'};
        delete $oppDirHash{'down'};

        # Pre-draw incomplete exits
        foreach my $key (keys %vectorHash) {

            my (
                $vectorRef, $exitStartXPosPixels, $exitStartYPosPixels, $blockEdgeXPosPixels,
                $blockEdgeYPosPixels,
            );

            # Get the key's corresponding value
            $vectorRef = $vectorHash{$key};

            # Find the coordinates of the pixel at the edge of the room, just outside the border,
            #   from which a cardinal exit (one using the eigh compass directions) starts
            ($exitStartXPosPixels, $exitStartYPosPixels) = $self->getExitStart(
                0,
                0,
                $vectorRef,
                $self->blockCentreXPosPixels,
                $self->blockCentreYPosPixels,
                $self->borderCornerXPosPixels,
                $self->borderCornerYPosPixels,
            );

            if ($mode eq 'complex_exit') {

                # Complex exits - find the coordinates of the pixel near (but not at) the edge of
                #   the gridblock, which is intersected by the cardinal exit
                ($blockEdgeXPosPixels, $blockEdgeYPosPixels) = $self->getNearBlockEdge(
                    0,
                    0,
                    $vectorRef,
                    $self->blockCentreXPosPixels,
                    $self->blockCentreYPosPixels,
                );

            } else {

                # Simple exits - find the coordinates of the pixel at the edge of the gridblock,
                #   which is intersected by the cardinal exit
                ($blockEdgeXPosPixels, $blockEdgeYPosPixels) = $self->getBlockEdge(
                    0,
                    0,
                    $vectorRef,
                    $self->blockCentreXPosPixels,
                    $self->blockCentreYPosPixels,
                );
            }

            # Store the coordinates as a list reference
            $incompleteExitHash{$key} = [
                $exitStartXPosPixels,
                $exitStartYPosPixels,
                $blockEdgeXPosPixels,
                $blockEdgeYPosPixels,
            ];
        }

        # Pre-draw uncertain exits (drawn the same way, regardless of the value of $mode)
        foreach my $key (keys %vectorHash) {

            my (
                $vectorRef, $exitStartXPosPixels, $exitStartYPosPixels, $blockEdgeXPosPixels,
                $blockEdgeYPosPixels,
            );

            # Get the key's corresponding value
            $vectorRef = $vectorHash{$key};

            # Find the coordinates of the pixel at the edge of the room, just outside the border,
            #   from which a cardinal exit starts
            ($exitStartXPosPixels, $exitStartYPosPixels) = $self->getExitStart(
                0,
                0,
                $vectorRef,
                $self->blockCentreXPosPixels,
                $self->blockCentreYPosPixels,
                $self->borderCornerXPosPixels,
                $self->borderCornerYPosPixels,
            );

            # Find the coordinates of the pixel at the edge of the gridblock, which is intersected
            #   by the cardinal exit
            ($blockEdgeXPosPixels, $blockEdgeYPosPixels) = $self->getBlockEdge(
                0,
                0,
                $vectorRef,
                $self->blockCentreXPosPixels,
                $self->blockCentreYPosPixels,
            );

            # Store the coordinates as a list reference
            $uncertainExitHash{$key} = [
                $exitStartXPosPixels,
                $exitStartYPosPixels,
                $blockEdgeXPosPixels,
                $blockEdgeYPosPixels,
            ];
        }

        # Pre-draw long exits (used by 1-way and 2-way exits; drawn the same way, regardless of the
        #   value of $mode)
        foreach my $key (keys %vectorHash) {

            my (
                $vectorRef, $oppCardinalDir, $oppVectorRef, $exitStartXPosPixels,
                $exitStartYPosPixels, $blockEdgeXPosPixels, $blockEdgeYPosPixels,
                $exitStopXPosPixels, $exitStopYPosPixels,
            );

            # Get the key's corresponding value
            $vectorRef = $vectorHash{$key};
            # Get the opposite cardinal direction (e.g. 'north' > 'south')
            $oppCardinalDir = $oppDirHash{$key};
            # Get the exit's vector from the centre of the arrival room (should be equal and
            #   opposite to $vectorRef)
            $oppVectorRef = $vectorHash{$oppCardinalDir};

            # Find the coordinates of the pixel at the edge of the room, just outside the border,
            #   from which a cardinal exit starts
            ($exitStartXPosPixels, $exitStartYPosPixels) = $self->getExitStart(
                0,
                0,
                $vectorRef,
                $self->blockCentreXPosPixels,
                $self->blockCentreYPosPixels,
                $self->borderCornerXPosPixels,
                $self->borderCornerYPosPixels,
            );

            # Find the coordinates of the pixel at the edge of the gridblock, which is intersected
            #   by the cardinal exit
            ($blockEdgeXPosPixels, $blockEdgeYPosPixels) = $self->getBlockEdge(
                0,
                0,
                $vectorRef,
                $self->blockCentreXPosPixels,
                $self->blockCentreYPosPixels,
            );

            # Find the coordinates of the pixel at the edge of the destination room, just outside
            #   the border, from which a cardinal exit stops
            ($exitStopXPosPixels, $exitStopYPosPixels) = $self->getExitStart(
                0,
                0,
                $oppVectorRef,
                $self->blockCentreXPosPixels,
                $self->blockCentreYPosPixels,
                $self->borderCornerXPosPixels,
                $self->borderCornerYPosPixels,
            );

            # Store the coordinates as a list reference
            $longExitHash{$key} = [
                $exitStartXPosPixels,
                $exitStartYPosPixels,
                $blockEdgeXPosPixels,
                $blockEdgeYPosPixels,
                $exitStopXPosPixels,
                $exitStopYPosPixels,
            ];
        }

        # Pre-draw square exits (used by unallocated, impassable, broken and region exits; drawn the
        #   same way, regardless of the value of $mode)
        foreach my $key (keys %vectorHash) {

            my (
                $vectorRef, $exitStartXPosPixels, $exitStartYPosPixels, $blockEdgeXPosPixels,
                $blockEdgeYPosPixels, $squareStartXPosPixels, $squareStartYPosPixels,
                $squareStopXPosPixels, $squareStopYPosPixels, $halfHorizontalLength,
                $halfVerticalLength,
            );

            # Get the key's corresponding value
            $vectorRef = $vectorHash{$key};

            # Find the coordinates of the pixel at the edge of the room, just outside the border,
            #   from which a cardinal exit starts
            ($exitStartXPosPixels, $exitStartYPosPixels) = $self->getExitStart(
                0,
                0,
                $vectorRef,
                $self->blockCentreXPosPixels,
                $self->blockCentreYPosPixels,
                $self->borderCornerXPosPixels,
                $self->borderCornerYPosPixels,
            );

            # Find the coordinates of the pixel near (but not at) the edge of the gridblock, which
            #   is intersected by the cardinal exit
            ($blockEdgeXPosPixels, $blockEdgeYPosPixels) = $self->getNearBlockEdge(
                0,
                0,
                $vectorRef,
                $self->blockCentreXPosPixels,
                $self->blockCentreYPosPixels,
            );

            # Adjust the coordinates of the two pixels at either end of the exit, so that it's
            #   easier to draw '+' exits (and so that '+' are the same size as 'x')

            # The line (or the square for which the ends of the line are in opposite corners) must
            #   have an odd-numbered length, so that the '+' and 'x' are symmetrical
            if (
                $exitStartXPosPixels != $blockEdgeXPosPixels
                && int(($blockEdgeXPosPixels - $exitStartXPosPixels) / 2)
                    != (($blockEdgeXPosPixels - $exitStartXPosPixels) / 2)
            ) {
                # Adjust the end of the line, to make it slightly bigger
                $blockEdgeXPosPixels += $$vectorRef[0];
            }

            if (
                $exitStartYPosPixels != $blockEdgeYPosPixels
                && int(($blockEdgeYPosPixels - $exitStartYPosPixels) / 2)
                    != (($blockEdgeYPosPixels - $exitStartYPosPixels) / 2)
            ) {
                # Adjust the end of the line, to make it slightly bigger
                $blockEdgeYPosPixels += $$vectorRef[1];
            }

            # Adjust the coordinates so that the exit's start is below and to the left of the
            #   block's edge, by reversing coordinates where necessary
            if ($exitStartXPosPixels > $blockEdgeXPosPixels) {

                ($exitStartXPosPixels, $blockEdgeXPosPixels)
                    = ($blockEdgeXPosPixels, $exitStartXPosPixels);
            }
            if ($exitStartYPosPixels < $blockEdgeYPosPixels) {

                ($exitStartYPosPixels, $blockEdgeYPosPixels)
                    = ($blockEdgeYPosPixels, $exitStartYPosPixels);
            }

            # North/east/south/west
            if (
                $exitStartXPosPixels == $blockEdgeXPosPixels
                || $exitStartYPosPixels == $blockEdgeYPosPixels
            ) {

                # North/south
                if ($exitStartXPosPixels == $blockEdgeXPosPixels) {

                    # Get half the length of the exit
                    $halfHorizontalLength = abs(
                        int(($blockEdgeYPosPixels - $exitStartYPosPixels) / 2),
                    );
                    # Find the coordinates of the one corner of the square occupied by the 'x'
                    $squareStartXPosPixels = $exitStartXPosPixels - $halfHorizontalLength;
                    $squareStartYPosPixels = $exitStartYPosPixels;
                    # Find the coordinates of the opposite corner of the square
                    $squareStopXPosPixels = $blockEdgeXPosPixels + $halfHorizontalLength;
                    $squareStopYPosPixels = $blockEdgeYPosPixels;

                # East/west
                } else {

                    # Get half the length of the exit
                    $halfVerticalLength = abs(
                        int(($blockEdgeXPosPixels - $exitStartXPosPixels) / 2),
                    );
                    # Find the coordinates of the one corner of the square occupied by the 'x'
                    $squareStartXPosPixels = $exitStartXPosPixels;
                    $squareStartYPosPixels = $exitStartYPosPixels + $halfVerticalLength;
                    # Find the coordinates of the opposite corner of the square
                    $squareStopXPosPixels = $blockEdgeXPosPixels;
                    $squareStopYPosPixels = $blockEdgeYPosPixels - $halfVerticalLength;
                }

                # Store the coordinates as a list reference
                $squareExitHash{$key} = [
                    $squareStartXPosPixels,
                    $squareStartYPosPixels,
                    $squareStopXPosPixels,
                    $squareStopYPosPixels,
                ];

            } else {

                # We already have the coordinates of opposite ends of a square
                $squareExitHash{$key} = [
                    $exitStartXPosPixels,
                    $exitStartYPosPixels,
                    $blockEdgeXPosPixels,
                    $blockEdgeYPosPixels,
                ];
            }
        }

        # Store the accumulated data
        $self->ivPoke('preDrawnIncompleteExitHash', %incompleteExitHash);
        $self->ivPoke('preDrawnUncertainExitHash', %uncertainExitHash);
        $self->ivPoke('preDrawnLongExitHash', %longExitHash);
        $self->ivPoke('preDrawnSquareExitHash', %squareExitHash);

        return 1;
    }

    sub drawRoom {

        # Called by $self->doDraw or $self->startDrag
        # Draws (or redraws) a single room on the grid, in the gridblock specified by the room's
        #   ->xPosBlocks, ->yPosBlocks and ->zPosBlocks IVs
        #
        # Expected arguments
        #   $roomObj        - Blessed reference of the GA::ModelObj::Room to draw
        #   $mode           - Matches GA::Obj::WorldModel->drawExitMode or
        #                       $self->currentRegionmap->drawExitMode; set to 'no_exit',
        #                       'simple_exit' or 'complex_exit'
        #
        # Optional arguments
        #   $noEmphasisFlag - Set to TRUE when called by $self->startDrag; the room's border is not
        #                       emphasised even when GA::Obj::WorldModel->currentRoomMode is set
        #                       to 'double'
        #
        # Return values
        #   'undef' on improper arguments or if the room can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $mode, $noEmphasisFlag, $check) = @_;

        # Local variables
        my (
            $regionObj, $xPos, $yPos, $unallocatedCount, $unallocatableCount, $shadowCount,
            $regionCount, $superRegionCount, $roomTagCanvasObj,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! defined $mode || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawRoom', @_);
        }

        # Get the room's parent region
        if ($roomObj->parent) {

            $regionObj = $self->worldModelObj->ivShow('modelHash', $roomObj->parent);
        }

        # Don't draw this room if:
        #   1. It doesn't have a parent region
        #   2. Its position on the map has not been set
        #   3. The room isn't on the currently-displayed level
        #   4. The room is not in the currently-displayed region
        if (
            ! defined $roomObj->parent
            || ! $regionObj
            || ! defined $roomObj->xPosBlocks
            || ! defined $roomObj->yPosBlocks
            || ! defined $roomObj->zPosBlocks
            || $regionObj->name ne $self->currentRegionmap->name
        ) {
            return undef;
        }

        # For rooms immediately above/below the current level, call a different function to draw a
        #   room echo (if allowed)
        if ($self->worldModelObj->drawRoomEchoFlag) {

            if ($roomObj->zPosBlocks == ($self->currentRegionmap->currentLevel + 1)) {

                # Before drawing the canvas object for the echo, destroy any existing canvas objects
                #   from the last time the room was drawn
                $self->deleteCanvasObj('room', $roomObj);

                return $self->drawRoomEcho($roomObj, 1);

            } elsif ($roomObj->zPosBlocks == ($self->currentRegionmap->currentLevel - 1)) {

                # Before drawing the canvas object for the echo, destroy any existing canvas objects
                #   from the last time the room was drawn
                $self->deleteCanvasObj('room', $roomObj);

                return $self->drawRoomEcho($roomObj, -1);
            }
        }

        # Otherwise, only draw a room on this level
        if ($roomObj->zPosBlocks != $self->currentRegionmap->currentLevel) {

            return undef;
        }

        # Before drawing the canvas objects that make up this room, destroy any existing canvas
        #   objects from the last time the room was drawn
        $self->deleteCanvasObj('room', $roomObj);

        # Get the position of $roomObj's gridblock
        $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
        $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

        # Draw the room's border and interior
        $self->drawRoomBox(
            $mode,
            $roomObj,
            $self->borderCornerXPosPixels + $xPos,
            $self->borderCornerYPosPixels + $yPos,
            $noEmphasisFlag,
        );

        # Check each exit, compiling various counts. If we're allowed to, draw the exits, too
        if ($roomObj->exitNumHash) {

            $unallocatedCount = 0;
            $unallocatableCount = 0;
            $shadowCount = 0;
            $regionCount = 0;
            $superRegionCount = 0;

            OUTER: foreach my $number ($roomObj->ivValues('exitNumHash')) {

                my $exitObj = $self->worldModelObj->ivShow('exitModelHash', $number);
                if ($exitObj) {

                    # Keep count of the total number of unallocated exits, and the number of
                    #   unallocatable exits - we'll need the counts later in this function
                    if (
                        $exitObj->drawMode eq 'temp_alloc'
                        || $exitObj->drawMode eq 'temp_unalloc'
                    ) {
                        $unallocatedCount++;
                        if ($exitObj->drawMode eq 'temp_unalloc') {

                            $unallocatableCount++;
                        }
                    }

                    # Likewise, we keep count of the number of exits with shadow exits
                    if ($exitObj->shadowExit) {

                        $shadowCount++;
                    }

                    # And we also keep count of region/super region exits
                    if ($exitObj->superFlag) {

                        $superRegionCount++;
                    }

                    if ($exitObj->regionFlag) {

                        $regionCount++;
                    }

                    # Draw the exit. In $mode 'no_exit', we only draw exits whose ->mapDir is 'up'
                    #   or 'down'
                    if (
                        $mode ne 'no_exit'
                        || (
                            $exitObj->mapDir
                            && ($exitObj->mapDir eq 'up' || $exitObj->mapDir eq 'down')
                        )
                    ) {
                        $self->drawExit($exitObj, $mode, $roomObj, $regionObj);
                    }

                    # Draw the exit tag, if any, but not in mode 'no_exit'
                    if ($mode ne 'no_exit' && $exitObj->exitTag) {

                        $self->drawExitTag($exitObj, $roomObj, $regionObj);
                    }
                }
            }
        }

        # Draw information displayed in the room's interior. If ->roomInteriorMode is set to
        #   'shadow_count', $unallocatedCount and $shadowCount are displayed; otherwise, these
        #   values are ignored and some other values are displayed
        if ($self->worldModelObj->roomInteriorMode ne 'none') {

            $self->drawRoomInteriorInfo(
                $roomObj,
                $self->borderCornerXPosPixels + $xPos,
                $self->borderCornerYPosPixels + $yPos,
                $unallocatedCount,
                $shadowCount,
                $regionCount,
                $superRegionCount,
            );
        }

        # If there are any unallocatable exits, the number is drawn in the bottom-centre of the
        #   room box
        if ($unallocatableCount) {

            $self->drawUnallocatableCount($roomObj, $unallocatableCount);
        }

        # Draw the room tag, if there is one
        if ($roomObj->roomTag) {

            # Draw the room tag
            $self->drawRoomTag(
                $roomObj,
                $xPos,
                $yPos,
            );
        }

        # Draw the room guild, if there is one
        if ($roomObj->roomGuild) {

            # Draw the room guild
            $self->drawRoomGuild(
                $roomObj,
                $xPos,
                $yPos,
            );
        }

        return 1;
    }

    sub drawExit {

        # Called by $self->doDraw or $self->drawRoom
        # Draws a single exit (and its exit tag, if necessary)
        #
        # Expected arguments
        #   $exitObj    - Blessed reference of the GA::Obj::Exit to draw
        #   $mode       - Matches GA::Obj::WorldModel->drawExitMode or
        #                   $self->currentRegionmap->drawExitMode; set to 'no_exit', 'simple_exit'
        #                   or 'complex_exit'
        #
        # Optional arguments
        #   $roomObj    - The parent room object, if known ('undef' otherwise)
        #   $regionObj  - The parent room's region, if known ('undef' otherwise)
        #
        # Return values
        #   'undef' on improper arguments or if the exit can't be drawn
        #   1 otherwise

        my ($self, $exitObj, $mode, $roomObj, $regionObj, $check) = @_;

        # Local variables
        my $twinExitObj;

        # Check for improper arguments
        if (! defined $exitObj || ! defined $mode || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawExit', @_);
        }

        # Get the parent room, if not specified by the calling function
        if (! $roomObj) {

            $roomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->parent);
        }

        # Get the room's region,  if not specified by the calling function
        if (! $regionObj && $roomObj->parent) {

            $regionObj = $self->worldModelObj->ivShow('modelHash', $roomObj->parent);
        }

        # Get the twin exit, if there is one
        if ($exitObj->twinExit) {

            $twinExitObj = $self->worldModelObj->ivShow('exitModelHash', $exitObj->twinExit);
        }

        # Don't draw this exit if:
        #   1. It has already been drawn during this drawing cycle (initiated by $self->doDraw)
        #   2. The exit doesn't have a parent room, or the parent room doesn't have a parent region
        #   3. The exit isn't on the current regionmap and current level
        #   4. It has a twin exit which has already been drawn - unless it's a region or normal
        #       (un-bent) broken exit, in which case we must draw both the exit and its twin during
        #       a drawing cycle, and unless it's an up/down exit, in which case we must draw it,
        #       in case the twin exit (which might already have been drawn) is in the 'east'
        #       direction, or something
        #   5. The exit has a shadow exit (e.g. 'enter cave' which leads to the same room as the
        #       exit 'west'; only the exit 'west' should be drawn)
        if (
            $self->ivExists('drawCycleExitHash', $exitObj->number)
            || ! $roomObj
            || ! $regionObj
            || $regionObj->name ne $self->currentRegionmap->name
            || $roomObj->zPosBlocks != $self->currentRegionmap->currentLevel
            || (
                $twinExitObj
                && $self->ivExists('drawCycleExitHash', $twinExitObj->number)
                && $exitObj->mapDir ne 'up'
                && $exitObj->mapDir ne 'down'
                && (
                    ! $exitObj->brokenFlag
                    || $exitObj->bentFlag
                ) && ! $exitObj->regionFlag
            ) || $exitObj->shadowExit
        ) {
            return undef;
        }

        # Before drawing the canvas objects that make up this exit, destroy any existing canvas
        #   objects from the last time the exit was drawn
        $self->deleteCanvasObj('exit', $exitObj);
        # The canvas objects for the exit may have been drawn associated with $exitObj, or with its
        #   twin exit (if any); make sure those canvas objects are destroyed, too (except for
        #   normal broken exits, region exits and impassable exits)
        if (
            $twinExitObj
            && (
                (! $twinExitObj->brokenFlag || $twinExitObj->bentFlag)
                && ! $twinExitObj->regionFlag
                && ! $twinExitObj->impassFlag
            )
        ) {
            $self->deleteCanvasObj('exit', $twinExitObj);
        }

        # Draw the exit
        if (
            (
                $exitObj->impassFlag
                || ($twinExitObj && $twinExitObj->impassFlag)
            ) && $self->worldModelObj->drawOrnamentsFlag
        ) {
            # It's an impassable exit. The impassable ornament and the exit are drawn together by
            #   this one function
            $self->drawImpassableExit($roomObj, $exitObj, $twinExitObj);

        } elsif ($exitObj->brokenFlag) {

            # It's a broken exit
            if ($exitObj->bentFlag) {
                $self->drawBentExit($roomObj, $exitObj, $mode, $twinExitObj);
            } else {
                $self->drawBrokenExit($roomObj, $exitObj);
            }

        } elsif ($exitObj->regionFlag) {

            # It's a region exit
            $self->drawRegionExit($roomObj, $exitObj);

        } elsif ($exitObj->drawMode eq 'temp_alloc') {

            # It's an unallocated exit, temporarily allocated to a primary direction
            # NB Unallocated exits, not temporarily allocated to a primary direction because none
            #   are available (->drawMode is 'temp_unalloc') are dealth with by ->drawRoom
            $self->drawUnallocatedExit($roomObj, $exitObj);

        } elsif ($exitObj->destRoom) {

            if ($exitObj->twinExit) {

                # If it's a two-way exit (we can come back in the opposite direction)
                $self->drawTwoWayExit($roomObj, $exitObj, $mode);

            } elsif ($exitObj->retraceFlag) {

                # It's a retracing exit (leading back to the same room)
                $self->drawRetracingExit($roomObj, $exitObj);

            } elsif ($exitObj->oneWayFlag) {

                # It's a one-way exit
                $self->drawOneWayExit($roomObj, $exitObj);

            } elsif ($exitObj->randomType ne 'none') {

                # It's a random exit (leading to a random room)
                $self->drawRandomExit($roomObj, $exitObj);

            } else {

                # It's an uncertain exit - we know we can go 'north' from A to B, but we don't yet
                #   know if we can go 'south' from B to A
                $self->drawUncertainExit($roomObj, $exitObj);
            }

        } elsif ($exitObj->randomType ne 'none') {

            # It's a random exit (leads to a random location)
            $self->drawRandomExit($roomObj, $exitObj);

        } else {

            # We don't know where this exit is going. Draw an incomplete exit (almost to
            #   the edge of the room's gridblock)
            $self->drawIncompleteExit($roomObj, $exitObj);
        }

        # Record the fact that we've drawn this exit, so that we don't draw it (or its twin) again
        #   during the current drawing cycle
        # Exceptions: draw both and exit and its twin if they are broken or regions exits

        $self->ivAdd('drawCycleExitHash', $exitObj->number, $exitObj);
        if ($twinExitObj && ! $twinExitObj->brokenFlag && ! $twinExitObj->regionFlag) {

            $self->ivAdd(
                'drawCycleExitHash',
                $twinExitObj->number,
                $twinExitObj,
            );

            # Delete the twin's canvas object (if it has one), so that $exitObj isn't drawn on
            #   top of its twin exit
            $self->deleteCanvasObj('exit', $twinExitObj);
        }

        # Also update the regionmap's hash of drawn exits
        $self->currentRegionmap->storeExit($exitObj);

        # Draw the exit tag, if any, but not in mode 'no_exit'
        if ($mode ne 'no_exit' && $exitObj->exitTag) {

            $self->drawExitTag($exitObj, $roomObj, $regionObj);
        }

        return 1;
    }

    sub drawRoomTag {

        # Called by $self->doDraw or $self->drawRoom
        # Draws (or redraws) a room tag, close to the room itself
        #
        # Expected arguments
        #   $roomObj    - The GA::ModelObj::Room whose tag is being drawn
        #
        # Optional arguments
        #   $blockCornerXPosPixels, $blockCornerYPosPixels
        #               - Coordinates of the pixel at the top-left corner of the room's gridblock,
        #                   if known (both set to 'undef', otherwise)
        #
        # Return values
        #   'undef' on improper arguments or if the room tag can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $blockCornerXPosPixels, $blockCornerYPosPixels, $check) = @_;

        # Local variables
        my ($regionObj, $posnListRef, $text, $newObj, $adjust, $posn);

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawRoomTag', @_);
        }

        # Get the room's parent region
        if ($roomObj->parent) {

            $regionObj = $self->worldModelObj->ivShow('modelHash', $roomObj->parent);
        }

        # Don't draw this room tag if:
        #   1. The room doesn't have a parent region
        #   2. The room's position on the map has not been set
        #   3. The room isn't on the currently-displayed level
        #   4. The room is not in the currently-displayed region
        #   5. The room doesn't have a room tag set
        if (
            ! defined $roomObj->parent
            || ! $regionObj
            || ! defined $roomObj->xPosBlocks
            || ! defined $roomObj->yPosBlocks
            || ! defined $roomObj->zPosBlocks
            || $roomObj->zPosBlocks != $self->currentRegionmap->currentLevel
            || $regionObj->name ne $self->currentRegionmap->name
            || ! $roomObj->roomTag
        ) {
            return undef;
        }

        # Before drawing the canvas objects that make up this room tag, destroy any existing canvas
        #   objects from the last time the room tag was drawn
        $self->deleteCanvasObj('room_tag', $roomObj);

        # Get the coordinates of the room's gridblock, if they weren't specified by the calling
        #   function
        if (! defined $blockCornerXPosPixels) {

            # Find the coordinates of the pixel occupying the top-left corner of the block
            ($blockCornerXPosPixels, $blockCornerYPosPixels) = $self->getBlockCorner(
                $roomObj->xPosBlocks,
                $roomObj->yPosBlocks,
            );
        }

        # The room tag is drawn above where the 'north' exit would be drawn, immediately above the
        #   room. Find the hypothetical exit's position
        $posnListRef = $self->ivShow('preDrawnIncompleteExitHash', 'north');

        # Set capitalisation
        if ($self->worldModelObj->capitalisedRoomTagFlag) {

            # Room tags displayed in caps
            $text = uc($roomObj->roomTag);

        } else {

            # Not displayed in caps
            $text = $roomObj->roomTag;
        }

        # Draw the canvas object
        $newObj = Gnome2::Canvas::Item->new(
            $self->canvasRoot,
            'Gnome2::Canvas::Text',
            x => ($blockCornerXPosPixels + $$posnListRef[2] + $roomObj->roomTagXOffset),
            y => ($blockCornerYPosPixels + $$posnListRef[3] + $roomObj->roomTagYOffset),
            fill_color => $self->getRoomTagColour($roomObj),
            font => $self->worldModelObj->mapFont,
            size => int($self->drawMapTextSize * $self->worldModelObj->roomTagRatio),
            anchor => 'GTK_ANCHOR_CENTER',   # Default position is at the hypothetical exit
            text => $text,
        );

        # Set the object's position in the canvas drawing stack
        $self->setTagLevel($newObj);

        # Set up the event handler for the canvas object
        $self->setupCanvasObjEvent('room_tag', $newObj, $roomObj);

        # Store the canvas object
        $posn = $roomObj->xPosBlocks . '_' . $roomObj->yPosBlocks . '_' . $roomObj->zPosBlocks;
        $self->ivAdd('drawnRoomTagHash', $posn, [$newObj]);

        return 1;
    }

    sub drawRoomGuild {

        # Called by $self->doDraw or $self->drawRoom
        # Draws (or redraws) a room guild, close to the room itself
        #
        # Expected arguments
        #   $roomObj    - The GA::ModelObj::Room whose tag is being drawn
        #
        # Optional arguments
        #   $blockCornerXPosPixels, $blockCornerYPosPixels
        #               - Coordinates of the pixel at the top-left corner of the room's gridblock,
        #                   if known (both set to 'undef', otherwise)
        #
        # Return values
        #   'undef' on improper arguments or if the room guild can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $blockCornerXPosPixels, $blockCornerYPosPixels, $check) = @_;

        # Local variables
        my ($regionObj, $posnListRef, $newObj, $adjust, $posn);

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawRoomGuild', @_);
        }

        # Get the room's parent region
        if ($roomObj->parent) {

            $regionObj = $self->worldModelObj->ivShow('modelHash', $roomObj->parent);
        }

        # Don't draw this room guild if:
        #   1. The room doesn't have a parent region
        #   2. The room's position on the map has not been set
        #   3. The room isn't on the currently-displayed level
        #   4. The room is not in the currently-displayed region
        #   5. The room doesn't have a room guild set
        if (
            ! defined $roomObj->parent
            || ! $regionObj
            || ! defined $roomObj->xPosBlocks
            || ! defined $roomObj->yPosBlocks
            || ! defined $roomObj->zPosBlocks
            || $roomObj->zPosBlocks != $self->currentRegionmap->currentLevel
            || $regionObj->name ne $self->currentRegionmap->name
            || ! $roomObj->roomGuild
        ) {
            return undef;
        }

        # Before drawing the canvas objects that make up this room guild, destroy any existing
        #   canvas objects from the last time the room guild was drawn
        $self->deleteCanvasObj('room_guild', $roomObj);

        # Get the coordinates of the room's gridblock, if they weren't specified by the calling
        #   function
        if (! defined $blockCornerXPosPixels) {

            # Find the coordinates of the pixel occupying the top-left corner of the block
            ($blockCornerXPosPixels, $blockCornerYPosPixels) = $self->getBlockCorner(
                $roomObj->xPosBlocks,
                $roomObj->yPosBlocks,
            );
        }

        # The room guild is drawn above where the 'south' exit would be drawn, immediately below the
        #   room. Find the hypothetical exit's position
        $posnListRef = $self->ivShow('preDrawnIncompleteExitHash', 'south');

        # Draw the canvas object
        $newObj = Gnome2::Canvas::Item->new(
            $self->canvasRoot,
            'Gnome2::Canvas::Text',
            x => ($blockCornerXPosPixels + $$posnListRef[2] + $roomObj->roomGuildXOffset),
            y => ($blockCornerYPosPixels + $$posnListRef[3] + $roomObj->roomGuildYOffset),
            fill_color => $self->getRoomGuildColour($roomObj),
            font => $self->worldModelObj->mapFont,
            size => int($self->drawMapTextSize * $self->worldModelObj->roomGuildRatio),
            anchor => 'GTK_ANCHOR_CENTER',  # Default position is at the hypothetical exit
            text => $roomObj->roomGuild,
        );

        # Set the object's position in the canvas drawing stack
        $self->setTagLevel($newObj);

        # Set up the event handler for the canvas object
        $self->setupCanvasObjEvent('room_guild', $newObj, $roomObj);

        # Store the canvas object
        $posn = $roomObj->xPosBlocks . '_' . $roomObj->yPosBlocks . '_' . $roomObj->zPosBlocks;
        $self->ivAdd('drawnRoomGuildHash', $posn, [$newObj]);

        return 1;
    }

    sub drawExitTag {

        # Called by $self->doDraw, ->drawRoom and ->drawExit
        # Draws (or redraws) an exit tag, close to the exit itself
        #
        # Expected arguments
        #   $exitObj    - Blessed reference of the GA::Obj::Exit to draw
        #
        # Optional arguments
        #   $roomObj    - The parent room object, if known ('undef' otherwise)
        #   $regionObj  - The parent room's region, if known ('undef' otherwise)
        #
        # Return values
        #   'undef' on improper arguments or if the room tag can't be drawn
        #   1 otherwise

        my ($self, $exitObj, $roomObj, $regionObj, $check) = @_;

        # Local variables
        my ($posnListRef, $colour, $xPos, $yPos, $newObj);

        # Check for improper arguments
        if (! defined $exitObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawExitTag', @_);
        }

        # Get the parent room, if not specified by the calling function
        if (! $roomObj) {

            $roomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->parent);
        }

        # Get the room's region,  if not specified by the calling function
        if (! $regionObj && $roomObj->parent) {

            $regionObj = $self->worldModelObj->ivShow('modelHash', $roomObj->parent);
        }

        # Don't draw this exit tag if:
        #   1. The exit doesn't have a parent room, or the parent room doesn't have a parent region
        #   2. The exit isn't on the current regionmap and current level
        #   3. The exit has a shadow exit (e.g. 'enter cave' which leads to the same room as the
        #       exit 'west'; only the exit 'west' should be drawn)
        #   4. The exit hasn't been allocated a primary direction (stored in ->mapDir)
        #   5. The exit doesn't have an exit tag set
        if (
            ! $roomObj
            || ! $regionObj
            || $regionObj->name ne $self->currentRegionmap->name
            || $roomObj->zPosBlocks != $self->currentRegionmap->currentLevel
            || $exitObj->shadowExit
            || ! $exitObj->mapDir
            || ! $exitObj->exitTag
        ) {
            return undef;
        }

        # Before drawing the canvas objects that make up this exit tag, destroy any existing canvas
        #   objects from the last time the exit tag was drawn
        $self->deleteCanvasObj('exit_tag', $exitObj);

        # Find the exit's position, if it had been drawn as an uncertain exit. For exits drawn as
        #   up/down, draw the exit tag as if it were at the north/south exit, by default
        if ($exitObj->mapDir eq 'up') {
            $posnListRef = $self->ivShow('preDrawnUncertainExitHash', 'north');
        } elsif ($exitObj->mapDir eq 'down') {
            $posnListRef = $self->ivShow('preDrawnUncertainExitHash', 'south');
        } else {
            $posnListRef = $self->ivShow('preDrawnUncertainExitHash', $exitObj->mapDir);
        }

        # Decide which colour to use
        $colour = $self->getExitTagColour($exitObj);

        # Get the position of $roomObj's gridblock
        $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
        $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

        # Draw the canvas object
        $newObj = Gnome2::Canvas::Item->new(
            $self->canvasRoot,
            'Gnome2::Canvas::Text',
            x => ($$posnListRef[2] + $xPos + $exitObj->exitTagXOffset),
            y => ($$posnListRef[3] + $yPos + $exitObj->exitTagYOffset),
            fill_color => $colour,
            font => $self->worldModelObj->mapFont,
            size => int($self->drawMapTextSize * $self->worldModelObj->exitTagRatio),
            anchor => $self->ivShow('constGtkAnchorHash', $exitObj->mapDir), # e.g. 'GTK_ANCHOR_S'
            text => $exitObj->exitTag,
        );

        # Set the object's position in the canvas drawing stack
        $self->setTagLevel($newObj);

        # Set up the event handler for the canvas object
        $self->setupCanvasObjEvent('exit_tag', $newObj, $exitObj);

        # Store the canvas object
        $self->ivAdd('drawnExitTagHash', $exitObj->number, [$newObj]);

        return 1;
    }

    sub drawLabel {

        # Called by $self->doDraw
        # Draws (or redraws) a map label
        #
        # Expected arguments
        #   $labelObj   - The GA::Obj::MapLabel being drawn
        #
        # Return values
        #   'undef' on improper arguments or if the label can't be drawn
        #   1 otherwise

        my ($self, $labelObj, $check) = @_;

        # Local variables
        my $newObj;

        # Check for improper arguments
        if (! defined $labelObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawLabel', @_);
        }

        # Don't draw this room tag if:
        #   1. The label's position on the map has not been set
        #   2. The label isn't on the currently-display level
        #   3. The label isn't on the currently-display region
        #   4. The label doesn't contain any text
        if (
            ! defined $labelObj->xPosPixels
            || ! defined $labelObj->yPosPixels
            || ! defined $labelObj->level
            || $labelObj->level != $self->currentRegionmap->currentLevel
            || ! defined $labelObj->region
            || $labelObj->region ne $self->currentRegionmap->name
            || ! defined $labelObj->name
            || $labelObj->name eq ''
        ) {
            return undef;
        }

        # Before drawing the canvas objects that make up this label, destroy any existing canvas
        #   objects from the last time the label was drawn
        $self->deleteCanvasObj('label', $labelObj);

        # Draw the canvas object
        $newObj = Gnome2::Canvas::Item->new(
            $self->canvasRoot,
            'Gnome2::Canvas::Text',
            x => $labelObj->xPosPixels,
            y => $labelObj->yPosPixels,
            fill_color => $self->getLabelColour($labelObj),
            font => $self->worldModelObj->mapFont,
            size => int(
                $self->drawMapTextSize * $self->worldModelObj->labelRatio * $labelObj->relSize
            ),
            anchor => 'GTK_ANCHOR_W',   # Draw text to the right of the original mouse click
            text => $labelObj->name,
        );

        # Set the object's position in the canvas drawing stack
        $self->setLabelLevel($newObj);

        # Set up the event handler for the canvas object
        $self->setupCanvasObjEvent('label', $newObj, $labelObj);

        # Store the canvas object
        $self->ivAdd('drawnLabelHash', $labelObj->number, [$newObj]);

        return 1;
    }

    # Graphical operations - map object stack functions

    sub setMapLevel {

        # Called by $self->resetMap (only)
        # Sets the map background's position in the canvas drawing stack
        #
        # The canvas drawing stack is organised like this:
        #   6   - labels and draggable exits (placed at the top of the stack)
        #   5   - room tags, room guilds and exit tags (lowered from the bottom)
        #   4   - exits and exit ornaments (lowered from the bottom)
        #   3   - room interior text (raised from the bottom)
        #   2   - room boxes (raised from the bottom)
        #   1   - room echose and fake room boxes (raised from the bottom)
        #   0   - map background (placed at the bottom of the stack)
        #
        # Expected arguments
        #   $canvasObj      - The canvas object which has just been drawn
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $canvasObj, $check) = @_;

        # Check for improper arguments
        if (! defined $canvasObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setMapLevel', @_);
        }

        $canvasObj->lower_to_bottom();

        return 1;
    }

    sub setEchoLevel {

        # Called by various drawing functions
        # Sets the canvas object's position in the canvas drawing stack
        #
        # Room echos and fake room boxes are drawn at level 1 (above the map background)
        #
        # Expected arguments
        #   $canvasObj      - The canvas object which has just been drawn
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $canvasObj, $roomObj, $check) = @_;

        # Check for improper arguments
        if (! defined $canvasObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setEchoLevel', @_);
        }

        $canvasObj->lower_to_bottom();
        $canvasObj->raise(1);

        return 1;
    }

    sub setRoomLevel {

        # Called by various drawing functions
        # Sets the canvas object's position in the canvas drawing stack
        #
        # Room boxes are drawn at level 2 (above the map background, room echos and fake room boxes)
        #
        # Expected arguments
        #   $canvasObj      - The canvas object which has just been drawn
        #
        # Optional arguments
        #   $level          - If this function has just been called by the same room-drawing
        #                       function, we don't need to calculate the stack position; we can use
        #                       the same one. Set to 'undef' otherwise
        #
        # Return values
        #   'undef' on improper arguments
        #   Otherwise returns the canvas object's drawing stack position, in case the same calling
        #       function wants to call this function again

        my ($self, $canvasObj, $level, $roomObj, $check) = @_;

        # Check for improper arguments
        if (! defined $canvasObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setRoomLevel', @_);
        }

        if (! defined $level) {

            $level = $self->ivPairs('drawnRoomEchoHash') + 1;
        }

        $canvasObj->lower_to_bottom();
        $canvasObj->raise($level);

        return $level;
    }

    sub setTextLevel {

        # Called by various drawing functions
        # Sets the canvas object's position in the canvas drawing stack
        #
        # Room interior text is drawn at level 3 (above the map background, room echos, fake room
        #   boxes and real room boxes)
        #
        # Expected arguments
        #   $canvasObj      - The canvas object which has just been drawn
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $canvasObj, $check) = @_;

        # Check for improper arguments
        if (! defined $canvasObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setTextLevel', @_);
        }

        $canvasObj->lower_to_bottom();
        $canvasObj->raise(
            $self->ivPairs('drawnRoomEchoHash')
            + $self->ivPairs('drawnRoomHash')
            + $self->ivPairs('dummyRoomHash')
            + 1
        );

        return 1;
    }

    sub setExitLevel {

        # Called by various drawing functions
        # Sets the canvas object's position in the canvas drawing stack
        #
        # Exits and exit ornaments are drawn at level 4 (below labels, draggable exits, room tags,
        #   room guilds and exit tags)
        #
        # Expected arguments
        #   $canvasObj      - The canvas object which has just been drawn
        #
        # Optional arguments
        #   $level          - If this function has just been called by the same exit-drawing
        #                       function, we don't need to calculate the stack position; we can use
        #                       the same one. Set to 'undef' otherwise
        #
        # Return values
        #   'undef' on improper arguments
        #   Otherwise returns the canvas object's drawing stack position, in case the same calling
        #       function wants to call this function again

        my ($self, $canvasObj, $level, $check) = @_;

        # Check for improper arguments
        if (! defined $canvasObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setExitLevel', @_);
        }

        if (! $level) {

            $level = $self->ivPairs('drawnLabelHash') + $self->ivPairs('drawnRoomTagHash')
                        + $self->ivPairs('drawnRoomGuildHash') + $self->ivPairs('drawnExitTagHash');
        }

        $canvasObj->raise_to_top();

        # (The call to $canvasObj->lower won't accept a value of 0, so only call it for a positive
        #   value)
        if ($level) {

            $canvasObj->lower($level);
        }

        return $level;
    }

    sub setTagLevel {

        # Called by various drawing functions
        # Sets the canvas object's position in the canvas drawing stack
        #
        # Room tags, room guilds and exit tags are drawn at level 5 (below labels and draggable
        #   exits)
        #
        # Expected arguments
        #   $canvasObj      - The canvas object which has just been drawn
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $canvasObj, $check) = @_;

        # Local variables
        my $level;

        # Check for improper arguments
        if (! defined $canvasObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setTagLevel', @_);
        }

        $canvasObj->raise_to_top();

        # (The call to $canvasObj->lower won't accept a value of 0, so only call it for a positive
        #   value)
        $level = $self->ivPairs('drawnLabelHash');
        if ($level) {

            $canvasObj->lower($level);
        }

        return 1;

    }

    sub setLabelLevel {

        # Called by various drawing functions
        # Sets the canvas object's position in the canvas drawing stack
        #
        # Labels (and draggable exits) are drawn at level 6 (above everything else)
        #
        # Expected arguments
        #   $canvasObj      - The canvas object which has just been drawn
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $canvasObj, $check) = @_;

        # Check for improper arguments
        if (! defined $canvasObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setLabelLevel', @_);
        }

        $canvasObj->raise_to_top();

        return 1;
    }

    # Graphical operations - map object support functions

    sub drawRoomBox {

        # Called by $self->drawRoom to draw the room box in two colours - a border, and an interior
        #
        # Expected arguments
        #   $mode
        #       - Matches GA::Obj::WorldModel->drawExitMode
        #           / $self->currentRegionmap->drawExitMode; set to 'no_exit', 'simple_exit' or
        #           'complex_exit'
        #   $roomObj
        #       - Blessed reference of the GA::ModelObj::Room being drawn
        #   $borderCornerXPosPixels, $borderCornerYPosPixels
        #       - Coordinates of the pixel at the top-left corner of the room's border
        #
        # Optional arguments
        #   $noEmphasisFlag
        #       - Set to TRUE when called by $self->drawRoom was itself called by $self->startDrag;
        #           the room's border is not emphasised even when
        #           GA::Obj::WorldModel->currentRoomMode is set to 'double'
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my (
            $self, $mode, $roomObj, $borderCornerXPosPixels, $borderCornerYPosPixels,
            $noEmphasisFlag, $check,
        ) = @_;

        # Local variables
        my (
            $x2, $y2, $posn, $borderColour, $currentMode, $roomColour, $newObj, $newObj2,
            $posnSet,
        );

        # Check for improper arguments
        if (
            ! defined $mode || ! defined $roomObj || ! defined $borderCornerXPosPixels
            || ! defined $borderCornerYPosPixels || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawRoomBox', @_);
        }

        # In draw exit mode 'no_exit' (draw no exits), the room takes up the whole gridblock
        if ($mode eq 'no_exit') {

            # Delete 2 pixels to allow a 1-pixel border on each side of the room box; otherwise,
            #   the room's borders touch and will look like double-width lines
            $x2 = $borderCornerXPosPixels + $self->currentRegionmap->blockWidthPixels - 3;
            $y2 = $borderCornerYPosPixels + $self->currentRegionmap->blockHeightPixels - 3;

        # In draw exit mode 'simple_exit'/'complex_exit', the room takes up the middle part of the
        #   gridblock
        } else {

            $x2 = $borderCornerXPosPixels + $self->currentRegionmap->roomWidthPixels - 1;
            $y2 = $borderCornerYPosPixels + $self->currentRegionmap->roomHeightPixels - 1;
        }

        # Get the border colour. If $roomObj is the current, last known or ghost room, $currentMode
        #   is set to the current value of GA::Obj::WorldModel->currentRoomMode; for all other
        #   rooms it is set to 'single'
        ($borderColour, $currentMode) = $self->getBorderColour($roomObj);
        # Get the interior colour
        if ($currentMode eq 'interior') {

            # (Instead of changing the border colour, fill in the room interior)
            $roomColour = $borderColour;
            $borderColour = $self->worldModelObj->borderColour;

        } else {

            $roomColour = $self->getRoomColour($roomObj);
        }

        # Draw the canvas object
        $newObj = Gnome2::Canvas::Item->new(
            $self->canvasRoot,
            'Gnome2::Canvas::Rect',
            x1 => $borderCornerXPosPixels,
            y1 => $borderCornerYPosPixels,
            x2 => $x2,
            y2 => $y2,
            outline_color => $borderColour,
            fill_color => $roomColour,
        );

        # For current/last known/ghost rooms, the border is drawn 2 pixels wide. In fact, we draw
        #   another box, just below the first one (unless the original call to $self->drawRoom was
        #   from ->startDrag, in which case $noEmphasisFlag is set)
        if ($currentMode eq 'double' && ! $noEmphasisFlag) {

            $newObj2 = Gnome2::Canvas::Item->new(
                $self->canvasRoot,
                'Gnome2::Canvas::Rect',
                x1 => ($borderCornerXPosPixels - 1),
                y1 => ($borderCornerYPosPixels - 1),
                x2 => ($x2 + 1),
                y2 => ($y2 + 1),
                outline_color => $borderColour,
            );
        }

        # Set the objects' positions in the canvas drawing stack
        if ($newObj2) {

            $posnSet = $self->setRoomLevel($newObj2);


            # Set up the event handler for the canvas object
            $self->setupCanvasObjEvent('room', $newObj2, $roomObj);
        }

        $self->setRoomLevel($newObj, $posnSet);         # $posnSet 'undef' if room not emphasised
        # Set up the event handler for the canvas object
        $self->setupCanvasObjEvent('room', $newObj, $roomObj);

        # Store the canvas object(s)
        $posn = $roomObj->xPosBlocks . '_' . $roomObj->yPosBlocks . '_' . $roomObj->zPosBlocks;

        if (! $newObj2) {
            $self->ivAdd('drawnRoomHash', $posn, [$newObj]);
        } else {
            $self->ivAdd('drawnRoomHash', $posn, [$newObj, $newObj2]);
            $self->ivAdd('dummyRoomHash', $posn, [$newObj, $newObj2]);  # Subset of ->drawnRoomHash
        }

        return 1;
    }

    sub drawFakeRoomBox {

        # Called by $self->startDrag
        # Draws a fake room on the grid, at the gridblock occupied by the specified room which is
        #   involved in a drag operation
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the GA::ModelObj::Room that's about to be dragged
        #
        # Return values
        #   'undef' on improper arguments or if the fake room can't be drawn
        #   Otherwise, returns the Gnome2::Canvas::Rect drawn

        my ($self, $roomObj, $check) = @_;

        # Local variables
        my ($regionObj, $mode, $xPos, $yPos, $x1, $y1, $x2, $y2, $newObj);

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawFakeRoomBox', @_);
        }

        # Get the dragged room's parent region
        if ($roomObj->parent) {

            $regionObj = $self->worldModelObj->ivShow('modelHash', $roomObj->parent);
        }

        # Don't draw this fake room if:
        #   1. The dragged room doesn't have a parent region
        #   2. The dragged room's position on the map has not been set
        #   3. The dragged room isn't on the currently-displayed level
        #   4. The dragged room is not in the currently-displayed region
        if (
            ! defined $roomObj->parent
            || ! $regionObj
            || ! defined $roomObj->xPosBlocks
            || ! defined $roomObj->yPosBlocks
            || ! defined $roomObj->zPosBlocks
            || $roomObj->zPosBlocks != $self->currentRegionmap->currentLevel
            || $regionObj->name ne $self->currentRegionmap->name
        ) {
            return undef;
        }

        # Get the draw exit mode in operation, which determines the size of the rooms drawn
        if ($self->worldModelObj->drawExitMode eq 'ask_regionmap') {
            $mode = $self->currentRegionmap->drawExitMode;
        } else {
            $mode = $self->worldModelObj->drawExitMode;
        }

        # Get the position of $roomObj's gridblock
        $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
        $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

        # Get the coordinates of the top-left corner of the room's border
        $x1 = $self->borderCornerXPosPixels + $xPos;
        $y1 = $self->borderCornerYPosPixels + $yPos;
        # Get the coordinates of the bottom-right corner of the room's border
        if ($mode eq 'no_exit') {

            $x2 = $x1 + $self->currentRegionmap->blockWidthPixels - 3;
            $y2 = $y1 + $self->currentRegionmap->blockHeightPixels - 3;

        } else {

            $x2 = $x1 + $self->currentRegionmap->roomWidthPixels - 1;
            $y2 = $y1 + $self->currentRegionmap->roomHeightPixels - 1;
        }

        # The fake room has a normal border colour (default black) and the same interior colour as
        #   the map itself (default cream)
        $newObj = Gnome2::Canvas::Item->new(
            $self->canvasRoot,
            'Gnome2::Canvas::Rect',
            x1 => $x1,
            y1 => $y1,
            x2 => $x2,
            y2 => $y2,
            outline_color => $self->worldModelObj->borderColour,
            fill_color => $self->worldModelObj->backgroundColour,
        );

        # Set the object's position in the canvas drawing stack
        $self->setEchoLevel($newObj);   # Same level as room echos, 1 above map background

        # The calling function stores $newObj in an IV
        return $newObj;
    }

    sub drawRoomEcho {

        # Called by $self->drawRoom
        # Draws a room echo for a room just above or just below the current level
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the GA::ModelObj::Room that's about to be dragged
        #   $echoMode   - Set to -1 if the room echo is for a room just below the current level,
        #                   set to +1 if the room echo is for a room just above the current level
        #
        # Return values
        #   'undef' on improper arguments or if the room echo can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $echoMode, $check) = @_;

        # Local variables
        my (
            $drawExitMode, $xPos, $yPos, $x1, $y1, $x2, $y2, $xMod, $yMod, $outlineColour,
            $fillColour, $posn, $newObj,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! defined $echoMode || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawRoomEcho', @_);
        }

        # Get the draw exit mode in operation, which determines the size of the rooms drawn
        if ($self->worldModelObj->drawExitMode eq 'ask_regionmap') {
            $drawExitMode = $self->currentRegionmap->drawExitMode;
        } else {
            $drawExitMode = $self->worldModelObj->drawExitMode;
        }

        # Get the position of $roomObj's gridblock
        $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
        $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

        # Get the coordinates of the top-left corner of the room's border
        $x1 = $self->borderCornerXPosPixels + $xPos;
        $y1 = $self->borderCornerYPosPixels + $yPos;
        # Get the coordinates of the bottom-right corner of the room's border
        if ($drawExitMode eq 'no_exit') {

            $x2 = $x1 + $self->currentRegionmap->blockWidthPixels - 3;
            $y2 = $y1 + $self->currentRegionmap->blockHeightPixels - 3;

        } else {

            $x2 = $x1 + $self->currentRegionmap->roomWidthPixels - 1;
            $y2 = $y1 + $self->currentRegionmap->roomHeightPixels - 1;
        }

        # A room above the current level should be drawn slightly northwest of the usual position.
        #   A room below the current level should be drawn slightly southeast
        $xMod = int($echoMode * ($self->currentRegionmap->roomWidthPixels / 5));
        $yMod = int($echoMode * ($self->currentRegionmap->roomHeightPixels / 5));

        # Set the colours to use
        if ($echoMode == 1) {

            $outlineColour = $self->worldModelObj->roomAboveColour;
            $fillColour = $self->worldModelObj->backgroundColour;

        } else {

            $outlineColour = $self->worldModelObj->roomBelowColour;
            $fillColour = $self->worldModelObj->roomBelowColour;
        }

        # Draw the room echo
        $newObj = Gnome2::Canvas::Item->new(
            $self->canvasRoot,
            'Gnome2::Canvas::Rect',
            x1 => ($x1 - $xMod),        # Use minus so that rooms above are drawn to northwest...
            y1 => ($y1 - $yMod),        # ...and rooms below are drawn to southeast
            x2 => ($x2 - $xMod),
            y2 => ($y2 - $yMod),
            outline_color => $outlineColour,
            fill_color => $fillColour,
        );

        # Set the object's position in the canvas drawing stack
        $self->setEchoLevel($newObj);

        # Set up the event handler for the canvas object. Pretend that it was a click on the map
        #   background by calling ->setupCanvasEvent, not ->setupCanvasObjEvent
        $self->setupCanvasEvent($newObj);

        # Store the canvas object(s)
        $posn = $roomObj->xPosBlocks . '_' . $roomObj->yPosBlocks . '_' . $roomObj->zPosBlocks;
        $self->ivAdd('drawnRoomEchoHash', $posn, [$newObj]);

        # The calling function stores $newObj in an IV
        return $newObj;
    }

    sub drawRoomInteriorInfo {

        # Called by $self->drawRoom
        # Draws information displayed in the room interior. Which information to display depends on
        #   GA::Obj::WorldModel->roomInteriorMode (for example, in mode 'temp_count', the number of
        #   living and non-living things in the Locator's current room is displayed)
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the GA::ModelObj::Room being drawn
        #   $borderCornerXPosPixels, $borderCornerYPosPixels
        #               - Coordinates of the pixel at the top-left corner of the room's border
        #
        # Optional arguments
        #   $unallocatedCount, $shadowCount
        #               - The number of exits in this room which are unallocated, and the number
        #                   which have have shadow exits (needed in mode 'shadow_count')
        #   $regionCount, $superRegionCount
        #               - The number of exits in this room which are region exits, and the number
        #                   which are super-region exits (needed in mode 'region_count')
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my (
            $self, $roomObj, $borderCornerXPosPixels, $borderCornerYPosPixels, $unallocatedCount,
            $shadowCount, $regionCount, $superRegionCount, $check
        ) = @_;

        # Local variables
        my (
            $mode, $livingCount, $nonLivingCount, $file, $assistedCount, $patternCount,
            @list,
        );

        # Check for improper arguments
        if (
            ! defined $roomObj || ! defined $borderCornerXPosPixels
            || ! defined $borderCornerYPosPixels || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawRoomInteriorInfo', @_);
        }

        # Import the world model's room interior mode IV
        $mode = $self->worldModelObj->roomInteriorMode;

        # Display the number of unallocated / shadow exits
        if ($mode eq 'shadow_count' && ($unallocatedCount || $shadowCount)) {

            $self->drawInteriorCounts(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
                $unallocatedCount,
                $shadowCount,
            );

        # Draw the number of region/super-region exits
        } elsif ($mode eq 'region_count' && ($regionCount || $superRegionCount)) {

            $self->drawInteriorCounts(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
                $regionCount,
                $superRegionCount,
            );

        # Draw the number of living and non-living objects contained in the room
        } elsif ($mode eq 'room_content' || $mode eq 'hidden_content') {

            # Count the number of living and non-living things
            $livingCount = 0;
            $nonLivingCount = 0;

            # Count either permanent, or only hidden, objects (which are usually also in the room's
            #   permanent object list too), depending on the value of $mode
            if ($mode eq 'room_content') {

                @list = $roomObj->ivKeys('childHash');

            } else {

                # (Mode 'hidden_content')
                @list = $roomObj->ivKeys('hiddenObjHash');
            }

            foreach my $number (@list) {

                my $obj = $self->worldModelObj->ivShow('modelHash', $number);
                if ($obj) {

                    if ($obj->aliveFlag) {
                        $livingCount++;
                    } else {
                        $nonLivingCount++;
                    }
                }
            }

            if ($livingCount || $nonLivingCount) {

                $self->drawInteriorCounts(
                    $roomObj,
                    $borderCornerXPosPixels,
                    $borderCornerYPosPixels,
                    $livingCount,
                    $nonLivingCount,
                );
            }

        # Draw the number of living and non-living (temporary) objects in the room, the last time it
        #   was visited (if known)
        } elsif ($mode eq 'temp_count') {

            # Retrieve the counts
            $livingCount = 0;
            $nonLivingCount = 0;

            if ($self->currentRegionmap->ivExists('livingCountHash', $roomObj->number)) {

                $livingCount = $self->currentRegionmap->ivShow('livingCountHash', $roomObj->number);
            }

            if ($self->currentRegionmap->ivExists('nonLivingCountHash', $roomObj->number)) {

                $nonLivingCount = $self->currentRegionmap->ivShow(
                    'nonLivingCountHash',
                    $roomObj->number,
                );
            }

            if ($livingCount || $nonLivingCount) {

                $self->drawInteriorCounts(
                    $roomObj,
                    $borderCornerXPosPixels,
                    $borderCornerYPosPixels,
                    $livingCount,
                    $nonLivingCount,
                );
            }

        # Draw the number of recognised nouns and adjectives in the room
        } elsif ($mode eq 'word_count' && ($roomObj->nounList || $roomObj->adjList)) {

            $self->drawInteriorCounts(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
                $roomObj->ivNumber('nounList'),
                $roomObj->ivNumber('adjList'),
            );

        # Draw the highest-priority room flag
        } elsif ($mode eq 'room_flag' && $roomObj->lastRoomFlag) {

            $self->drawRoomFlagText(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
                $roomObj->lastRoomFlag,
            );

        # Draw character visits
        } elsif ($mode eq 'visit_count') {

            $self->drawInteriorVisits(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
            );

        # Draw room's exclusive profiles
        } elsif ($mode eq 'profile_count' && ($roomObj->exclusiveFlag || $roomObj->exclusiveHash)) {

            $self->drawInteriorProfiles(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
            );

        # Draw room titles/verbose descriptions
        } elsif ($mode eq 'title_descrip' && ($roomObj->titleList || $roomObj->descripHash)) {

            $self->drawInteriorCounts(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
                $roomObj->ivNumber('titleList'),
                $roomObj->ivPairs('descripHash'),
            );

        # Draw assisted moves and exit patterns
        } elsif ($mode eq 'exit_pattern') {

            # Count assisted moves for each exit
            $assistedCount = 0;
            foreach my $exitNum ($roomObj->ivValues('exitNumHash')) {

                my $exitObj = $self->worldModelObj->ivShow('exitModelHash', $exitNum);

                $assistedCount += $exitObj->ivPairs('assistedHash');
            }

            # Count fail exit (etc) patterns
            $patternCount = $roomObj->ivNumber('failExitPatternList')
                                + $roomObj->ivNumber('involuntaryExitPatternList')
                                + $roomObj->ivNumber('repulseExitPatternList')
                                + $roomObj->ivNumber('specialDepartPatternList');

            $self->drawInteriorCounts(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
                $assistedCount,
                $patternCount,
            );

        # Drawm room's source code path
        } elsif ($mode eq 'source_code' && $roomObj->sourceCodePath) {

            $self->drawRoomSourceText(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
            );

        # Draw world's room vnum
        } elsif ($mode eq 'vnum' && $roomObj->ivExists('protocolRoomHash', 'vnum')) {

            # For this value only, don't worry if the number is bigger than the room box (nothing we
            #   can do about it)
            $self->drawInteriorText(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
                0,          # Top-left corner
                $roomObj->ivShow('protocolRoomHash', 'vnum'),
            );
        }

        return 1;
    }

    sub drawIncompleteExit {

        # Called by $self->drawExit to draw an exit with an unknown destination
        # The exit is drawn almost to the edge of the gridblock, so that it's clear the exit doesn't
        #   necessarily lead to adjacent rooms
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the parent GA::ModelObj::Room
        #   $exitObj    - Blessed reference of the GA::Obj::Exit being drawn
        #
        # Return values
        #   'undef' on improper arguments or if the exit can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $exitObj, $check) = @_;

        # Local variables
        my ($mapDir, $posnListRef, $colour, $xPos, $yPos, $newObj);

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawIncompleteExit', @_);
        }

        # Fetch the equivalent primary direction (the direction in which the exit is drawn on the
        #   map)
        $mapDir = $exitObj->mapDir;
        if (! $mapDir) {

            return undef;
        }

        # Draw 'up' and 'down' - the letters 'U' and 'D' in the bottom corners of the room
        if ($mapDir eq 'up' || $mapDir eq 'down') {

            $self->drawUpDown($roomObj, $exitObj);

        # Draw cardinal directions (the sixteen primary directions which aren't 'up' and 'down')
        } else {

            # Find the exit's position
            $posnListRef = $self->ivShow('preDrawnIncompleteExitHash', $mapDir);
            # Decide which colour to use
            $colour = $self->getExitColour($exitObj);

            # Get the position of $roomObj's gridblock
            $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
            $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

            # Draw the canvas object
            $newObj = Gnome2::Canvas::Item->new (
                $self->canvasRoot,
                'Gnome2::Canvas::Line',
                points => [
                    $$posnListRef[0] + $xPos,
                    $$posnListRef[1] + $yPos,
                    $$posnListRef[2] + $xPos,
                    $$posnListRef[3] + $yPos,
                ],
                fill_color => $colour,
                width_units => 1,
                cap_style => 'projecting',
            );

            # Set the object's position in the canvas drawing stack
            $self->setExitLevel($newObj);

            # Set up the event handler for the canvas object
            $self->setupCanvasObjEvent('exit', $newObj, $exitObj);

            # Store the canvas object
            $self->ivAdd('drawnExitHash', $exitObj->number, [$newObj]);

            # Draw ornaments for this exit, if there are any (and if allowed)
            if ($exitObj->ornamentFlag && $self->worldModelObj->drawOrnamentsFlag) {

                $self->drawExitOrnaments(
                    $exitObj,
                    $colour,
                    $xPos,
                    $yPos,
                    undef,      # No twin exit object
                    undef,      # No destination room
                    undef,
                    $posnListRef,
                );
            }
        }

        return 1;
    }

    sub drawUncertainExit {

        # Called by $self->drawExit to draw an uncertain exit (when we know we can go 'north' from
        #   A to B, but we don't yet know if we can go 'south' from B to A)
        # The exit is drawn longer than an incomplete exit, so we can see from the map which of the
        #   two linked exits we already know about, and which we don't
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the parent GA::ModelObj::Room
        #   $exitObj    - Blessed reference of the GA::Obj::Exit being drawn
        #
        # Return values
        #   'undef' on improper arguments or if the exit can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $exitObj, $check) = @_;

        # Local variables
        my ($mapDir, $xPos, $yPos, $posnListRef, $colour, $newObj);

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawUncertainExit', @_);
        }

        # Fetch the equivalent primary direction (the direction in which the exit is drawn on the
        #   map)
        $mapDir = $exitObj->mapDir;
        if (! $mapDir) {

            return undef;
        }

        # Draw 'up' and 'down' - the letters 'U' and 'D' in the bottom corners of the room
        if ($mapDir eq 'up' || $mapDir eq 'down') {

            $self->drawUpDown($roomObj, $exitObj);

        # Draw cardinal directions (the sixteen primary directions which aren't 'up' and 'down')
        } else {

            # Find the exit's position
            $posnListRef = $self->ivShow('preDrawnUncertainExitHash', $mapDir);
            # Decide which colour to use
            $colour = $self->getExitColour($exitObj);

            # Get the position of $roomObj's gridblock
            $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
            $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

            # Draw the canvas object
            $newObj = Gnome2::Canvas::Item->new (
                $self->canvasRoot,
                'Gnome2::Canvas::Line',
                points => [
                    $$posnListRef[0] + $xPos,
                    $$posnListRef[1] + $yPos,
                    $$posnListRef[2] + $xPos,
                    $$posnListRef[3] + $yPos,
                ],
                fill_color => $colour,
                width_units => 1,
                cap_style => 'projecting',
            );

            # Set the object's position in the canvas drawing stack
            $self->setExitLevel($newObj);

            # Set up the event handler for the canvas object
            $self->setupCanvasObjEvent('exit', $newObj, $exitObj);

            # Store the canvas object
            $self->ivAdd('drawnExitHash', $exitObj->number, [$newObj]);

            # Draw ornaments for this exit, if there are any (and if allowed)
            if ($exitObj->ornamentFlag && $self->worldModelObj->drawOrnamentsFlag) {

                $self->drawExitOrnaments(
                    $exitObj,
                    $colour,
                    $xPos,
                    $yPos,
                    undef,      # No twin exit object
                    undef,      # No destination room
                    undef,
                    $posnListRef,
                );
            }
        }

        return 1;
    }

    sub drawOneWayExit {

        # Called by $self->drawExit to draw a one-way exit. The exit is drawn all the way from one
        #   room to the other. (Also called by $self->drawBentExit in certain circumstances)
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the parent GA::ModelObj::Room
        #   $exitObj    - Blessed reference of the GA::Obj::Exit being drawn
        #
        # Return values
        #   'undef' on improper arguments or if the exit can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $exitObj, $check) = @_;

        # Local variables
        my (
            $mapDir, $oneWayDir, $posnListRef, $posnListRef2, $colour, $xPos, $yPos, $destRoomObj,
            $destXPos, $destYPos, $newObj, $newObj2, $newObj3, $alignMode, $twinExitObj,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawOneWayExit', @_);
        }

        # Fetch the equivalent primary direction (the direction in which the exit is
        #   drawn on the map)
        $mapDir = $exitObj->mapDir;
        $oneWayDir = $exitObj->oneWayDir;
        if (! $mapDir || ! $oneWayDir) {

            return undef;
        }

        # Draw 'up' and 'down' - the letters 'U' and 'D' in the bottom corners of the room
        if ($mapDir eq 'up' || $mapDir eq 'down') {

            $self->drawUpDown($roomObj, $exitObj);

        # Draw cardinal directions (the sixteen primary directions which aren't 'up' and 'down')
        } else {

            # $exitObj is supposed to touch the destination room at the point, at which a
            #   hypothetical exit in the direction $exitObj->oneWayDir is drawn (e.g. if ->mapDir
            #   is 'east', ->oneWayDir is, by default, 'west')
            # Find out if the destination room has an exit (an incomplete exit, or some other kind
            #   of exit which doesn't lead back to $roomObj) drawn in the ->oneWayDir direction
            # If so, the one-way exit is drawn to the edge of its own gridblock; if not, the
            #   one-way exit is drawn all the way to the destination room
            $destRoomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->destRoom);
            if (! $destRoomObj->ivExists('exitNumHash', $oneWayDir)) {

                # Draw this exit all the way to the destination room.

                # Find the exit's position
                $posnListRef = $self->ivShow('preDrawnLongExitHash', $mapDir);
                # Get the position of the destination room's gridblock
                $destXPos = $destRoomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
                $destYPos = $destRoomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

                # If ->mapDir and ->oneWayDir are not opposites, then we can't use
                #   $self->preDrawnLongExitHash to draw the far end of the exit
                if (
                    $axmud::CLIENT->ivShow('constOppDirHash', $mapDir) eq $oneWayDir
                ) {
                    $posnListRef2 = $self->ivShow('preDrawnLongExitHash', $oneWayDir);
                    $alignMode = 'opposite';

                } else {

                    $posnListRef2 = $self->ivShow('preDrawnUncertainExitHash', $oneWayDir);
                    $alignMode = 'not_opposite';
                }

            } else {

                # Draw this exit to the edge of its own gridblock
                # Find the exit's position (as if it were an uncertain exit)
                $posnListRef = $self->ivShow('preDrawnUncertainExitHash', $mapDir);
                $alignMode = 'edge';
            }

            # Get the position of $roomObj's gridblock
            $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
            $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;
            # Decide which colour to use
            $colour = $self->getExitColour($exitObj);

            # Draw the canvas object for the line part of the exit
            if ($alignMode eq 'opposite') {

                # Draw this exit all the way to the destination room, where $mapDir and $oneWayDir
                #   are opposites
                $newObj = Gnome2::Canvas::Item->new (
                    $self->canvasRoot,
                    'Gnome2::Canvas::Line',
                    points => [
                        $$posnListRef[0] + $xPos,
                        $$posnListRef[1] + $yPos,
                        $$posnListRef2[0] + $destXPos,
                        $$posnListRef2[1] + $destYPos,
                    ],
                    fill_color => $colour,
                    width_units => 1,
                    cap_style => 'projecting',
                );


            } elsif ($alignMode eq 'not_opposite') {

                # Draw this exit all the way to the destination room, where $mapDir and $oneWayDir
                #   are not opposites
                $newObj = Gnome2::Canvas::Item->new (
                    $self->canvasRoot,
                    'Gnome2::Canvas::Line',
                    points => [
                        $$posnListRef[0] + $xPos,
                        $$posnListRef[1] + $yPos,
                        $$posnListRef[2] + $xPos,
                        $$posnListRef[3] + $yPos,
                        $$posnListRef2[2] + $destXPos,
                        $$posnListRef2[3] + $destYPos,
                        $$posnListRef2[0] + $destXPos,
                        $$posnListRef2[1] + $destYPos,
                    ],
                    fill_color => $colour,
                    width_units => 1,
                    cap_style => 'projecting',
                );

            } else {

                # Draw this exit to the edge of its own gridblock
                $newObj = Gnome2::Canvas::Item->new (
                    $self->canvasRoot,
                    'Gnome2::Canvas::Line',
                    points => [
                        $$posnListRef[0] + $xPos,
                        $$posnListRef[1] + $yPos,
                        $$posnListRef[2] + $xPos,
                        $$posnListRef[3] + $yPos,
                    ],
                    fill_color => $colour,
                    width_units => 1,
                    cap_style => 'projecting',
                );
            }

            # Draw the canvas objects for the arrowhead part of the exit
            ($newObj2, $newObj3) = $self->drawArrowHead(
                $exitObj,
                $xPos,
                $yPos,
                $posnListRef,
                $colour,
            );

            # Set the objects' position in the canvas drawing stack (this has already been done for
            #   $newObj2 and $newObj3)
            $self->setExitLevel($newObj);

            # Set up the event handlers for the canvas objects (this has already been done for
            #   $newObj2 and $newObj3)
            $self->setupCanvasObjEvent('exit', $newObj, $exitObj);

            if ($newObj2 && $newObj3) {
                $self->ivAdd('drawnExitHash', $exitObj->number, [$newObj, $newObj2, $newObj3]);
            } else {
                $self->ivAdd('drawnExitHash', $exitObj->number, [$newObj]);
            }

            # Draw ornaments for this exit and/or the twin exit, if there are any (and if allowed)
            if ($self->worldModelObj->drawOrnamentsFlag) {

                if ($exitObj->twinExit) {

                    $twinExitObj
                        = $self->worldModelObj->ivShow('exitModelHash', $exitObj->twinExit);
                }

                if ($exitObj->ornamentFlag || ($twinExitObj && $twinExitObj->ornamentFlag)) {

                    $self->drawExitOrnaments(
                        $exitObj,
                        $colour,
                        $xPos,
                        $yPos,
                        $twinExitObj,
                        $destXPos,
                        $destYPos,
                    );
                }
            }
        }

        return 1;
    }

    sub drawArrowHead {

        # Called by $self->drawOneWayExit and $self->drawBentExit to draw an arrow-head on a one-way
        #   exit, on the line section nearest to the parent room
        #
        # Expected arguments
        #   $exitObj        - The GA::Obj::Exit for which an arrowhead is being drawn
        #   $xPos, $yPos    - The position of the parent room's gridblock on the map
        #   $posnListRef    - Reference to a list of coordinates describing the start and end of
        #                       the line section nearest to the parent room (only the first four
        #                       items in the list are used)
        #   $colour         - The exit's colour
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise, returns a list of canvas objects for the arrowhead, which the calling
        #       function can add to the other canvas objects that comprise the drawn exit

        my ($self, $exitObj, $xPos, $yPos, $posnListRef, $colour, $check) = @_;

        # Local variables
        my (
            $arrowVectorRef, $newObj, $newObj2, $posnSet,
            @emptyList,
        );

        # Check for improper arguments
        if (
            ! defined $exitObj || ! defined $xPos || ! defined $yPos || ! defined $posnListRef
            || ! defined $colour || defined $check
        ) {
            $axmud::CLIENT->writeImproper($self->_objClass . '->drawArrowHead', @_);
            return @emptyList;
        }

        # Find the exit's arrow vector - a reference to a list, (x1, y1, x2, y2)
        # (x1, y1) is a vector showing the direction of one half of the arrowhead, starting at
        #   the edge of the block. (x2, y2) is a vector showing the direction of travel of the
        #   other half
        # (NB The calling function has already checked that $exitObj->mapDir is defined)
        $arrowVectorRef = $self->ivShow('constArrowVectorHash', $exitObj->mapDir);

        # Draw the arrowhead canvas objects
        $newObj = Gnome2::Canvas::Item->new (
            $self->canvasRoot,
            'Gnome2::Canvas::Line',
            points => [
                $$posnListRef[2] + $xPos,
                $$posnListRef[3] + $yPos,
                $$posnListRef[2] + $xPos + (
                    $$arrowVectorRef[0] * int(
                        ($self->currentRegionmap->blockWidthPixels
                            - $self->currentRegionmap->roomWidthPixels) / 3
                    )
                ),
                $$posnListRef[3] + $yPos + (
                    $$arrowVectorRef[1] * int(
                        ($self->currentRegionmap->blockHeightPixels
                            - $self->currentRegionmap->roomHeightPixels) / 3
                    )
                ),
            ],
            fill_color => $colour,
            width_units => 1,
            cap_style => 'projecting',
        );

        $newObj2 = Gnome2::Canvas::Item->new (
            $self->canvasRoot,
            'Gnome2::Canvas::Line',
            points => [
                $$posnListRef[2] + $xPos,
                $$posnListRef[3] + $yPos,
                $$posnListRef[2] + $xPos + (
                    $$arrowVectorRef[2] * int(
                        ($self->currentRegionmap->blockWidthPixels
                            - $self->currentRegionmap->roomWidthPixels) / 3
                    )
                ),
                $$posnListRef[3] + $yPos + (
                    $$arrowVectorRef[3] * int(
                        ($self->currentRegionmap->blockHeightPixels
                            - $self->currentRegionmap->roomHeightPixels) / 3
                    )
                ),
            ],
            fill_color => $colour,
            width_units => 1,
            cap_style => 'projecting',
        );

        # Set the objects' position in the canvas drawing stack
        $posnSet = $self->setExitLevel($newObj);
        $self->setExitLevel($newObj2, $posnSet);

        # Set up the event handlers for the canvas objects
        $self->setupCanvasObjEvent('exit', $newObj, $exitObj);
        $self->setupCanvasObjEvent('exit', $newObj2, $exitObj);

        return ($newObj, $newObj2);
    }

    sub drawTwoWayExit {

        # Called by $self->drawExit to draw a two-way exit. The exit is drawn as two parallel lines
        #   all the way from one room to the other
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the parent GA::ModelObj::Room
        #   $exitObj    - Blessed reference of the GA::Obj::Exit being drawn
        #   $mode       - Matches GA::Obj::WorldModel->drawExitMode or
        #                   $self->currentRegionmap->drawExitMode; set to 'simple_exit' or
        #                   'complex_exit'
        #
        # Return values
        #   'undef' on improper arguments or if the exit can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $exitObj, $mode, $check) = @_;

        # Local variables
        my (
            $mapDir, $posnListRef, $vectorRef, $doubleVectorRef, $colour, $xPos, $yPos,
            $destRoomObj, $destXPos, $destYPos, $newObj, $newObj2, $twinExitObj, $posnSet,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawTwoWayExit', @_);
        }

        # Fetch the equivalent primary direction (the direction in which the exit is drawn on the
        #   map)
        $mapDir = $exitObj->mapDir;
        if (! $mapDir) {

            return undef;
        }

        # Draw 'up' and 'down' - the letters 'U' and 'D' in the bottom corners of the room
        if ($mapDir eq 'up' || $mapDir eq 'down') {

            $self->drawUpDown($roomObj, $exitObj);

        # Draw cardinal directions (the sixteen primary directions which aren't 'up' and 'down')
        } else {

            # Find the exit's position
            $posnListRef = $self->ivShow('preDrawnLongExitHash', $mapDir);
            # Find the exit's vector - a reference to a list of 3d coordinates, (x, y, z)
            #   e.g. northeast > (1, 1, 0)
            $vectorRef = $self->ivShow('constVectorHash', $mapDir);
            # Find the exit's double vector - a reference to a list of 3d coordinates,
            #   (x1, y1, x2, y2) which we add to the start and stop pixels of what would have been a
            #   single line, to produce two parallel lines either side of it
            $doubleVectorRef = $self->ivShow('constDoubleVectorHash', $mapDir);
            # Decide which colour to use
            $colour = $self->getExitColour($exitObj);

            # Get the position of $roomObj's gridblock
            $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
            $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;
            # Get the position of the destination room's griblock
            $destRoomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->destRoom);
            $destXPos = $destRoomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
            $destYPos = $destRoomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

            if ($mode eq 'complex_exit') {

                # (Complex exits) For diagonal two-way exits, we have a small problem
                # We need to increase the length of each of the two lines by one pixel, to
                #   compensate for the fact that each line is being drawn 1 pixel closer to one of
                #   the rooms (and one pixel further away from the others)
                # When the exit is diagonal, $$vectorRef[0] and $$vectorRef[1] both equal either 1
                #   or -1

                # Draw the canvas objects
                if ($$vectorRef[0] && $$vectorRef[1] && ! $$vectorRef[2]) {

                    # It's a diagonal two-way exit
                    $newObj = Gnome2::Canvas::Item->new (
                        $self->canvasRoot,
                        'Gnome2::Canvas::Line',
                        points => [
                            $$posnListRef[0] + $xPos + $$doubleVectorRef[0],
                            $$posnListRef[1] + $yPos + $$doubleVectorRef[1],
                            $$posnListRef[4] + $destXPos + $$doubleVectorRef[0] + $$vectorRef[0],
                            $$posnListRef[5] + $destYPos + $$doubleVectorRef[1] + $$vectorRef[1],
                        ],
                        fill_color => $colour,
                        width_units => 1,
                        cap_style => 'projecting',
                    );

                    $newObj2 = Gnome2::Canvas::Item->new (
                        $self->canvasRoot,
                        'Gnome2::Canvas::Line',
                        points => [
                            $$posnListRef[0] + $xPos + $$doubleVectorRef[2],
                            $$posnListRef[1] + $yPos + $$doubleVectorRef[3],
                            $$posnListRef[4] + $destXPos + $$doubleVectorRef[2] + $$vectorRef[0],
                            $$posnListRef[5] + $destYPos + $$doubleVectorRef[3] + $$vectorRef[1],
                        ],
                        fill_color => $colour,
                        width_units => 1,
                        cap_style => 'projecting',
                    );

                } else {

                    # It's not a diagonal two-way exit
                    $newObj = Gnome2::Canvas::Item->new (
                        $self->canvasRoot,
                        'Gnome2::Canvas::Line',
                        points => [
                            $$posnListRef[0] + $xPos + $$doubleVectorRef[0],
                            $$posnListRef[1] + $yPos + $$doubleVectorRef[1],
                            $$posnListRef[4] + $destXPos + $$doubleVectorRef[0],
                            $$posnListRef[5] + $destYPos + $$doubleVectorRef[1],
                        ],
                        fill_color => $colour,
                        width_units => 1,
                        cap_style => 'projecting',
                    );

                    $newObj2 = Gnome2::Canvas::Item->new (
                        $self->canvasRoot,
                        'Gnome2::Canvas::Line',
                        points => [
                            $$posnListRef[0] + $xPos + $$doubleVectorRef[2],
                            $$posnListRef[1] + $yPos + $$doubleVectorRef[3],
                            $$posnListRef[4] + $destXPos + $$doubleVectorRef[2],
                            $$posnListRef[5] + $destYPos + $$doubleVectorRef[3],
                        ],
                        fill_color => $colour,
                        width_units => 1,
                        cap_style => 'projecting',
                    );
                }

                # Set the object's position in the canvas drawing stack
                $posnSet = $self->setExitLevel($newObj);
                $self->setExitLevel($newObj2, $posnSet);

                # Set up the event handlers for the canvas objects
                $self->setupCanvasObjEvent('exit', $newObj, $exitObj);
                $self->setupCanvasObjEvent('exit', $newObj2, $exitObj);

                # Store the canvas objects together
                $self->ivAdd('drawnExitHash', $exitObj->number, [$newObj, $newObj2]);

            } else {

                # (Simple exits) Draw the canvas object
                $newObj = Gnome2::Canvas::Item->new (
                    $self->canvasRoot,
                    'Gnome2::Canvas::Line',
                    points => [
                        $$posnListRef[0] + $xPos,
                        $$posnListRef[1] + $yPos,
                        $$posnListRef[4] + $destXPos,
                        $$posnListRef[5] + $destYPos,
                    ],
                    fill_color => $colour,
                    width_units => 1,
                    cap_style => 'projecting',
                );

                # Set the object's position in the canvas drawing stack
                $self->setExitLevel($newObj);

                # Set up the event handler for the canvas object
                $self->setupCanvasObjEvent('exit', $newObj, $exitObj);

                # Store the canvas object
                $self->ivAdd('drawnExitHash', $exitObj->number, [$newObj]);
            }

            # Draw ornaments for this exit and/or the twin exit, if there are any (and if allowed)
            if ($self->worldModelObj->drawOrnamentsFlag) {

                if ($exitObj->twinExit) {

                    $twinExitObj
                        = $self->worldModelObj->ivShow('exitModelHash', $exitObj->twinExit);
                }

                if ($exitObj->ornamentFlag || ($twinExitObj && $twinExitObj->ornamentFlag)) {

                    $self->drawExitOrnaments(
                        $exitObj,
                        $colour,
                        $xPos,
                        $yPos,
                        $twinExitObj,
                        $destXPos,
                        $destYPos,
                    );
                }
            }
        }

        return 1;
    }

    sub drawUnallocatedExit {

        # Called by $self->drawExit to draw an exit in a non-primary direction, which has been
        #   allocated a (temporary) primary direction (it's up to the user to allocate the primary
        #   direction they actually want)
        # The exit is drawn as an 'x'
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the parent GA::ModelObj::Room
        #   $exitObj    - Blessed reference of the GA::Obj::Exit being drawn
        #
        # Return values
        #   'undef' on improper arguments or if the exit can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $exitObj, $check) = @_;

        # Local variables
        my ($mapDir, $posnListRef, $colour, $xPos, $yPos, $newObj, $newObj2, $posnSet);

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawUnallocatedExit', @_);
        }

        # Fetch the equivalent primary direction (the direction in which the exit is drawn on the
        #   map)
        $mapDir = $exitObj->mapDir;
        if (! $mapDir) {

            return undef;
        }

        # Draw 'up' and 'down' - the letters 'U' and 'D' in the bottom corners of the room
        if ($mapDir eq 'up' || $mapDir eq 'down') {

            $self->drawUpDown($roomObj, $exitObj);

        # Draw cardinal directions (the sixteen primary directions which aren't 'up' and 'down')
        } else {

            # Find the exit's position
            $posnListRef = $self->ivShow('preDrawnSquareExitHash', $mapDir);
            # Decide which colour to use
            $colour = $self->getExitColour($exitObj);

            # Get the position of $roomObj's gridblock
            $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
            $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

            # Draw the canvas objects
            $newObj = Gnome2::Canvas::Item->new (
                $self->canvasRoot,
                'Gnome2::Canvas::Line',
                points => [
                    $$posnListRef[0] + $xPos,
                    $$posnListRef[1] + $yPos,
                    $$posnListRef[2] + $xPos,
                    $$posnListRef[3] + $yPos,
                ],
                fill_color => $colour,
                width_units => 1,
                cap_style => 'projecting',
            );

            $newObj2 = Gnome2::Canvas::Item->new (
                $self->canvasRoot,
                'Gnome2::Canvas::Line',
                points => [
                    $$posnListRef[2] + $xPos,
                    $$posnListRef[1] + $yPos,
                    $$posnListRef[0] + $xPos,
                    $$posnListRef[3] + $yPos,
                ],
                fill_color => $colour,
                width_units => 1,
                cap_style => 'projecting',
            );

            # Set the object's position in the canvas drawing stack
            $posnSet = $self->setExitLevel($newObj);
            $self->setExitLevel($newObj2, $posnSet);

            # Set up the event handlers for the canvas objects
            $self->setupCanvasObjEvent('exit', $newObj, $exitObj);
            $self->setupCanvasObjEvent('exit', $newObj2, $exitObj);

            # Store the canvas objects together
            $self->ivAdd('drawnExitHash', $exitObj->number, [$newObj, $newObj2]);
        }

        return 1;
    }

    sub drawImpassableExit {

        # Called by $self->drawExit to draw an exit which is impassable, or an exit whose twin
        #   exit is impassable
        # This is the only one of the draw functions which draws an exit and an exit ornament at the
        #   same time, in the same function
        # If one of the exits drawn here isn't impassable, it's drawn in the same colour as the
        #   impassable exit (default purple) and without its own exit ornaments (if there are any)
        #
        # Expected arguments
        #   $roomObj        - Blessed reference of the parent GA::ModelObj::Room
        #   $exitObj        - Blessed reference of the GA::Obj::Exit being drawn
        #
        # Optional arguments
        #   $twinExitObj    - The twin GA::Obj::Exit, if there is one ('undef' otherwise)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $roomObj, $exitObj, $twinExitObj, $check) = @_;

        # Local variables
        my (
            $colour, $twinRoomObj,
            @list, @newObjList,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawImpassableExit', @_);
        }

        # If the twin exit's room is in a different region or on a different level, then we
        #   don't draw it using this function
        if ($twinExitObj) {

            # Get the parent room of $twinExitObj
            $twinRoomObj = $self->worldModelObj->ivShow('modelHash', $twinExitObj->parent);

            if (
                ! $twinRoomObj
                || $roomObj->parent ne $twinRoomObj->parent
                || $roomObj->zPosBlocks ne $twinRoomObj->zPosBlocks
            ) {
                # Don't draw the twin exit right now
                $twinExitObj = undef;
                $twinRoomObj = undef;
            }
        }

        # Decide which colour to use. If one of the exits is not impassable, it's drawn in the same
        #   colour (default purple) as the one that is impassable. If either of them are selected,
        #   then of course they'll be both drawn in the selected object colour (default blue)
        if ($exitObj->impassFlag) {
            $colour = $self->getExitColour($exitObj);
        } elsif ($twinExitObj && $twinExitObj->impassFlag) {
            $colour = $self->getExitColour($twinExitObj);
        }

        # If @exitList contains two exits, one (or both) of them are impassable. If one is not
        #   impassable, it's drawn like an uncertain exit would be drawn
        # If @exitList contains one exit, it's definitely impassable
        @list = ($exitObj, $roomObj, $twinExitObj, $twinRoomObj);

        do {

            my (
                $thisExitObj, $thisRoomObj, $mapDir, $posnListRef, $xPos, $yPos, $newObj,
                $squareStartXPosPixels, $squareStopYPosPixels, $squareStopXPosPixels,
                $squareStartYPosPixels,
            );

            $thisExitObj = shift @list;
            $thisRoomObj = shift @list;

            # Don't try to draw a twin exit if there isn't one
            if (defined $thisExitObj) {

                # Fetch the equivalent primary direction (the direction in which the exit is drawn
                #   on the map)
                $mapDir = $thisExitObj->mapDir;
            }

            if ($mapDir) {

                # Draw 'up' and 'down' - the letters 'U' and 'D' in the bottom corners of the room
                if ($mapDir eq 'up' || $mapDir eq 'down') {

                    $self->drawUpDown($thisRoomObj, $thisExitObj);

                # Draw cardinal directions (the sixteen primary directions which aren't 'up' and
                #   'down')
                } else {

                    # Get the position of $thisRoomObj's gridblock
                    $xPos = $thisRoomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
                    $yPos = $thisRoomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

                    if ($thisExitObj->impassFlag) {

                        # Draw an impassable exit. Find the exit's position
                        $posnListRef = $self->ivShow('preDrawnSquareExitHash', $mapDir);

                        # Invert the coordinates of the square occupied by the exit so that
                        #   ($squareStartXPosPixels, $squareStartYPosPixels) is the top-left corner,
                        #   rather than the bottom-left corner
                        (
                            $squareStartXPosPixels, $squareStopYPosPixels,
                            $squareStopXPosPixels, $squareStartYPosPixels,
                        ) = (
                            $$posnListRef[0], $$posnListRef[1],
                            $$posnListRef[2], $$posnListRef[3],
                        );

                        # Draw the canvas object
                        $newObj = Gnome2::Canvas::Item->new (
                            $self->canvasRoot,
                            'Gnome2::Canvas::Rect',
                            x1 => $squareStartXPosPixels + $xPos,
                            y1 => $squareStartYPosPixels + $yPos,
                            x2 => $squareStopXPosPixels + $xPos,
                            y2 => $squareStopYPosPixels + $yPos,
                            outline_color => $colour,
                            fill_color => $colour,
                        );

                        # Set the object's position in the canvas drawing stack
                        $self->setExitLevel($newObj);

                        # Set up the event handler for the canvas object (using the first exit in
                        #   @exitList, even if there are two)
                        $self->setupCanvasObjEvent('exit', $newObj, $exitObj);

                        push (@newObjList, $newObj);

                    } else {

                        # Draw a non-impassable exit whose twin is impassable. Find the exit's
                        #   position
                        $posnListRef = $self->ivShow('preDrawnUncertainExitHash', $mapDir);

                        # Draw the canvas object
                        $newObj = Gnome2::Canvas::Item->new (
                            $self->canvasRoot,
                            'Gnome2::Canvas::Line',
                            points => [
                                $$posnListRef[0] + $xPos,
                                $$posnListRef[1] + $yPos,
                                $$posnListRef[2] + $xPos,
                                $$posnListRef[3] + $yPos,
                            ],
                            fill_color => $colour,
                            width_units => 1,
                            cap_style => 'projecting',
                        );

                        # Set the object's position in the canvas drawing stack
                        $self->setExitLevel($newObj);

                        # Set up the event handler for the canvas object (using the first exit in
                        #   @exitList, even if there are two)
                        $self->setupCanvasObjEvent('exit', $newObj, $exitObj);

                        push (@newObjList, $newObj);
                    }
                }
            }

        } until (! @list);

        # Store the canvas objects (assuming at least one was created)
        if (@newObjList) {

            $self->ivAdd('drawnExitHash', $exitObj->number, \@newObjList);
        }

        return 1;
    }

    sub drawBentExit {

        # Called by $self->drawExit to draw an exit that leads to another room at an arbitrary
        #   position in this region (and not necessarily on the same level). 'Bent' broken exits are
        #   drawn as a line with one or more bends
        #
        # Expected arguments
        #   $roomObj        - Blessed reference of the parent GA::ModelObj::Room
        #   $exitObj        - Blessed reference of the GA::Obj::Exit being drawn
        #   $mode           - Matches GA::Obj::WorldModel->drawExitMode or
        #                       $self->currentRegionmap->drawExitMode; set to 'simple_exit' or
        #                       'complex_exit'
        #
        # Optional arguments
        #   $twinExitObj    - The exit's twin (if it has one - must be specified, if so; set to
        #                       'undef', if not)
        #
        # Return values
        #   'undef' on improper arguments or if the exit can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $exitObj, $mode, $twinExitObj, $check) = @_;

        # Local variables
        my (
            $mapDir, $xPos, $yPos, $colour, $posnListRef, $destRoomObj, $destXPos, $destYPos,
            $posnListRef2, $vectorRef, $doubleVectorRef, $newObj, $newObj2, $newObj3, $bendSize,
            $listRef2, $listRef3, $posnSet,
            @offsetList, @pointList, @pointList2, @pointList3, @canvasObjList,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || ! defined $mode || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawBrokenExit', @_);
        }

        # For one-way exits, if the destination room has an exit drawn in the primary used by
        #   $exitObj->oneWayDir, then we draw the exit as a normal one-way exit (which will be
        #   confined to its own gridblock, to avoid overlapping the destination room's opposite
        #   exit)
        $destRoomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->destRoom);
        if ($exitObj->oneWayFlag && $destRoomObj->ivExists('exitNumHash', $exitObj->oneWayDir)) {

            return $self->drawOneWayExit($roomObj, $exitObj);

        # For uncertain exits, draw the exit as a normal uncertain exit
        } elsif (! $exitObj->oneWayFlag && ! $twinExitObj) {

            return $self->drawUncertainExit($roomObj, $exitObj);
        }

        # Fetch the equivalent primary direction (the direction in which the exit is drawn on the
        #   map)
        $mapDir = $exitObj->mapDir;
        if (! $mapDir) {

            return undef;
        }

        # Draw 'up' and 'down' - the letters 'U' and 'D' in the bottom corners of the room
        if ($mapDir eq 'up' || $mapDir eq 'down') {

            $self->drawUpDown($roomObj, $exitObj);

        # Draw cardinal directions (the sixteen primary directions which aren't 'up' and 'down')
        } else {

            # Get the position of $roomObj's gridblock
            $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
            $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;
            # Decide which colour to use
            $colour = $self->getExitColour($exitObj);

            # Compile a list of points in the form (x, y, x, y...) which mark the start and finish,
            #   and the intermediate bends, of a bent exit

            # The first section of a bent exit consists of the line that would be drawn as an
            #   uncertain exit. $posnListRef is a reference to a list in the form
            #   (startx, starty, stopx, stopy)
            $posnListRef = $self->ivShow('preDrawnUncertainExitHash', $mapDir);
            push (@pointList,
                ($xPos + $$posnListRef[0]),
                ($yPos + $$posnListRef[1]),
                ($xPos + $$posnListRef[2]),
                ($yPos + $$posnListRef[3]),
            );

            # The middle section of a bent exit consists of a list of points, starting at the end of
            #   the hypothetical uncertain exit. If there are no bends marked in the exit's
            #   ->bendOffsetList (after being added by the user), then nothing is added to
            #   @offsetList here
            if ($exitObj->bendOffsetList) {

                @offsetList = $exitObj->bendOffsetList;
                do {

                    my ($offsetXPos, $offsetYPos);

                    $offsetXPos = (shift @offsetList) + $xPos + $$posnListRef[2];
                    $offsetYPos = (shift @offsetList) + $yPos + $$posnListRef[3];

                    push (@pointList, $offsetXPos, $offsetYPos);

                } until (! @offsetList);
            }

            # The final section of a bent exit consists of a line that would be drawn as an
            #   uncertain exit at the destination room
            # If $exitObj has a twin exit that's drawn in a cardinal direction, that's the uncertain
            #   exit whose line we use.
            # If $exitObj is a one-way exit, we use the uncertain exit specified by its
            #   ->oneWayDir (which is the opposite of ->mapDir, by default, but this can be changed
            #   by the user)
            # Otherwise, we use the uncertain exit in the opposite direction that $exitObj is drawn
            #   (e.g. west - east)
            $destXPos = $destRoomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
            $destYPos = $destRoomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

            if ($twinExitObj) {

                $posnListRef2 = $self->ivShow('preDrawnUncertainExitHash', $twinExitObj->mapDir);

            } elsif (
                $exitObj->oneWayFlag
                # (->preDrawnUncertainExitHash doesn't contain 'up' and 'down')
                && $self->ivExists('preDrawnUncertainExitHash', $exitObj->oneWayDir)
            ) {
                $posnListRef2 = $self->ivShow('preDrawnUncertainExitHash', $exitObj->oneWayDir);

            } else {

                $posnListRef2 = $self->ivShow(
                    'preDrawnUncertainExitHash',
                    $axmud::CLIENT->ivShow('constOppDirHash', $exitObj->mapDir),
                );
            }

            push (@pointList,
                ($destXPos + $$posnListRef2[2]),
                ($destYPos + $$posnListRef2[3]),
                ($destXPos + $$posnListRef2[0]),
                ($destYPos + $$posnListRef2[1]),
            );

            if ($twinExitObj && $mode eq 'complex_exit') {

                # Draw a two-way bent exit in complex exits mode (only)

                # First and middle section

                # Find the exit's vector - a reference to a list of 3d coordinates, (x, y, z)
                #   e.g. northeast > (1, 1, 0)
                $vectorRef = $self->ivShow('constVectorHash', $mapDir);
                # Find the exit's double vector - a reference to a list of 3d coordinates,
                #   (x1, y1, x2, y2) which we add to the start and stop pixels of what would have
                #   been a single line, to produce two parallel lines either side of it
                $doubleVectorRef = $self->ivShow('constDoubleVectorHash', $mapDir);

                # Convert the list of coordinates in @pointList, representing a bent exit drawn as
                #   a single line, into two lists of coordinates, @pointList2 and @pointList3,
                #   representing a bent exit drawn as two parallel lines
                # Don't do anything to the last four values in @pointList (which represent the end
                #   section) yet
                for (my $count = 0; $count < ((scalar @pointList) - 4); $count += 2) {

                    push (@pointList2, $pointList[$count] + $$doubleVectorRef[0]);
                    push (@pointList2, $pointList[$count + 1] + $$doubleVectorRef[1]);

                    push (@pointList3, $pointList[$count] + $$doubleVectorRef[2]);
                    push (@pointList3, $pointList[$count + 1] + $$doubleVectorRef[3]);
                }

                # End section

                # Find the exit's vector - a reference to a list of 3d coordinates, (x, y, z)
                #   e.g. northeast > (1, 1, 0)
                $vectorRef = $self->ivShow('constVectorHash', $twinExitObj->mapDir);
                # Find the exit's double vector - a reference to a list of 3d coordinates,
                #   (x1, y1, x2, y2) which we add to the start and stop pixels of what would have
                #   been a single line, to produce two parallel lines either side of it
                $doubleVectorRef = $self->ivShow('constDoubleVectorHash', $twinExitObj->mapDir);

                # Convert the list of coordinates in @pointList, representing a bent exit drawn as
                #   a single line, into two lists of coordinates, @pointList2 and @pointList3,
                #   representing a bent exit drawn as two parallel lines
                # Don't do anything to the last four values in @pointList (which represent the end
                #   section) yet
                for (
                    my $count = ((scalar @pointList) - 4);
                    $count < scalar @pointList;
                    $count += 2
                ) {
                    push (@pointList2, $pointList[$count] + $$doubleVectorRef[0]);
                    push (@pointList2, $pointList[$count + 1] + $$doubleVectorRef[1]);

                    push (@pointList3, $pointList[$count] + $$doubleVectorRef[2]);
                    push (@pointList3, $pointList[$count + 1] + $$doubleVectorRef[3]);
                }

                # For diagonal two-way exits, we have a small problem
                # We need to increase the length of each of the two lines by one pixel, to
                #   compensate for the fact that each line is being drawn 1 pixel closer to one of
                #   the rooms (and one pixel further away from the others)
                # When the exit is diagonal, $$vectorRef[0] and $$vectorRef[1] both equal either 1
                #   or -1
                if ($$vectorRef[0] && $$vectorRef[1] && ! $$vectorRef[2]) {

                    $pointList2[-2] += $$vectorRef[0];
                    $pointList2[-1] += $$vectorRef[1];

                    $pointList3[-2] += $$vectorRef[0];
                    $pointList3[-1] += $$vectorRef[1];
                }

                # Depending on the relative positions of the two twinned exits, and the positions
                #   of any bends between them, the two parallel may intersect and cross over each
                #   other
                # Eliminate these intersections by reversing the positions of corresponding points
                #   in @pointList2 and @pointList3, as necessary
                ($listRef2, $listRef3) = $self->reverseIntersectingLines(
                    \@pointList2,
                    \@pointList3,
                );

                @pointList2 = @$listRef2;
                @pointList3 = @$listRef3;

                # Draw the canvas objects
                $newObj = Gnome2::Canvas::Item->new (
                    $self->canvasRoot,
                    'Gnome2::Canvas::Line',
                    points => [@pointList2],
                    fill_color => $colour,
                    width_units => 1,
                    cap_style => 'projecting',
                );

                $newObj2 = Gnome2::Canvas::Item->new (
                    $self->canvasRoot,
                    'Gnome2::Canvas::Line',
                    points => [@pointList3],
                    fill_color => $colour,
                    width_units => 1,
                    cap_style => 'projecting',
                );

                # Set the object's position in the canvas drawing stack
                $posnSet = $self->setExitLevel($newObj);
                $self->setExitLevel($newObj2, $posnSet);

                # Set up the event handlers for the canvas objects
                $self->setupCanvasObjEvent('exit', $newObj, $exitObj);
                $self->setupCanvasObjEvent('exit', $newObj2, $exitObj);

                push (@canvasObjList, $newObj, $newObj2);

            } else {

                # Draw a one-way exit in simple/complex exits mode, or a two-way exit in simple
                #   exits mode

                # Draw the canvas object
                $newObj = Gnome2::Canvas::Item->new (
                    $self->canvasRoot,
                    'Gnome2::Canvas::Line',
                    points => [@pointList],
                    fill_color => $colour,
                    width_units => 1,
                    cap_style => 'projecting',
                );

                # For one-way exits, draw the arrowhead over the first section of the bent exit
                if ($exitObj->oneWayFlag) {

                    # Draw the canvas objects for the arrowhead part of the exit
                    ($newObj2, $newObj3) = $self->drawArrowHead(
                        $exitObj,
                        $xPos,
                        $yPos,
                        $posnListRef,
                        $colour,
                    );
                }

                # Set the object's position in the canvas drawing stack (this has already been done
                #   for $newObj2 and $newObj3, if set)
                $self->setExitLevel($newObj);

                # Set up the event handler for the canvas object (this has already been done for
                #   $newObj2 and $newObj3, if set)
                $self->setupCanvasObjEvent('exit', $newObj, $exitObj);

                # Store the canvas object(s)
                push (@canvasObjList, $newObj);
                if ($newObj2 && $newObj3) {

                    push (@canvasObjList, $newObj2, $newObj3);
                }
            }

            # For a selected exit, draw a square at each bend
            if (
                $exitObj->bendOffsetList
                && (
                    (
                        ($self->selectedExit && $self->selectedExit eq $exitObj)
                        || ($self->ivExists('selectedExitHash', $exitObj->number))
                    ) || (
                        $twinExitObj
                        && (
                            ($self->selectedExit && $self->selectedExit eq $twinExitObj)
                            || ($self->ivExists('selectedExitHash', $twinExitObj->number))
                        )
                    )
                )
            ) {
                @offsetList = $exitObj->bendOffsetList;
                $bendSize = $self->exitBendSize;            # Default 2
                do {

                    my ($offsetXPos, $offsetYPos, $bendObj);

                    $offsetXPos = (shift @offsetList) + $$posnListRef[2];
                    $offsetYPos = (shift @offsetList) + $$posnListRef[3];

                    # Draw the canvas object
                    $bendObj = Gnome2::Canvas::Item->new(
                        $self->canvasRoot,
                        'Gnome2::Canvas::Rect',
                        x1 => $xPos + $offsetXPos - $bendSize,
                        y1 => $yPos + $offsetYPos - $bendSize,
                        x2 => $xPos + $offsetXPos + $bendSize,
                        y2 => $yPos + $offsetYPos + $bendSize,
                        outline_color => $colour,
                        fill_color => $colour,
                    );

                    # Set the object's position in the canvas drawing stack
                    $self->setExitLevel($newObj);

                    # Set up the event handler for the canvas object
                    $self->setupCanvasObjEvent('exit', $bendObj, $exitObj);

                    push (@canvasObjList, $bendObj);

                } until (! @offsetList);
            }

            # Store the canvas objects together
            $self->ivAdd('drawnExitHash', $exitObj->number, \@canvasObjList);

            # Draw ornaments for this exit and/or the twin exit, if there are any (and if allowed)
            if ($self->worldModelObj->drawOrnamentsFlag) {

                if ($exitObj->ornamentFlag || ($twinExitObj && $twinExitObj->ornamentFlag)) {

                    $self->drawExitOrnaments(
                        $exitObj,
                        $colour,
                        $xPos,
                        $yPos,
                        $twinExitObj,
                        $destXPos,
                        $destYPos,
                    );
                }
            }
        }

        return 1;
    }

    sub reverseIntersectingLines {

        # Called by $self->drawBentExit when drawing a 2-way bent exit with two roughly parellel
        #   lines
        # This function detects whether those lines intersect each other, rather than run roughly
        #   parallel and, if so, corrrects the problem
        #
        # Expected arguments
        #   $listRef, $listRef2
        #           - Two list references, each of which contain a list of points which make up the
        #               beginning, end and middle bends of a single line
        #           - Each list is in the form
        #               (x, y, x, y, x, y, ...)
        #
        # Return values
        #   'undef' on improper arguments
        #   Otherwise returns a list containing the modified $listRef and $listRef2

        my ($self, $listRef, $listRef2, $check) = @_;

        # Local variables
        my $total;

        # Check for improper arguments
        if (! defined $listRef || ! defined $listRef2 || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->reverseIntersectingLines',
                @_,
            );
        }

        # Each pair of neighbouring points:
        #   x = $$listRef[n], y = $$listRef[n + 1]
        #   x = $$listRef2[n], y = $$listRef2[n + 1]
        # ...may be the wrong way around, such that the two lines are not roughly parallel, but
        #   cross over each other somewhere between the two points
        #
        # For each neighbouring pairs of points, [n] and [n+1], work out whether the two lines
        #   between them lines intersect each other (rather than remaining roughly parallel) and, if
        #   so, reverse them to remove the intersection

        # Get the number of points in the line (2 for each end of the line, plus the number of
        #   bends - if there are no bends, there are four points in the line)
        $total = (scalar @$listRef / 2);
        # Don't reverse points at the beginning of the lines (but we might have to reverse points
        #   at the end, as a knock-on effect of reversing points somewhere in the middle)
        for (my $count = 1; $count < ($total - 1); $count++) {

            my ($index, $dist1, $dist2, $dist3, $dist4, $spare);

            $index = $count * 2;

            # Work out the length of the two lines that should be parallel, as they are at the
            #   moment
            $dist1 = sqrt(
                        (($$listRef[$index] - $$listRef[($index+ 2)]) ** 2)
                        + (($$listRef[($index + 1)] - $$listRef[($index + 3)]) ** 2)
                    );

            $dist2 = sqrt(
                        (($$listRef2[$index] - $$listRef2[($index + 2)]) ** 2)
                        + (($$listRef2[($index + 1)] - $$listRef2[($index + 3)]) ** 2)
                    );

            # Work out the length of the two lines that would be drawn, if the points at one end
            #   were reversed
            $dist3 = sqrt(
                        (($$listRef[$index] - $$listRef2[($index + 2)]) ** 2)
                        + (($$listRef[($index + 1)] - $$listRef2[($index + 3)]) ** 2)
                    );

            $dist4 = sqrt(
                        (($$listRef2[$index] - $$listRef[($index + 2)]) ** 2)
                        + (($$listRef2[($index + 1)] - $$listRef[($index + 3)]) ** 2)
                    );

            # By adding the distances, we can work out if the second pair of points need to be
            #   reversed, in order to make the lines parallel
            if (($dist1 + $dist2) > ($dist3 + $dist4)) {

                $spare = $$listRef[($index + 2)];
                $$listRef[($index + 2)] = $$listRef2[($index + 2)];
                $$listRef2[($index + 2)] = $spare;

                $spare = $$listRef[($index + 3)];
                $$listRef[($index + 3)] = $$listRef2[($index + 3)];
                $$listRef2[($index + 3)] = $spare;
            }
        }

        return $listRef, $listRef2;
    }

    sub drawDraggableExit {

        # Called by $self->startDrag and ->continueDrag to draw a draggable exit
        #
        # Expected arguments
        #   $exitObj    - Blessed reference of the GA::Obj::Exit being drawn
        #   $mouseXPos, $mouseYPos
        #               - The position on the map of the end of the exit (the current position of
        #                       the user's mouse)
        #
        # Return values
        #   'undef' on improper arguments or if the exit can't be drawn
        #   Otherwise, returns the canvas object drawn

        my ($self, $exitObj, $mouseXPos, $mouseYPos, $check) = @_;

        # Local variables
        my (
            $mapDir, $roomObj, $xPos, $yPos, $posnListRef, $newObj,
            @pointList,
        );

        # Check for improper arguments
        if (! defined $exitObj || ! defined $mouseXPos || ! defined $mouseYPos || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawDraggableExit', @_);
        }

        # Fetch the equivalent primary direction (the direction in which the exit is drawn on the
        #   map)
        $mapDir = $exitObj->mapDir;
        if (! $mapDir) {

            return undef;
        }

        # Get the position of the parent room's gridblock
        $roomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->parent);
        $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
        $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

        # Compile a list of points in the form (x, y, x, y...) which mark the start and finish,
        #   and the intermediate bends, of a bent exit

        # The first section of a draggable exit consists of the line that would be drawn as an
        #   uncertain exit. $posnListRef is a reference to a list in the form (startx, starty,
        #   stopx, stopy)
        $posnListRef = $self->ivShow('preDrawnUncertainExitHash', $mapDir);
        push (@pointList,
            ($xPos + $$posnListRef[0]),
            ($yPos + $$posnListRef[1]),
            ($xPos + $$posnListRef[2]),
            ($yPos + $$posnListRef[3]),
        );

        # The middle section of a draggable exit leads from the end of the hypothetical uncertain
        #   exit, to the current mouse position
        push (@pointList, $mouseXPos, $mouseYPos);

        # Draw the canvas object
        $newObj = Gnome2::Canvas::Item->new (
            $self->canvasRoot,
            'Gnome2::Canvas::Line',
            points => [@pointList],
            fill_color => $self->worldModelObj->dragExitColour,
            width_units => 1,
            cap_style => 'projecting',
        );

        # Set the object's position in the canvas drawing stack
        $self->setLabelLevel($newObj);
        # Set up the event handlers for the canvas objects
        $self->setupCanvasObjEvent('exit', $newObj, $exitObj);

        return $newObj;
    }

    sub drawBrokenExit {

        # Called by $self->drawExit to draw an exit that leads to another room at an arbitrary
        #   position in this region (and not necessarily on the same level). 'Normal' broken exits
        #   are drawn as a filled-in square
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the parent GA::ModelObj::Room
        #   $exitObj    - Blessed reference of the GA::Obj::Exit being drawn
        #
        # Return values
        #   'undef' on improper arguments or if the exit can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $exitObj, $check) = @_;

        # Local variables
        my (
            $mapDir, $posnListRef, $colour, $xPos, $yPos, $squareStartXPosPixels,
            $squareStopYPosPixels, $squareStopXPosPixels, $squareStartYPosPixels, $newObj,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawBrokenExit', @_);
        }

        # Fetch the equivalent primary direction (the direction in which the exit is drawn on the
        #   map)
        $mapDir = $exitObj->mapDir;
        if (! $mapDir) {

            return undef;
        }

        # Draw 'up' and 'down' - the letters 'U' and 'D' in the bottom corners of the room
        if ($mapDir eq 'up' || $mapDir eq 'down') {

            $self->drawUpDown($roomObj, $exitObj);

        # Draw cardinal directions (the sixteen primary directions which aren't 'up' and 'down')
        } else {

            # Find the exit's position
            $posnListRef = $self->ivShow('preDrawnSquareExitHash', $mapDir);
            # Decide which colour to use
            $colour = $self->getExitColour($exitObj);

            # Get the position of $roomObj's gridblock
            $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
            $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

            # Invert the coordinates of the square occupied by the exit so that
            #   ($squareStartXPosPixels, $squareStartYPosPixels) is the top-left corner, rather than
            #   the bottom-left corner
            (
                $squareStartXPosPixels, $squareStopYPosPixels,
                $squareStopXPosPixels, $squareStartYPosPixels,
            ) = ($$posnListRef[0], $$posnListRef[1], $$posnListRef[2], $$posnListRef[3]);

            # Draw the canvas object
            $newObj = Gnome2::Canvas::Item->new(
                $self->canvasRoot,
                'Gnome2::Canvas::Rect',
                x1 => $squareStartXPosPixels + $xPos,
                y1 => $squareStartYPosPixels + $yPos,
                x2 => $squareStopXPosPixels + $xPos,
                y2 => $squareStopYPosPixels + $yPos,
                outline_color => $colour,
                fill_color => $colour,
            );

            # Set the object's position in the canvas drawing stack
            $self->setExitLevel($newObj);

            # Set up the event handler for the canvas object
            $self->setupCanvasObjEvent('exit', $newObj, $exitObj);

            # Store the canvas object
            $self->ivAdd('drawnExitHash', $exitObj->number, [$newObj]);
        }

        return 1;
    }

    sub drawRegionExit {

        # Called by $self->drawExit to draw an exit that leads to another room in a different region
        #   - currently drawn as an unfilled-in square
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the parent GA::ModelObj::Room
        #   $exitObj    - Blessed reference of the GA::Obj::Exit being drawn
        #
        # Return values
        #   'undef' on improper arguments or if the exit can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $exitObj, $check) = @_;

        # Local variables
        my (
            $mapDir, $posnListRef, $xPos, $yPos, $newObj, $squareStartXPosPixels,
            $squareStopYPosPixels, $squareStopXPosPixels, $squareStartYPosPixels,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawRegionExit', @_);
        }

        # Fetch the equivalent primary direction (the direction in which the exit is drawn on the
        #   map)
        $mapDir = $exitObj->mapDir;
        if (! $mapDir) {

            return undef;
        }

        # Draw 'up' and 'down' - the letters 'U' and 'D' in the bottom corners of the room
        if ($mapDir eq 'up' || $mapDir eq 'down') {

            $self->drawUpDown($roomObj, $exitObj);

        # Draw cardinal directions (the sixteen primary directions which aren't 'up' and 'down')
        } else {

            # Find the exit's position
            $posnListRef = $self->ivShow('preDrawnSquareExitHash', $mapDir);

            # Get the position of $roomObj's gridblock
            $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
            $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

            # Invert the coordinates of the square occupied by the exit so that
            #   ($squareStartXPosPixels, $squareStartYPosPixels) is the top-left corner, rather than
            #   the bottom-left corner
            (
                $squareStartXPosPixels, $squareStopYPosPixels,
                $squareStopXPosPixels, $squareStartYPosPixels,
            ) = ($$posnListRef[0], $$posnListRef[1], $$posnListRef[2], $$posnListRef[3]);

            # Draw the canvas object
            $newObj = Gnome2::Canvas::Item->new(
                $self->canvasRoot,
                'Gnome2::Canvas::Rect',
                x1 => $squareStartXPosPixels + $xPos,
                y1 => $squareStartYPosPixels + $yPos,
                x2 => $squareStopXPosPixels + $xPos,
                y2 => $squareStopYPosPixels + $yPos,
                outline_color => $self->getExitColour($exitObj),
            );

            # Set the object's position in the canvas drawing stack
            $self->setExitLevel($newObj);

            # Set up the event handler for the canvas object
            $self->setupCanvasObjEvent('exit', $newObj, $exitObj);

            # Store the canvas object
            $self->ivAdd('drawnExitHash', $exitObj->number, [$newObj]);
        }

        return 1;
    }

    sub drawRandomExit {

        # Called by $self->drawExit to draw an exit that leads to a random location
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the parent GA::ModelObj::Room
        #   $exitObj    - Blessed reference of the GA::Obj::Exit being drawn
        #
        # Return values
        #   'undef' on improper arguments or if the exit can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $exitObj, $check) = @_;

        # Local variables
        my ($mapDir, $posnListRef, $colour, $xPos, $yPos, $newObj, $fillColour);

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawRegionExit', @_);
        }

        # Fetch the equivalent primary direction (the direction in which the exit is drawn on the
        #   map)
        $mapDir = $exitObj->mapDir;
        if (! $mapDir) {

            return undef;
        }

        # Draw 'up' and 'down' - the letters 'U' and 'D' in the bottom corners of the room
        if ($mapDir eq 'up' || $mapDir eq 'down') {

            $self->drawUpDown($roomObj, $exitObj);

        # Draw cardinal directions (the sixteen primary directions which aren't 'up' and 'down')
        } else {

            # Find the exit's position
            $posnListRef = $self->ivShow('preDrawnSquareExitHash', $mapDir);

            # Get the position of $roomObj's gridblock
            $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
            $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

            # Set the exit colour
            $colour = $self->getExitColour($exitObj);

            # Draw the canvas object
            if ($exitObj->randomType eq 'any_region') {

                # Random exits leading anywhere in the world aren't filled in
                $newObj = Gnome2::Canvas::Item->new(
                    $self->canvasRoot,
                    'Gnome2::Canvas::Ellipse',
                    x1 => $$posnListRef[0] + $xPos,
                    y1 => $$posnListRef[1] + $yPos,
                    x2 => $$posnListRef[2] + $xPos,
                    y2 => $$posnListRef[3] + $yPos,
                    outline_color => $colour,
                );

            } else {

                # Other types of random exits are filled in
                if ($exitObj->randomType eq 'same_region') {
                    $fillColour = $colour;
                } else {
                    $fillColour = $self->worldModelObj->randomExitColour;
                }

                $newObj = Gnome2::Canvas::Item->new(
                    $self->canvasRoot,
                    'Gnome2::Canvas::Ellipse',
                    x1 => $$posnListRef[0] + $xPos,
                    y1 => $$posnListRef[1] + $yPos,
                    x2 => $$posnListRef[2] + $xPos,
                    y2 => $$posnListRef[3] + $yPos,
                    outline_color => $colour,
                    fill_color => $fillColour,
                );
            }

            # Set the object's position in the canvas drawing stack
            $self->setExitLevel($newObj);

            # Set up the event handler for the canvas object
            $self->setupCanvasObjEvent('exit', $newObj, $exitObj);

            # Store the canvas object
            $self->ivAdd('drawnExitHash', $exitObj->number, [$newObj]);
        }

        return 1;
    }

    sub drawRetracingExit {

        # Called by $self->drawExit to draw an exit that leads back to the same room (drawn as a
        #   triangle)
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the parent GA::ModelObj::Room
        #   $exitObj    - Blessed reference of the GA::Obj::Exit being drawn
        #
        # Return values
        #   'undef' on improper arguments or if the exit can't be drawn
        #   1 otherwise

        my ($self, $roomObj, $exitObj, $check) = @_;

        # Local variables
        my ($mapDir, $lineListRef, $squareListRef, $triangleListRef, $xPos, $yPos, $newObj);

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawRetracingExit', @_);
        }

        # Fetch the equivalent primary direction (the direction in which the exit is drawn on the
        #   map)
        $mapDir = $exitObj->mapDir;
        if (! $mapDir) {

            return undef;
        }

        # Draw 'up' and 'down' - the letters 'U' and 'D' in the bottom corners of the room
        if ($mapDir eq 'up' || $mapDir eq 'down') {

            $self->drawUpDown($roomObj, $exitObj);

        # Draw cardinal directions (the sixteen primary directions which aren't 'up' and 'down')
        } else {

            # Find the position of an incomplete exit. The first two values in the list, which is
            #   the point at which the exit meets the room, give us the coordinates of a triangle
            #   corner
            $lineListRef = $self->ivShow('preDrawnIncompleteExitHash', $mapDir);

            # Find the position of a broken or region exit. Two of the corners are used as the
            #   remaining corners in the triangle
            $squareListRef = $self->ivShow('preDrawnSquareExitHash', $mapDir);
            # Make sure the four elements in @$squareListRef are in the order
            #   (top_left_x, top_left_y, bottom_right_x, bottom_right_y)
            if ($$squareListRef[0] > $$squareListRef[2]) {

                ($$squareListRef[0], $$squareListRef[2]) = ($$squareListRef[2], $$squareListRef[0]);
            }
            if ($$squareListRef[1] > $$squareListRef[3]) {

                ($$squareListRef[1], $$squareListRef[3]) = ($$squareListRef[3], $$squareListRef[1]);
            }

            # $self->constTriangleCornerHash tells us which of the square's four corners to use
            $triangleListRef = $self->ivShow('constTriangleCornerHash', $mapDir);

            # Get the position of $roomObj's gridblock
            $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
            $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

            # Draw the canvas object
            $newObj = Gnome2::Canvas::Item->new(
                $self->canvasRoot,
                'Gnome2::Canvas::Polygon',
                points => [
                    # Triangle corner nearest the room
                    $$lineListRef[0] + $xPos,
                    $$lineListRef[1] + $yPos,
                    # Second triangle corner
                    $$squareListRef[$$triangleListRef[0]] + $xPos,
                    $$squareListRef[$$triangleListRef[1]] + $yPos,
                    # Third triangle corner
                    $$squareListRef[$$triangleListRef[2]] + $xPos,
                    $$squareListRef[$$triangleListRef[3]] + $yPos,
                    # Back to the beginning
                    $$lineListRef[0] + $xPos,
                    $$lineListRef[1] + $yPos,
                ],
                outline_color => $self->getExitColour($exitObj),
            );

            # Set the object's position in the canvas drawing stack
            $self->setExitLevel($newObj);

            # Set up the event handler for the canvas object
            $self->setupCanvasObjEvent('exit', $newObj, $exitObj);

            # Store the canvas object
            $self->ivAdd('drawnExitHash', $exitObj->number, [$newObj]);
        }

        return 1;
    }

    sub drawInteriorText {

        # Called by $self->drawUpDown, ->drawInteriorCounts, ->drawInteriorVisits,
        #   ->drawInteriorText and ->drawRoomFlagText
        # Draws some pango text in a room's interior at ones of five positions - the top-left or
        #   top-right corners, or at the bottom-left, bottom-centre or bottom-right
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the GA::ModelObj::Room being drawn
        #   $borderCornerXPosPixels, $borderCornerYPosPixels
        #               - Coordinates of the pixel at the top-left corner of the room's border
        #   $corner     - Which corner to use
        #                   - 0 for top-left
        #                   - 1 for top-right
        #                   - 2 for bottom-left     (reserved for 'up' exits)
        #                   - 3 for bottom-centre   (reserved for unallocatable exits)
        #                   - 4 for bottom-right    (reserved for 'down' exits)
        #
        # Optional arguments
        #   $text       - The text to draw. If an empty string or 'undef', nothing is drawn
        #   $style      - The text's style, 'normal', 'oblique' or 'italic'. If 'undef', 'normal' is
        #                   used
        #   $underline  - The text's underline, 'none', 'single', 'double' or 'error'. If 'undef',
        #                   'none' is used (NB Only intended to be used for up/down exits)
        #   $weight     - The text's weight. A normal weight is 400; a bold weight is 600. If
        #                   'undef', 400 is used (NB Only intended to be used for up/down exits)
        #
        # Return values
        #   'undef' on improper arguments, or if the text is too small to be drawn
        #   1 otherwise

        my (
            $self, $roomObj, $borderCornerXPosPixels, $borderCornerYPosPixels, $corner,
            $text, $style, $underline, $weight, $check
        ) = @_;

        # Local variables
        my (
            $textXPosPixels, $textYPosPixels, $textSizePixels, $listRef, $newObj, $posn, $yMod,
            @newList,
        );

        # Check for improper arguments
        if (
            ! defined $roomObj || ! defined $borderCornerXPosPixels
            || ! defined $borderCornerYPosPixels || ! defined $corner || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawInteriorText', @_);
        }

        # Check that, for very small rooms, we don't try to draw text with a size of less than 1
        #   pixel
        if ($self->drawRoomTextSize < 1) {

            # Draw nothing
            return undef;
        }

        # Set the text's position
        if ($corner == 0) {

            # Top-left corner
            $textXPosPixels = $borderCornerXPosPixels + 1;
            $textYPosPixels = $borderCornerYPosPixels + 1;

        } elsif ($corner == 1) {

            # Top-right corner
            $textXPosPixels = $borderCornerXPosPixels + int($self->drawRoomTextWidth / 2) + 1;
            $textYPosPixels = $borderCornerYPosPixels + 1;

        } elsif ($corner == 2) {

            # Bottom-left corner (reserved for 'up')
            $textXPosPixels = $borderCornerXPosPixels + 1;
            $textYPosPixels = $borderCornerYPosPixels + int($self->drawRoomTextHeight / 2) + 1;

        } elsif ($corner == 3) {

            # Bottom-centre (reserved for unallocatable exits)
            $textXPosPixels = $borderCornerXPosPixels + int($self->drawRoomTextWidth / 3) + 1;
            $textYPosPixels = $borderCornerYPosPixels + int($self->drawRoomTextHeight / 2) + 1;

        } elsif ($corner == 4) {

            # Bottom right-corner (reserved for 'down')
            $textXPosPixels = $borderCornerXPosPixels + int(($self->drawRoomTextWidth * 2) / 3);
            $textYPosPixels = $borderCornerYPosPixels + int($self->drawRoomTextHeight / 2) + 1;
        }

        # Set the style, if not specified
        if (! $style) {

            $style = 'normal';
        }

        # Set the underline, if not specified. Some underline settings need us to move the
        #   character up a few pixels, in which case $yMod is set
        $yMod = 0;

        if (! $underline) {
            $underline = 'none';
        } elsif ($underline eq 'single') {
            $yMod = -2;
        } elsif ($underline eq 'error') {
            $yMod = -2;     # -3 would be consistent with other values
        } elsif ($underline eq 'double') {
            $yMod = -4;     # not currently used by any text
        }

        # Set the weight, if not specified
        if (! $weight) {

            $weight = 400;
        }

        # Draw the canvas object
        $newObj = Gnome2::Canvas::Item->new(
            $self->canvasRoot,
            'Gnome2::Canvas::Text',
            x => $textXPosPixels,
            y => $textYPosPixels + $yMod,
            font => $self->worldModelObj->mapFont,
            size => int($self->drawRoomTextSize * $self->worldModelObj->roomTextRatio),
            fill_color => $self->worldModelObj->roomTextColour,
            anchor => 'GTK_ANCHOR_NW',
            text => $text,
            style => $style,
            underline => $underline,
            weight => $weight,
        );

        # Set the object's position in the canvas drawing stack
        $self->setTextLevel($newObj);

        # (No event handler for this canvas object)

        # Store the canvas object
        $posn = $roomObj->xPosBlocks . '_' . $roomObj->yPosBlocks . '_' . $roomObj->zPosBlocks;
        if ($self->ivExists('drawnRoomTextHash', $posn)) {

            $listRef = $self->ivShow('drawnRoomTextHash', $posn);
            @newList = (@$listRef, $newObj);

        } else {

            @newList = ($newObj);
        }

        $self->ivAdd('drawnRoomTextHash', $posn, \@newList);

        return 1;
    }

    sub drawUpDown {

        # Called by $self->drawIncompleteExit, $self->drawOneWayExit, $self->drawTwoWayExit (etc)
        # To show the exits 'up' and 'down', draws the letter 'U' or 'D' in one of the bottom
        #   corners of a room on the map
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the parent GA::ModelObj::Room
        #   $exitObj    - Blessed reference of the GA::Obj::Exit being drawn
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $roomObj, $exitObj, $check) = @_;

        # Local variables
        my ($mapDir, $xPos, $yPos, $text, $style, $weight, $underline);

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawUpDown', @_);
        }

        # Get the primary direction ('up' or 'down'). The calling function should already have
        #   eliminated exits whose ->mapDir is set to other values (or to 'undef')
        $mapDir = $exitObj->mapDir;

        # Get the position of $roomObj's gridblock
        $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
        $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

        # Broken/region exits are drawn with an underline
        if ($exitObj->brokenFlag) {
            $underline = 'single';
        } elsif ($exitObj->regionFlag) {
            $underline = 'error';       # Wavy line
        }

        # Impassable and retracing exits are drawn bold. Other exit ornaments are drawn oblique
        if ($exitObj->impassFlag || $exitObj->retraceFlag) {

            $weight = 600;
            $style = 'normal';

        } else {

            $weight = 400;

            if ($exitObj->ornamentFlag) {
                $style = 'oblique';
            } elsif ($exitObj->randomType ne 'none') {
                $style = 'italic';
            } else {
                $style = 'normal';
            }
        }

        # Set the letter to be used, and its size and position
        if ($mapDir eq 'up') {

            # Decide which letter to use (upper case for 1-way/2-way exits, lower case for
            #   uncertain/incomplete exits)
            if ($exitObj->twinExit || $exitObj->oneWayFlag) {
                $text = 'U';
            } else {
                $text = 'u';
            }

            # Draw the letter
            $self->drawInteriorText(
                $roomObj,
                $self->borderCornerXPosPixels + $xPos,
                $self->borderCornerYPosPixels + $yPos,
                2,          # Bottom-left corner
                $text,
                $style,
                $underline,
                $weight,
            );

        } elsif ($mapDir eq 'down') {

            # Decide which letter to use (upper case for 1-way/2-way exits, lower case for
            #   uncertain/incomplete exits)
            if ($exitObj->twinExit || $exitObj->oneWayFlag) {
                $text = 'D';
            } else {
                $text = 'd';
            }

            # Draw the letter
            $self->drawInteriorText(
                $roomObj,
                $self->borderCornerXPosPixels + $xPos,
                $self->borderCornerYPosPixels + $yPos,
                4,          # Bottom-right corner
                $text,
                $style,
                $underline,
                $weight,
            );
        }

        return 1;
    }

    sub drawUnallocatableCount {

        # Called by $self->drawRoom to display the number of unallocatable exits for this room in
        #   the bottom-centre of the room box
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the parent GA::ModelObj::Room
        #   $count      - The number of unallocatable exits in this room
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $roomObj, $count, $check) = @_;

        # Local variables
        my ($xPos, $yPos, $text);

        # Check for improper arguments
        if (! defined $roomObj || ! defined $count || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawUnallocatedCount', @_);
        }

        # There's only room to draw a single figure so, if $count > 9, draw a '+'
        if ($count > 9) {

            $count = '+';
        }

        # Get the position of $roomObj's gridblock
        $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
        $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

        # Draw the letter
        $self->drawInteriorText(
            $roomObj,
            $self->borderCornerXPosPixels + $xPos,
            $self->borderCornerYPosPixels + $yPos,
            3,          # Bottom-centre
            $count,
            'normal',
        );

        return 1;
    }

    sub drawInteriorCounts {

        # Called by $self->drawRoomInteriorInfo
        # Draws up to two numbers in the room's interior, giving information about its exits and/or
        #   contents
        # Because of limited space, the maximum number drawn is 9. Numbers above that are drawn as a
        #   '+'. The number 0 is not drawn at all
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the GA::ModelObj::Room being drawn
        #   $borderCornerXPosPixels, $borderCornerYPosPixels
        #               - Coordinates of the pixel at the top-left corner of the room's border
        #   $leftCount  - The number drawn in the top-left of the room's interior
        #   $rightCount - The number drawn in the top-right of the room's interior
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my (
            $self, $roomObj, $borderCornerXPosPixels, $borderCornerYPosPixels, $leftCount,
            $rightCount, $check
        ) = @_;

        # Check for improper arguments
        if (
            ! defined $roomObj || ! defined $borderCornerXPosPixels
            || ! defined $borderCornerYPosPixels || ! defined $leftCount || ! defined $rightCount
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawInteriorCounts', @_);
        }

        # Set maximum numbers
        if ($leftCount && $leftCount > 9) {

            $leftCount = '+';
        }

        if ($rightCount && $rightCount > 9) {

            $rightCount = '+';
        }

        if ($leftCount) {

            # Draw the count
            $self->drawInteriorText(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
                0,          # Top-left corner
                $leftCount,
            );
        }

        if ($rightCount) {

            # Draw the count
            $self->drawInteriorText(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
                1,          # Top-right corner
                $rightCount,
            );
        }

        return 1;
    }

    sub drawRoomFlagText {

        # Called by $self->drawRoomInteriorInfo
        # Draws the room flag text - two letters which are equivalent to the room's highest-priority
        #   room flag - in the room's interior
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the GA::ModelObj::Room being drawn
        #   $borderCornerXPosPixels, $borderCornerYPosPixels
        #               - Coordinates of the pixel at the top-left corner of the room's border
        #   $roomFlag   - The room flag to draw. Matches a key in
        #                   GA::Obj::WorldModel->roomFlagTextHash
        #
        # Return values
        #   'undef' on improper arguments, or if the correct room flag text can't be found
        #   1 otherwise

        my (
            $self, $roomObj, $borderCornerXPosPixels, $borderCornerYPosPixels, $roomFlag, $check,
        ) = @_;

        # Local variables
        my $text;

        # Check for improper arguments
        if (
            ! defined $roomObj || ! defined $borderCornerXPosPixels
            || ! defined $borderCornerYPosPixels || ! defined $roomFlag || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawRoomFlagText', @_);
        }

        # Get the label itself (A two-letter scalar, e.g. 'St' for stash rooms)
        $text = $self->worldModelObj->ivShow('roomFlagTextHash', $roomFlag);
        if ($text) {

            # Draw the label
            $self->drawInteriorText(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
                0,          # Top-left corner
                $text,
            );
        }

        return 1;
    }

    sub drawRoomSourceText {

        # Called by $self->drawRoomInteriorInfo
        # Draws the first four letters of the room's filename in the room's interior (this function
        #   assumes that the room has a ->sourceCodePath value set)
        #
        # Expected arguments
        #   $roomObj    - Blessed reference of the GA::ModelObj::Room being drawn
        #   $borderCornerXPosPixels, $borderCornerYPosPixels
        #               - Coordinates of the pixel at the top-left corner of the room's border
        #
        # Return values
        #   'undef' on improper arguments, or if the correct room flag text can't be found
        #   1 otherwise

        my ($self, $roomObj, $borderCornerXPosPixels, $borderCornerYPosPixels, $check) = @_;

        # Local variables
        my $text;

        # Check for improper arguments
        if (
            ! defined $roomObj || ! defined $borderCornerXPosPixels
            || ! defined $borderCornerYPosPixels || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawRoomFlagText', @_);
        }

        # Split a file path like /home/name/ds/lib/domains/town/room/start.c into path,
        #   filename and extension (we only want the filename)
        $roomObj->sourceCodePath =~ m/^(.*\/)?(?:$|(.+?)(?:(\.[^.]*$)|$))/;
        # Get the first three characters of the filename (e.g. 'sta')
        $text = substr($2, 0, 3);
        if ($text) {

            # Draw the text
            $self->drawInteriorText(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
                0,          # Top-left corner
                $text,
            );
        }

        return 1;
    }

    sub drawInteriorVisits {

        # Called by $self->drawRoomInteriorInfo
        # Draws a single number in the room's interior, showing the number of times a character has
        #   visited the room
        # Because of limited space, the maximum number drawn is 999. Numbers above that are drawn
        #   as '1k', '2k', or even '1m' (etc). The number 0 is not drawn at all
        #
        # Expected arguments
        #   $roomObj    - The GA::ModelObj::Room being drawn
        #   $borderCornerXPosPixels, $borderCornerYPosPixels
        #               - Coordinates of the pixel at the top-left corner of the room's border
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $roomObj, $borderCornerXPosPixels, $borderCornerYPosPixels, $check) = @_;

        # Local variables
        my ($name, $visitCount);

        # Check for improper arguments
        if (
            ! defined $roomObj || ! defined $borderCornerXPosPixels
            || ! defined $borderCornerYPosPixels || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawInteriorVisits', @_);
        }

        # If $self->showChar specifies a character, show that character's visits. Otherwise, show
        #   visits of the current character (if there is one)
        if ($self->showChar) {
            $name = $self->showChar;
        } elsif ($self->session->currentChar) {
            $name = $self->session->currentChar->name;
        }

        if ($name && $roomObj->ivExists('visitHash', $name)) {

            # Get the number of times character $name has visited the room $roomObj
            $visitCount = $roomObj->ivShow('visitHash', $name);

            # Set maximum number to draw (because of limited space inside the room)
            if ($visitCount && $visitCount > 999999) {

                $visitCount = '1m+';        # Absolute maximum

            } elsif ($visitCount > 999) {

                $visitCount = int($visitCount / 1000) . 'k';
            }

            # Draw the number of visits
            $self->drawInteriorText(
                $roomObj,
                $borderCornerXPosPixels,
                $borderCornerYPosPixels,
                0,          # Top-left corner
                $visitCount,
            );
        }

        return 1;
    }

    sub drawInteriorProfiles {

        # Called by $self->drawRoomInteriorInfo
        # Draws two bits of text in the room's interior - a symbol to show whether the room's
        #   ->exclusiveFlag is set, and the number of exclusive profiles for the room (if there
        #   is only one, the first two characters of the profile's name are drawn instead)
        #
        # Expected arguments
        #   $roomObj    - The GA::ModelObj::Room being drawn
        #   $borderCornerXPosPixels, $borderCornerYPosPixels
        #               - Coordinates of the pixel at the top-left corner of the room's border
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $roomObj, $borderCornerXPosPixels, $borderCornerYPosPixels, $check) = @_;

        # Local variables
        my (
            $string,
            @profList,
        );

        # Check for improper arguments
        if (
            ! defined $roomObj || ! defined $borderCornerXPosPixels
            || ! defined $borderCornerYPosPixels || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawInteriorProfiles', @_);
        }

        # Create a string to draw
        if ($roomObj->exclusiveFlag) {
            $string = '*';
        } else {
            $string = ' ';
        }

        if ($roomObj->exclusiveHash) {

            @profList = $roomObj->ivKeys('exclusiveHash');

            # Only one exclusive profile, so use its name
            if (@profList == 1) {

                $string .= substr($profList[0], 0, 3);

            # More than one exclusive profile, so use the number of profiles
            } elsif (@profList > 99) {

                # Number is too large to fit
                $string .= '99+';

            } else {

                $string .= scalar @profList;
            }
        }

        # Draw the string
        $self->drawInteriorText(
            $roomObj,
            $borderCornerXPosPixels,
            $borderCornerYPosPixels,
            0,          # Top-left corner
            $string,
        );

        return 1;
    }

    sub drawExitOrnaments {

        # Called by $self->drawOneWayExit, etc
        # Checks the ornament flags for a GA::Obj::Exit and/or its twin GA::Obj::Exit and,
        #   depending on which ornaments need to be drawn, calls the relevant functions
        # (NB Impassable exits have their failed exit ornament drawn by ->drawImpassableExit at the
        #   same time as the exit is drawn)
        #
        # Expected arguments
        #   $exitObj        - The GA::Obj::Exit being drawn in a drawing cycle initiated by
        #                       $self->doDraw
        #   $colour         - The colour in which the whole exit is being drawn
        #   $xPos, $yPos    - Coordinates of the gridblock occupied by GA::Obj::Exit's parent room
        #
        # Optional arguments
        #   $twinExitObj    - The blessed reference of the twin GA::Obj::Exit, if there is one
        #                       (otherwise set to 'undef')
        #   $destXPos, $destYPos
        #                   - Coordinates of the gridblock occupied by the destination room
        #                       ('undef' if $exitObj is an incomplete or uncertain exit)
        #   $posnListRef    - For incomplete and uncertain exits, a reference to a list defining the
        #                       position of the exit actually drawn. The ornament is drawn at the
        #                       centre of the line. (Otherwise set to 'undef', and the ornament is
        #                       drawn at the centre of a hypothetical uncertain exit)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my (
            $self, $exitObj, $colour, $xPos, $yPos, $twinExitObj, $destXPos, $destYPos,
            $posnListRef, $check,
        ) = @_;

        # Check for improper arguments
        if (
            ! defined $exitObj || ! defined $colour || ! defined $xPos || ! defined $yPos
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawExitOrnaments', @_);
        }

        # Draw an ornament for $exitObj (if this GA::Obj::Exit has an ornament)
        if ($exitObj->ornamentFlag) {

            if ($exitObj->breakFlag) {

                $self->drawBreakableOrnament(
                    $exitObj, $exitObj,
                    $colour,
                    $xPos, $yPos,
                    $posnListRef,
                );

            } elsif ($exitObj->pickFlag) {

                $self->drawPickableOrnament(
                    $exitObj, $exitObj,
                    $colour,
                    $xPos, $yPos,
                    $posnListRef,
                );

            } elsif ($exitObj->lockFlag) {

                $self->drawLockableOrnament(
                    $exitObj, $exitObj,
                    $colour,
                    $xPos, $yPos,
                    $posnListRef,
                );

            } elsif ($exitObj->openFlag) {

                $self->drawOpenableOrnament(
                    $exitObj, $exitObj,
                    $colour,
                    $xPos, $yPos,
                    $posnListRef,
                );
            }
        }

        # Draw an ornament for $twinExitObj (if this GA::Obj::Exit has an ornament)
        if ($twinExitObj && $twinExitObj->ornamentFlag) {

            if ($twinExitObj->breakFlag) {

                $self->drawBreakableOrnament(
                    $twinExitObj, $exitObj,
                    $colour,
                    $destXPos, $destYPos,
                    $posnListRef,
                );

            } elsif ($twinExitObj->pickFlag) {

                $self->drawPickableOrnament(
                    $twinExitObj, $exitObj,
                    $colour,
                    $destXPos, $destYPos,
                    $posnListRef,
                );

            } elsif ($twinExitObj->lockFlag) {

                $self->drawLockableOrnament(
                    $twinExitObj, $exitObj,
                    $colour,
                    $destXPos, $destYPos,
                    $posnListRef,
                );

            } elsif ($twinExitObj->openFlag) {

                $self->drawOpenableOrnament(
                    $twinExitObj, $exitObj,
                    $colour,
                    $destXPos, $destYPos,
                    $posnListRef,
                );
            }
        }

        return 1;
    }

    sub drawBreakableOrnament {

        # Called by $self->drawExitOrnaments
        # Draws an exit ornament that shows the exit is breakable
        #
        # Expected arguments
        #   $parentObj      - The GA::Obj::Exit to which this ornament belongs
        #   $drawObj        - The GA::Obj::Exit which is actually being drawn in the current draw
        #                       cycle (initiated after a call to $self->doDraw). An exit and its
        #                       twin exit are drawn as a single object; if we're drawing an ornament
        #                       for an exit, $parentObj and $drawObj will be the same, but if we're
        #                       drawing an ornament for the twin, $drawObj will be the exit and
        #                       $parentObj will be the twin
        #   $colour         - The colour in which the whole exit is being drawn
        #   $xPos, $yPos    - Coordinates of the gridblock occupied by the parent room of the
        #                       GA::Obj::Exit whose ornament is being drawn (i.e. $parentObj)
        #
        # Optional arguments
        #   $posnListRef    - For incomplete and uncertain exits, a reference to list defining the
        #                       position of the exit actually drawn. The ornament is drawn at the
        #                       centre of the line. (Otherwise set to 'undef', and the ornament is
        #                       drawn at the centre of a hypothetical uncertain exit)
        #
        # Return values
        #   'undef' on improper arguments or if the ornament can't be drawn
        #   1 otherwise

        my ($self, $parentObj, $drawObj, $colour, $xPos, $yPos, $posnListRef, $check) = @_;

        # Local variables
        my (
            $vectorRef, $perpVectorRef, $startXPosPixels, $startYPosPixels, $stopXPosPixels,
            $stopYPosPixels, $exitMiddleXPosPixels, $exitMiddleYPosPixels, $perpStartXPosPixels,
            $perpStartYPosPixels, $perpStopXPosPixels, $perpStopYPosPixels, $newObj, $newObj2,
            $newObj3, $listRef, $posnSet,
            @polygonList,
        );

        # Check for improper arguments
        if (
            ! defined $parentObj || ! defined $drawObj || ! defined $colour || ! defined $xPos
            || ! defined $yPos || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawBreakableOrnament', @_);
        }

        if (! defined $posnListRef) {

            if (! $parentObj->mapDir) {

                # (Almost certainly an unallocatable exit)
                return undef;
            }

            # An uncertain exit would be drawn from the edge of the room to the edge of the
            #   gridblock. The exit ornament is drawn in the middle of this hypothetical line. Find
            #   the position of the line
            $posnListRef = $self->ivShow('preDrawnUncertainExitHash', $parentObj->mapDir);
        }

        # Get the coordinates at the pixel in the middle of this hypothetical line
        ($exitMiddleXPosPixels, $exitMiddleYPosPixels) = $self->findExitMiddle(
            ($$posnListRef[0] + $xPos), ($$posnListRef[1] + $yPos),
            ($$posnListRef[2] + $xPos), ($$posnListRef[3] + $yPos),
        );

        # Get the vector of the exit line
        $vectorRef = $self->ivShow('constVectorHash', $parentObj->mapDir);
        # Get the vector of the perpendicular to the exit line
        $perpVectorRef = $self->ivShow('constPerpVectorHash', $parentObj->mapDir);

        # The ornament is made of up one line drawn perpendicular to the exit, with two smaller
        #   lines at each end of this perpendicular, running parallel with the exit line - in other
        #   words:
        #
        #       XXX         X - Ornament
        #        X          e - Exit line
        # eeeeeeeXeeeeeee
        #        X
        #       XXX
        #
        # The perpendicular is exactly the same as the line drawn in an openable ornament
        # The two smaller lines are opposite sides of the polygon drawn in a pickable ornament

        # Find the ends of each half of the perpendicular line
        $perpStartXPosPixels = $exitMiddleXPosPixels + (
            $$perpVectorRef[0] * int(
                ($self->currentRegionmap->blockWidthPixels
                    - $self->currentRegionmap->roomWidthPixels
                ) / 3
            )
        );
        $perpStartYPosPixels = $exitMiddleYPosPixels + (
            $$perpVectorRef[1] * int(
                ($self->currentRegionmap->blockHeightPixels
                    - $self->currentRegionmap->roomHeightPixels
                ) / 3
            )
        );

        $perpStopXPosPixels = $exitMiddleXPosPixels + (
            $$perpVectorRef[2] * int(
                ($self->currentRegionmap->blockWidthPixels
                    - $self->currentRegionmap->roomWidthPixels
                ) / 3
            )
        );
        $perpStopYPosPixels = $exitMiddleYPosPixels + (
            $$perpVectorRef[3] * int(
                ($self->currentRegionmap->blockHeightPixels
                    - $self->currentRegionmap->roomHeightPixels
                ) / 3
            )
        );

        # Draw the canvas object
        $newObj = Gnome2::Canvas::Item->new (
            $self->canvasRoot,
            'Gnome2::Canvas::Line',
            points => [
                $perpStartXPosPixels,
                $perpStartYPosPixels,
                $perpStopXPosPixels,
                $perpStopYPosPixels,
            ],
            fill_color => $colour,
            width_units => 1,
            cap_style => 'projecting',
        );

        # Now get the vertices of the polygon that would have been drawn in a pickable
        #   ornament
        push (@polygonList, $self->findPerpendicular(
                $vectorRef, $perpVectorRef, -1,
                $exitMiddleXPosPixels,
                $exitMiddleYPosPixels,
            )
        );

        ($startXPosPixels, $startYPosPixels, $stopXPosPixels, $stopYPosPixels)
            = $self->findPerpendicular(
                $vectorRef, $perpVectorRef, 1,
                $exitMiddleXPosPixels,
                $exitMiddleYPosPixels,
            );

        push (@polygonList, $stopXPosPixels, $stopYPosPixels, $startXPosPixels, $startYPosPixels);

        # Draw lines between adjacent points on the polygon
        $newObj2 = Gnome2::Canvas::Item->new (
            $self->canvasRoot,
            'Gnome2::Canvas::Line',
            points => [
                $polygonList[0], $polygonList[1],
                $polygonList[6], $polygonList[7],
            ],
            fill_color => $colour,
            width_units => 1,
            cap_style => 'projecting',
        );

        $newObj3 = Gnome2::Canvas::Item->new (
            $self->canvasRoot,
            'Gnome2::Canvas::Line',
            points => [
                $polygonList[2], $polygonList[3],
                $polygonList[4], $polygonList[5],
            ],
            fill_color => $colour,
            width_units => 1,
            cap_style => 'projecting',
        );

        # Set the objects' position in the canvas drawing stack
        $posnSet = $self->setExitLevel($newObj);
        $self->setExitLevel($newObj2, $posnSet);
        $self->setExitLevel($newObj3, $posnSet);

        # Set up the event handler for the canvas objects
        $self->setupCanvasObjEvent('exit', $newObj, $drawObj);
        $self->setupCanvasObjEvent('exit', $newObj2, $drawObj);
        $self->setupCanvasObjEvent('exit', $newObj3, $drawObj);

        # Store the canvas objects
        if ($self->ivExists('drawnOrnamentHash', $drawObj->number)) {

            $listRef = $self->ivShow('drawnOrnamentHash', $drawObj->number);
        }

        push (@$listRef, $newObj, $newObj2, $newObj3);
        $self->ivAdd('drawnOrnamentHash', $drawObj->number, $listRef);

        return 1;
    }

    sub drawPickableOrnament {

        # Called by $self->drawExitOrnaments
        # Draws an exit ornament that shows the exit is pickable
        #
        # Expected arguments
        #   $parentObj      - The GA::Obj::Exit to which this ornament belongs
        #   $drawObj        - The GA::Obj::Exit which is actually being drawn in the current draw
        #                       cycle (initiated after a call to $self->doDraw). An exit and its
        #                       twin exit are drawn as a single object; if we're drawing an ornament
        #                       for an exit, $parentObj and $drawObj will be the same, but if we're
        #                       drawing an ornament for the twin, $drawObj will be the exit and
        #                       $parentObj will be the twin
        #   $colour         - The colour in which the whole exit is being drawn
        #   $xPos, $yPos    - Coordinates of the gridblock occupied by the parent room of the
        #                       GA::Obj::Exit whose ornament is being drawn (i.e. $parentObj)
        #
        # Optional arguments
        #   $posnListRef    - For incomplete and uncertain exits, a reference to list defining the
        #                       position of the exit actually drawn. The ornament is drawn at the
        #                       centre of the line. (Otherwise set to 'undef', and the ornament is
        #                       drawn at the centre of a hypothetical uncertain exit)
        #
        # Return values
        #   'undef' on improper arguments or if the ornament can't be drawn
        #   1 otherwise

        my ($self, $parentObj, $drawObj, $colour, $xPos, $yPos, $posnListRef, $check) = @_;

        # Local variables
        my (
            $vectorRef, $perpVectorRef, $startXPosPixels, $startYPosPixels, $stopXPosPixels,
            $stopYPosPixels, $exitMiddleXPosPixels, $exitMiddleYPosPixels, $newObj, $listRef,
            @polygonList,
        );

        # Check for improper arguments
        if (
            ! defined $parentObj || ! defined $drawObj || ! defined $colour || ! defined $xPos
            || ! defined $yPos || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawPickableOrnament', @_);
        }

        if (! defined $posnListRef) {

            if (! $parentObj->mapDir) {

                # (Almost certainly an unallocatable exit)
                return undef;
            }

            # An uncertain exit would be drawn from the edge of the room to the edge of the
            #   gridblock. The exit ornament is drawn in the middle of this hypothetical line. Find
            #   the position of the line
            $posnListRef = $self->ivShow('preDrawnUncertainExitHash', $parentObj->mapDir);
        }

        # Get the coordinates at the pixel in the middle of this hypothetical line
        ($exitMiddleXPosPixels, $exitMiddleYPosPixels) = $self->findExitMiddle(
            ($$posnListRef[0] + $xPos), ($$posnListRef[1] + $yPos),
            ($$posnListRef[2] + $xPos), ($$posnListRef[3] + $yPos),
        );

        # Get the vector of the exit line
        $vectorRef = $self->ivShow('constVectorHash', $parentObj->mapDir);
        # Get the vector of the perpendicular to the exit line
        $perpVectorRef = $self->ivShow('constPerpVectorHash', $parentObj->mapDir);

        # The ornament is made up of two lines running parallel to each other, which is then drawn
        #   as a polygon (in Gnome2::Canvas, actually a series of lines, each ending where the
        #   previous one stopped)
        # Call ->findPerpendicular to find the coordinates of both ends of each line, and store them
        #   in @polygonList - a sequential list of coordinates of the vertices of the polygon,
        #   i.e. (x, y, x, y, ...)
        push (@polygonList, $self->findPerpendicular(
                $vectorRef, $perpVectorRef, -1,
                $exitMiddleXPosPixels,
                $exitMiddleYPosPixels,
            )
        );

        ($startXPosPixels, $startYPosPixels, $stopXPosPixels, $stopYPosPixels)
            = $self->findPerpendicular(
                $vectorRef, $perpVectorRef, 1,
                $exitMiddleXPosPixels,
                $exitMiddleYPosPixels,
            );

        push (@polygonList, $stopXPosPixels, $stopYPosPixels, $startXPosPixels, $startYPosPixels);

        # The first pixel in the list must be added again at the end, in order to complete the
        #   polygon
        push (@polygonList, $polygonList[0], $polygonList[1]);

        # Draw the canvas object
        $newObj = Gnome2::Canvas::Item->new (
            $self->canvasRoot,
            'Gnome2::Canvas::Line',
            points => \@polygonList,
            fill_color => $colour,
            width_units => 1,
            cap_style => 'projecting',
        );

        # Set the object's position in the canvas drawing stack
        $self->setExitLevel($newObj);

        # Set up the event handler for the canvas object
        $self->setupCanvasObjEvent('exit', $newObj, $drawObj);

        # Store the canvas object
        if ($self->ivExists('drawnOrnamentHash', $drawObj->number)) {

            $listRef = $self->ivShow('drawnOrnamentHash', $drawObj->number);
        }

        push (@$listRef, $newObj);
        $self->ivAdd('drawnOrnamentHash', $drawObj->number, $listRef);

        return 1;
    }

    sub drawLockableOrnament {

        # Called by $self->drawExitOrnaments
        # Draws an exit ornament that shows the exit is lockable (like a door)
        #
        # Expected arguments
        #   $parentObj      - The GA::Obj::Exit to which this ornament belongs
        #   $drawObj        - The GA::Obj::Exit which is actually being drawn in the current draw
        #                       cycle (initiated after a call to $self->doDraw). An exit and its
        #                       twin exit are drawn as a single object; if we're drawing an ornament
        #                       for an exit, $parentObj and $drawObj will be the same, but if we're
        #                       drawing an ornament for the twin, $drawObj will be the exit and
        #                       $parentObj will be the twin
        #   $colour         - The colour in which the whole exit is being drawn
        #   $xPos, $yPos    - Coordinates of the gridblock occupied by the parent room of the
        #                       GA::Obj::Exit whose ornament is being drawn (i.e. $parentObj)
        #
        # Optional arguments
        #   $posnListRef    - For incomplete and uncertain exits, a reference to list defining the
        #                       position of the exit actually drawn. The ornament is drawn at the
        #                       centre of the line. (Otherwise set to 'undef', and the ornament is
        #                       drawn at the centre of a hypothetical uncertain exit)
        #
        # Return values
        #   'undef' on improper arguments or if the ornament can't be drawn
        #   1 otherwise

        my ($self, $parentObj, $drawObj, $colour, $xPos, $yPos, $posnListRef, $check) = @_;

        # Local variables
        my (
            $exitMiddleXPosPixels, $exitMiddleYPosPixels, $vectorRef, $perpVectorRef,
            $perpStartXPosPixels, $perpStartYPosPixels, $perpStopXPosPixels, $perpStopYPosPixels,
            $listRef,
            @factorList, @newObjList,
        );

        # Check for improper arguments
        if (
            ! defined $parentObj || ! defined $drawObj || ! defined $colour || ! defined $xPos
            || ! defined $yPos || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawLockableOrnament', @_);
        }

        if (! defined $posnListRef) {

            if (! $parentObj->mapDir) {

                # (Almost certainly an unallocatable exit)
                return undef;
            }

            # An uncertain exit would be drawn from the edge of the room to the edge of the
            #   gridblock. The exit ornament is drawn in the middle of this hypothetical line. Find
            #   the position of the line
            $posnListRef = $self->ivShow('preDrawnUncertainExitHash', $parentObj->mapDir);
        }

        # Get the coordinates at the pixel in the middle of the exit line
        ($exitMiddleXPosPixels, $exitMiddleYPosPixels) = $self->findExitMiddle(
            ($$posnListRef[0] + $xPos), ($$posnListRef[1] + $yPos),
            ($$posnListRef[2] + $xPos), ($$posnListRef[3] + $yPos),
        );

        # Get the vector of the exit line, and the vector of the opposite direction
        $vectorRef = $self->ivShow('constVectorHash', $parentObj->mapDir);
        # Get the vector of the perpendicular to the exit line
        $perpVectorRef = $self->ivShow('constPerpVectorHash', $parentObj->mapDir);

        # The ornament is made up of two lines running parallel to each other, and both
        #   perpendicular to the exit line
        @factorList = (-1, 1);

        # Draw each line in turn, one pixel apart
        foreach my $factor (@factorList) {

            my ($newObj);

            ($perpStartXPosPixels, $perpStartYPosPixels, $perpStopXPosPixels, $perpStopYPosPixels)
                = $self->findPerpendicular(
                    $vectorRef,
                    $perpVectorRef,
                    $factor,
                    $exitMiddleXPosPixels,
                    $exitMiddleYPosPixels,
                );

            # Draw the canvas object
            $newObj = Gnome2::Canvas::Item->new (
                $self->canvasRoot,
                'Gnome2::Canvas::Line',
                points => [
                    $perpStartXPosPixels,
                    $perpStartYPosPixels,
                    $perpStopXPosPixels,
                    $perpStopYPosPixels,
                ],
                fill_color => $colour,
                width_units => 1,
                cap_style => 'projecting',
            );

            # Set the object's position in the canvas drawing stack
            $self->setExitLevel($newObj);

            # Set up the event handler for the canvas object
            $self->setupCanvasObjEvent('exit', $newObj, $drawObj);

            push (@newObjList, $newObj);
        }

        # Store the canvas objects
        if ($self->ivExists('drawnOrnamentHash', $drawObj->number)) {

            $listRef = $self->ivShow('drawnOrnamentHash', $drawObj->number);
        }

        push (@$listRef, @newObjList);
        $self->ivAdd('drawnOrnamentHash', $drawObj->number, $listRef);

        return 1;
    }

    sub drawOpenableOrnament {

        # Called by $self->drawExitOrnaments
        # Draws an exit ornament that shows the exit is openable (like a door)
        #
        # Expected arguments
        #   $parentObj      - The GA::Obj::Exit to which this ornament belongs
        #   $drawObj        - The GA::Obj::Exit which is actually being drawn in the current draw
        #                       cycle (initiated after a call to $self->doDraw). An exit and its
        #                       twin exit are drawn as a single object; if we're drawing an ornament
        #                       for an exit, $parentObj and $drawObj will be the same, but if we're
        #                       drawing an ornament for the twin, $drawObj will be the exit and
        #                       $parentObj will be the twin
        #   $colour         - The colour in which the whole exit is being drawn
        #   $xPos, $yPos    - Coordinates of the gridblock occupied by the parent room of the
        #                       GA::Obj::Exit whose ornament is being drawn (i.e. $parentObj)
        #
        # Optional arguments
        #   $posnListRef    - For incomplete and uncertain exits, a reference to list defining the
        #                       position of the exit actually drawn. The ornament is drawn at the
        #                       centre of the line. (Otherwise set to 'undef', and the ornament is
        #                       drawn at the centre of a hypothetical uncertain exit)
        #
        # Return values
        #   'undef' on improper arguments, or if the ornament can't be drawn
        #   1 otherwise

        my ($self, $parentObj, $drawObj, $colour, $xPos, $yPos, $posnListRef, $check) = @_;

        # Local variables
        my (
            $exitMiddleXPosPixels, $exitMiddleYPosPixels, $perpVectorRef, $perpStartXPosPixels,
            $perpStartYPosPixels, $perpStopXPosPixels, $perpStopYPosPixels, $newObj, $listRef,
        );

        # Check for improper arguments
        if (
            ! defined $parentObj || ! defined $drawObj || ! defined $colour || ! defined $xPos
            || ! defined $yPos || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->drawOpenableOrnament', @_);
        }

        if (! defined $posnListRef) {

            if (! $parentObj->mapDir) {

                # (Almost certainly an unallocatable exit)
                return undef;
            }

            # An uncertain exit would be drawn from the edge of the room to the edge of the
            #   gridblock. The exit ornament is drawn in the middle of this hypothetical line. Find
            #   the position of the line
            $posnListRef = $self->ivShow('preDrawnUncertainExitHash', $parentObj->mapDir);
        }

        # Get the coordinates at the pixel in the middle of this hypothetical line
        ($exitMiddleXPosPixels, $exitMiddleYPosPixels) = $self->findExitMiddle(
            ($$posnListRef[0] + $xPos), ($$posnListRef[1] + $yPos),
            ($$posnListRef[2] + $xPos), ($$posnListRef[3] + $yPos),
        );

        # Get the vector of the perpendicular to the exit line
        $perpVectorRef = $self->ivShow('constPerpVectorHash', $parentObj->mapDir);

        # Find the ends of each half of the perpendicular line
        $perpStartXPosPixels = $exitMiddleXPosPixels + (
            $$perpVectorRef[0] * int(
                ($self->currentRegionmap->blockWidthPixels
                    - $self->currentRegionmap->roomWidthPixels
                ) / 3
            )
        );
        $perpStartYPosPixels = $exitMiddleYPosPixels + (
            $$perpVectorRef[1] * int(
                ($self->currentRegionmap->blockHeightPixels
                    - $self->currentRegionmap->roomHeightPixels
                ) / 3
            )
        );

        $perpStopXPosPixels = $exitMiddleXPosPixels + (
            $$perpVectorRef[2] * int(
                ($self->currentRegionmap->blockWidthPixels
                    - $self->currentRegionmap->roomWidthPixels
                ) / 3
            )
        );
        $perpStopYPosPixels = $exitMiddleYPosPixels + (
            $$perpVectorRef[3] * int(
                ($self->currentRegionmap->blockHeightPixels
                    - $self->currentRegionmap->roomHeightPixels
                ) / 3
            )
        );

        # Draw the canvas object
        $newObj = Gnome2::Canvas::Item->new (
            $self->canvasRoot,
            'Gnome2::Canvas::Line',
            points => [
                $perpStartXPosPixels,
                $perpStartYPosPixels,
                $perpStopXPosPixels,
                $perpStopYPosPixels,
            ],
            fill_color => $colour,
            width_units => 1,
            cap_style => 'projecting',
        );

        # Set the object's position in the canvas drawing stack
        $self->setExitLevel($newObj);

        # Set up the event handler for the canvas object
        $self->setupCanvasObjEvent('exit', $newObj, $drawObj);

        # Store the canvas object
        if ($self->ivExists('drawnOrnamentHash', $drawObj->number)) {

            $listRef = $self->ivShow('drawnOrnamentHash', $drawObj->number);
        }

        push (@$listRef, $newObj);
        $self->ivAdd('drawnOrnamentHash', $drawObj->number, $listRef);

        return 1;
    }

    # Graphical lookup functions

    sub getBlockCorner {

        # Called by various functions
        # Find the coordinates on the pixmap of the pixel occupying the top-left corner of the
        #   specified gridblock
        #
        # Expected arguments
        #   $xPosBlocks, $yPosBlocks
        #           - Grid coordinates of a gridblock
        #
        # Optional arguments
        #   $flag   - If set to TRUE, the gridblock coordinates are checked for validity. If set to
        #               FALSE (or 'undef'), we assume that they're valid
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise, returns the pixel's coordinates as a list in the form (x, y)

        my ($self, $xPosBlocks, $yPosBlocks, $flag, $check) = @_;

        # Local variables
        my @emptyList;

        # Check for improper arguments
        if (! defined $xPosBlocks || ! defined $yPosBlocks || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->getBlockCorner', @_);
            return @emptyList;
        }

        # Check the arguments, if necessary
        if ($flag) {

            if (
                ! $self->currentRegionmap->checkGridBlock(
                    $xPosBlocks,
                    $yPosBlocks,
                    $self->currentRegionmap->currentLevel,
                )
            ) {
                return @emptyList;
            }
        }

        # Return the coordinates
        return (
            $xPosBlocks * $self->currentRegionmap->blockWidthPixels,
            $yPosBlocks * $self->currentRegionmap->blockHeightPixels,
        );
    }

    sub getBlockCentre {

        # Called by various functions
        # Find the coordinates on the pixmap of the pixel occupying the centre of the specified
        #   gridblock (and of the room, if there is one)
        #
        # Expected arguments
        #   $xPosBlocks, $yPosBlocks
        #           - Grid coordinates of a gridblock
        #
        # Optional arguments
        #   $blockCornerXPosPixels, $blockCornerYPosPixels
        #           - The coordinates of the top-left pixel of the gridblock (the return values of
        #               $self->getBlockCorner). If not specified, $self->getBlockCorner is called to
        #               get them)
        #   $flag   - If set to TRUE, the gridblock coordinates are checked for validity. If set to
        #               FALSE (or 'undef'), we assume that they're valid
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise, the pixel's coordinates in a list in the form (x, y)

        my (
            $self, $xPosBlocks, $yPosBlocks, $blockCornerXPosPixels, $blockCornerYPosPixels, $flag,
            $check
        ) = @_;

        # Local variables
        my @emptyList;

        # Check for improper arguments
        if (! defined $xPosBlocks || ! defined $yPosBlocks || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->getBlockCentre', @_);
            return @emptyList;
        }

        # Check the arguments, if necessary
        if ($flag) {

            if (
                ! $self->currentRegionmap->checkGridBlock(
                    $xPosBlocks,
                    $yPosBlocks,
                    $self->currentRegionmap->currentLevel,
                )
            ) {
                return @emptyList;
            }
        }

        # Get the coordinates of the block's top-left corner, if we don't have them
        if (! defined $blockCornerXPosPixels) {

            ($blockCornerXPosPixels, $blockCornerYPosPixels)
                = $self->getBlockCorner($xPosBlocks, $yPosBlocks);
        }

        # Return the coordinates
        return (
            $blockCornerXPosPixels + int($self->currentRegionmap->blockWidthPixels / 2),
            $blockCornerYPosPixels + int($self->currentRegionmap->blockHeightPixels / 2),
        );
    }

    sub getBorderCorner {

        # Called by various functions
        # Find the coordinates on the pixmap of the pixel occupying the top-left corner of a room's
        #   border in the specified gridblock (if a room is drawn in this gridblock)
        #
        # Expected arguments
        #   $xPosBlocks, $yPosBlocks
        #           - Grid coordinates of a gridblock
        #
        # Optional arguments
        #   $blockCornerXPosPixels, $blockCornerYPosPixels
        #           - The coordinates of the top-left pixel of the gridblock (the return values of
        #               $self->getBlockCorner). If not specified, $self->getBlockCorner is called to
        #               get them)
        #   $flag   - If set to TRUE, the gridblock coordinates are checked for validity. If set to
        #               FALSE (or 'undef'), we assume that they're valid
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise, the pixel's coordinates in a list in the form (x, y)

        my (
            $self, $xPosBlocks, $yPosBlocks, $blockCornerXPosPixels, $blockCornerYPosPixels, $flag,
            $check
        ) = @_;

        # Local variables
        my @emptyList;

        # Check for improper arguments
        if (! defined $xPosBlocks || ! defined $yPosBlocks || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->getBorderCorner', @_);
            return @emptyList;
        }

        # Check the arguments, if necessary
        if ($flag) {

            if (
                ! $self->currentRegionmap->checkGridBlock(
                    $xPosBlocks,
                    $yPosBlocks,
                    $self->currentRegionmap->currentLevel,
                )
            ) {
                return @emptyList;
            }
        }

        # Get the coordinates of the block's top-left corner, if we don't have them
        if (! defined $blockCornerXPosPixels) {

            ($blockCornerXPosPixels, $blockCornerYPosPixels)
                = $self->getBlockCorner($xPosBlocks, $yPosBlocks);
        }

        # Return the coordinates
        return (
            $blockCornerXPosPixels + int(
                (
                    $self->currentRegionmap->blockWidthPixels
                        - $self->currentRegionmap->roomWidthPixels
                ) / 2
            ),
            $blockCornerYPosPixels + int(
                (
                    $self->currentRegionmap->blockHeightPixels
                        - $self->currentRegionmap->roomHeightPixels
                ) / 2
            ),
        );
    }

    sub getBlockEdge {

        # Called by various functions
        # Find the coordinates on the pixmap of the pixel at the edge of a gridblock which is
        #   intersected by a cardinal exit
        #
        # Expected arguments
        #   $xPosBlocks, $yPosBlocks
        #           - Grid coordinates of a gridblock
        #   $vectorRef
        #           - A value in the hash $self->constVectorHash, matching the direction of the
        #               exit's primary direction. The value is a reference to a list, e.g.
        #               northeast > [1, -1, 0]
        #
        # Optional arguments
        #   $blockCentreXPosPixels, $blockCentreYPosPixels
        #           - The coordinates of the pixel at the centre of the gridblock (and of the room,
        #               if one is drawn there)
        #   $flag   - If set to TRUE, the gridblock coordinates are checked for validity. If set to
        #               FALSE (or 'undef'), we assume that they're valid
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise, the pixel's coordinates in a list in the form (x, y)

        my (
            $self, $xPosBlocks, $yPosBlocks, $vectorRef, $blockCentreXPosPixels,
            $blockCentreYPosPixels, $flag, $check,
        ) = @_;

        # Local variables
        my @emptyList;

        # Check for improper arguments
        if (
            ! defined $xPosBlocks || ! defined $yPosBlocks || ! defined $vectorRef || defined $check
        ) {
            $axmud::CLIENT->writeImproper($self->_objClass . '->getBlockEdge', @_);
            return @emptyList;
        }

        # Check the arguments, if necessary
        if ($flag) {

            if (
                ! $self->currentRegionmap->checkGridBlock(
                    $xPosBlocks,
                    $yPosBlocks,
                    $self->currentRegionmap->currentLevel,
                )
            ) {
                return @emptyList;
            }
        }

        # Get the coordinates of the block's centre, if we don't have them
        if (! defined $blockCentreXPosPixels) {

            ($blockCentreXPosPixels, $blockCentreYPosPixels)
                = $self->getBlockCentre($xPosBlocks, $yPosBlocks);
        }

        # Return the coordinates
        return (
            $blockCentreXPosPixels + (
                $$vectorRef[0] * int(
                    $self->currentRegionmap->blockWidthPixels / 2
                )
            ),
            $blockCentreYPosPixels + (
                $$vectorRef[1] * int(
                    $self->currentRegionmap->blockHeightPixels / 2
                )
            ),
        );
    }

    sub getNearBlockEdge {

        # Called by various functions
        # Find the coordinates on the pixmap of the pixel near to the edge of a gridblock which is
        #   intersected by a cardinal exit (but not actually at the edge)
        #
        # Expected arguments
        #   $xPosBlocks, $yPosBlocks
        #           - Grid coordinates of a gridblock
        #   $vectorRef
        #           - A value in the hash $self->constVectorHash, matching the direction of the
        #               exit's primary direction. The value is a reference to a list, e.g.
        #               northeast > [1, -1, 0]
        #
        # Optional arguments
        #   $blockCentreXPosPixels, $blockCentreYPosPixels
        #           - The coordinates of the pixel at the centre of the gridblock (and of the room,
        #               if one is drawn there)
        #   $flag   - If set to TRUE, the gridblock coordinates are checked for validity. If set to
        #               FALSE (or 'undef'), we assume that they're valid
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise, the pixel's coordinates in a list in the form (x, y)

        my (
            $self, $xPosBlocks, $yPosBlocks, $vectorRef, $blockCentreXPosPixels,
            $blockCentreYPosPixels, $flag, $check,
        ) = @_;

        # Local variables
        my @emptyList;

        # Check for improper arguments
        if (
            ! defined $xPosBlocks || ! defined $yPosBlocks || ! defined $vectorRef
            || defined $check
        ) {
            $axmud::CLIENT->writeImproper($self->_objClass . '->getNearBlockEdge', @_);
            return @emptyList;
        }

        # Check the arguments, if necessary
        if ($flag) {

            if (
                ! $self->currentRegionmap->checkGridBlock(
                    $xPosBlocks,
                    $yPosBlocks,
                    $self->currentRegionmap->currentLevel,
                )
            ) {
                return @emptyList;
            }
        }

        # Get the coordinates of the block's centre, if we don't have them
        if (! defined $blockCentreXPosPixels) {

            ($blockCentreXPosPixels, $blockCentreYPosPixels)
                = $self->getBlockCentre($xPosBlocks, $yPosBlocks);
        }

        # Return the coordinates
        return (
            $blockCentreXPosPixels + (
                $$vectorRef[0] * (
                    int($self->currentRegionmap->blockWidthPixels / 2) - int(
                        ($self->currentRegionmap->blockWidthPixels
                            - $self->currentRegionmap->roomWidthPixels) / 4
                    )
                )
            ),
            $blockCentreYPosPixels + (
                $$vectorRef[1] * (
                    int($self->currentRegionmap->blockHeightPixels / 2) - int(
                        ($self->currentRegionmap->blockHeightPixels
                            - $self->currentRegionmap->roomHeightPixels) / 4
                    )
                )
            ),
        );
    }

    sub getExitStart {

        # Called by various functions
        # Find the coordinates on the pixmap of the pixel at the edge of the room (i.e. one pixel
        #   outside its border), from which a cardinal exit starts
        #
        # Expected arguments
        #   $xPosBlocks, $yPosBlocks
        #           - Grid coordinates of a gridblock
        #   $vectorRef
        #           - A value in the hash $self->constVectorHash, matching the direction of the
        #               exit's primary direction. The value is a reference to a list, e.g.
        #               northeast > [1, -1, 0]
        #
        # Optional arguments
        #   $blockCentreXPosPixels, $blockCentreYPosPixels
        #           - The coordinates of the pixel at the centre of the gridblock (and of the room,
        #               if one is drawn there)
        #   $borderCornerXPosPixels, $borderCornerYPosPixels
        #           - Coordinates of the pixel at the top-left corner of the room's border
        #   $flag   - If set to TRUE, the gridblock coordinates are checked for validity. If set to
        #               FALSE (or 'undef'), we assume that they're valid
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise, the pixel's coordinates in a list in the form (x, y)

        my (
            $self, $xPosBlocks, $yPosBlocks, $vectorRef, $blockCentreXPosPixels,
            $blockCentreYPosPixels, $borderCornerXPosPixels, $borderCornerYPosPixels, $flag,
            $check
        ) = @_;

        # Local variables
        my (
            $exitStartXPosPixels, $exitStartYPosPixels,
            @emptyList,
        );

        # Check for improper arguments
        if (
            ! defined $xPosBlocks || ! defined $yPosBlocks || ! defined $vectorRef || defined $check
        ) {
            $axmud::CLIENT->writeImproper($self->_objClass . '->getExitStart', @_);
            return @emptyList;
        }

        # Check the arguments, if necessary
        if ($flag) {

            if (
                ! $self->currentRegionmap->checkGridBlock(
                    $xPosBlocks,
                    $yPosBlocks,
                    $self->currentRegionmap->currentLevel,
                )
            ) {
                return @emptyList;
            }
        }

        # Get the coordinates of the room border's top-left corner and/or the block's centre if we
        #   don't have them
        if (! defined $blockCentreXPosPixels) {

            ($blockCentreXPosPixels, $blockCentreYPosPixels)
                = $self->getBlockCentre($xPosBlocks, $yPosBlocks);
        }

        if (! defined $borderCornerXPosPixels) {

            ($borderCornerXPosPixels, $borderCornerYPosPixels)
                = $self->getBorderCorner($xPosBlocks, $yPosBlocks);
        }

        # Get the coordinates
        $exitStartXPosPixels = $blockCentreXPosPixels + (
            $$vectorRef[0] * (
                int($self->currentRegionmap->roomWidthPixels / 2) + 1
            )
        );

        $exitStartYPosPixels = $blockCentreYPosPixels + (
            $$vectorRef[1] * (
                int($self->currentRegionmap->roomHeightPixels / 2) + 1
            )
        );

        # This algorithm can produce a pixel located on the border itself, not just outside it.
        #   Adjust the coordinates of the pixel as necessary
        if ($exitStartXPosPixels == $borderCornerXPosPixels) {

            $exitStartXPosPixels--;

        } elsif (
            $exitStartXPosPixels
                == ($borderCornerXPosPixels + $self->currentRegionmap->roomWidthPixels - 1)
        ) {
            $exitStartXPosPixels++;
        }

        if ($exitStartYPosPixels == $borderCornerYPosPixels) {

            $exitStartYPosPixels--;

        } elsif (
            $exitStartYPosPixels
                == ($borderCornerYPosPixels + $self->currentRegionmap->roomHeightPixels - 1)
        ) {
            $exitStartYPosPixels++;
        }

        # Return the coordinates
        return ($exitStartXPosPixels, $exitStartYPosPixels);
    }

    sub getBorderColour {

        # Returns the colour in which a room's border should be drawn (which depends on several
        #   factors)
        #
        # Expected arguments
        #   $roomObj    - The GA::ModelObj::Room being drawn
        #
        # Return values
        #   On improper arguments, returns the list
        #       (default_border_colour, 'single')
        #   Otherwise, returns the list
        #       (border_colour, draw_room_mode)
        #   ...where 'draw_room_mode' is 'single', 'double' or 'interior' for the current/last
        #       known/ghost rooms, and 'single' for all other rooms

        my ($self, $roomObj, $check) = @_;

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->getBorderColour', @_);

            # Return the default colour
            return ($self->worldModelObj->borderColour, 'single');
        }

        # Decide which colour to use
        if ($self->pairedTwinRoom && $roomObj eq $self->pairedTwinRoom) {

            if ($self->mapObj->currentRoom && $roomObj eq $self->mapObj->currentRoom) {

                # Twin paired current room
                return (
                    $self->worldModelObj->selectExitTwinColour,
                    $self->worldModelObj->currentRoomMode,
                );

            } else {

                # Twin paired (not current) room
                return ($self->worldModelObj->selectExitTwinColour, 'single');
            }

        } elsif (
            ! $self->mapObj->currentRoom
            && $self->mapObj->lastKnownRoom
            && $roomObj eq $self->mapObj->lastKnownRoom
        ) {
            # Automapper is lost
            if (
                ($self->selectedRoom && $roomObj eq $self->selectedRoom)
                || ($self->ivExists('selectedRoomHash', $roomObj->number))
            ) {
                # Last known room is selected
                return (
                    $self->worldModelObj->lostSelectBorderColour,
                    $self->worldModelObj->currentRoomMode,
                );

            } else {

                # Last known room is not selected
                return (
                    $self->worldModelObj->lostBorderColour,
                    $self->worldModelObj->currentRoomMode,
                );
            }

        } elsif (
            $self->mapObj->ghostRoom
            && $roomObj eq $self->mapObj->ghostRoom
            && (! $self->mapObj->currentRoom || $roomObj ne $self->mapObj->currentRoom)
        ) {
            # Ghost room (presumed actual location of character right now)
            if (
                ($self->selectedRoom && $roomObj eq $self->selectedRoom)
                || ($self->ivExists('selectedRoomHash', $roomObj->number))
            ) {
                # Ghost room is selected
                return (
                    $self->worldModelObj->ghostSelectBorderColour,
                    $self->worldModelObj->currentRoomMode,
                );

            } else {

                # Ghost room is not selected
                return (
                    $self->worldModelObj->ghostBorderColour,
                    $self->worldModelObj->currentRoomMode,
                );
            }

        } elsif (
            ($self->selectedRoom && $roomObj eq $self->selectedRoom)
            || ($self->ivExists('selectedRoomHash', $roomObj->number))
        ) {

            if ($self->mapObj->currentRoom && $roomObj eq $self->mapObj->currentRoom) {

                # Current and selected room
                return (
                    $self->worldModelObj->currentSelectBorderColour,
                    $self->worldModelObj->currentRoomMode,
                );

            } else {

                # Selected (but not current) room
                return ($self->worldModelObj->selectBorderColour, 'single');
            }

        } elsif ($self->mapObj->currentRoom && $roomObj eq $self->mapObj->currentRoom) {

            if ($self->mode eq 'wait') {

                # Current room in 'wait' mode
                return (
                    $self->worldModelObj->currentWaitBorderColour,
                    $self->worldModelObj->currentRoomMode,
                );

            } elsif ($self->mode eq 'follow') {

                # Current room in 'follow' mode
                return (
                    $self->worldModelObj->currentFollowBorderColour,
                    $self->worldModelObj->currentRoomMode,
                );

            } else {

                # Current room in 'update' mode
                return (
                    $self->worldModelObj->currentBorderColour,
                    $self->worldModelObj->currentRoomMode,
                );
            }

        } else {

            # Default border colour
            return ($self->worldModelObj->borderColour, 'single');
        }
    }

    sub getRoomColour {

        # Returns the colour in which a room's interior should be drawn (which depends on several
        #   factors)
        #
        # Expected arguments
        #   $roomObj    - The GA::ModelObj::Room being drawn
        #
        # Return values
        #   The default interior colour on improper arguments
        #   Otherwise, returns the colour with which to draw the room interior

        my ($self, $roomObj, $check) = @_;

        # Local variables
        my (
            $highestFlag, $highestScore,
            @flagList,
        );

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->getRoomColour', @_);
            # Return the default colour
            return $self->worldModelObj->roomColour;
        }

        # Work out which flag has the highest priority, while still being available through the
        #   filters specified in GA::Obj::WorldModel->roomFlagFilterHash
        $highestScore = 0;
        if ($roomObj->roomFlagHash) {

            @flagList = $roomObj->ivKeys('roomFlagHash');

            foreach my $flag (@flagList) {

                my ($filter, $score);

                # Check that the flag is available through the current filter (or that all filters
                #   have been applied using the flag)
                $filter = $self->worldModelObj->ivShow('roomFlagFilterHash', $flag);
                if (
                    $self->worldModelObj->allRoomFiltersFlag
                    || $self->worldModelObj->ivShow('roomFilterHash', $filter)
                ) {
                    $score = $self->worldModelObj->ivShow('roomFlagPriorityHash', $flag);
                    if ($highestScore == 0 || $score < $highestScore) {

                        # $flag's colour takes priority
                        $highestFlag = $flag;
                        $highestScore = $score;
                    }
                }
            }
        }

        if ($highestFlag) {

            # This flag's colour has the highest priority, so it's the one we draw
            $roomObj->ivPoke('lastRoomFlag', $highestFlag);

            return $self->worldModelObj->ivShow('roomFlagColourHash', $highestFlag);

        } else {

            # Room has no flags set - use the default colour
            return $self->worldModelObj->roomColour;
        }
    }

    sub getRoomTagColour {

        # Returns the colour in which a room tag should be drawn (which depends on several factors)
        #
        # Expected arguments
        #   $roomObj    - The GA::ModelObj::Room whose room tag is being drawn
        #
        # Return values
        #   The default room tag colour on improper arguments
        #   Otherwise, returns the colour with which to draw the room tag

        my ($self, $roomObj, $check) = @_;

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->getRoomTagColour', @_);
            # Return the default colour
            return $self->worldModelObj->roomTagColour;
        }

        # Decide which colour to use
        if (
            ($self->selectedRoomTag && $self->selectedRoomTag eq $roomObj)
            || $self->ivExists('selectedRoomTagHash', $roomObj->number)
        ) {
            # Selected room tag
            return $self->worldModelObj->selectRoomTagColour;

        } else {

            # Default room tag colour
            return $self->worldModelObj->roomTagColour;
        }
    }

    sub getRoomGuildColour {

        # Returns the colour in which a room guild should be drawn (which depends on several
        #   factors)
        #
        # Expected arguments
        #   $roomObj    - The GA::ModelObj::Room whose room guild is being drawn
        #
        # Return values
        #   The default room guild colour on improper arguments
        #   Otherwise, returns the colour with which to draw the room guild

        my ($self, $roomObj, $check) = @_;

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->getRoomGuildColour', @_);
            # Return the default colour
            return $self->worldModelObj->roomGuildColour;
        }

        # Decide which colour to use
        if (
            ($self->selectedRoomGuild && $self->selectedRoomGuild eq $roomObj)
            || $self->ivExists('selectedRoomGuildHash', $roomObj->number)
        ) {
            # Selected room guild
            return $self->worldModelObj->selectRoomGuildColour;

        } else {

            # Default room guild colour
            return $self->worldModelObj->roomGuildColour;
        }
    }

    sub getExitColour {

        # Returns the colour in which an exit should be drawn (which depends on several factors)
        #
        # Expected arguments
        #   $exitObj    - The GA::Obj::Exit being drawn
        #
        # Return values
        #   The default exit colour on improper arguments
        #   Otherwise, returns the colour with which to draw the exit

        my ($self, $exitObj, $check) = @_;

        # Local variables
        my $twinExitObj;

        # Check for improper arguments
        if (! defined $exitObj || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->getExitColour', @_);
            # Return the default colour
            return $self->worldModelObj->exitColour;
        }

        # Get the exit's twin, if there is one
        if ($exitObj->twinExit) {

            $twinExitObj = $self->worldModelObj->ivShow('exitModelHash', $exitObj->twinExit);
        }

        # Decide which colour to use
        if ($self->pairedTwinExit && $exitObj eq $self->pairedTwinExit) {

            # Twin paired room
            return $self->worldModelObj->selectExitTwinColour;

        } elsif (
            (
                $self->selectedExit
                && (
                    $exitObj eq $self->selectedExit
                    || (
                        $twinExitObj
                        && $twinExitObj eq $self->selectedExit
                        # (Bent exits have their ->brokenFlag set to TRUE - we only care about
                        #   non-bent broken exits here)
                        && ((! $twinExitObj->brokenFlag) || $twinExitObj->bentFlag)
                        && ! $twinExitObj->regionFlag
                    )
                )
            ) || (
                $self->ivExists('selectedExitHash', $exitObj->number)
            ) || (
                $twinExitObj && $self->ivExists(
                    'selectedExitHash',
                    $twinExitObj->number,
                )
            )
        ) {
            # Selected exit (or the exit's twin is selected, so draw it as if it were selected too)
            return $self->worldModelObj->selectExitColour;

        } elsif (
            $self->selectedExit && $exitObj ne $self->selectedExit
            && $self->selectedExit->shadowExit
            && $self->selectedExit->shadowExit == $exitObj->number
        ) {
            # $exitObj is a shadow exit of the selected exit. Draw it in a slightly different colour
            #   to a selected exit (default is dark blue)
            return $self->worldModelObj->selectExitShadowColour;

        } elsif ($exitObj->impassFlag) {

            return $self->worldModelObj->impassableExitColour;

        } else {

            # Default exit colour
            return $self->worldModelObj->exitColour;
        }
    }

    sub getExitTagColour {

        # Returns the colour in which an exit tag should be drawn (which depends on several factors)
        #
        # Expected arguments
        #   $exitObj    - The GA::Obj::Exit whose exit tag is being drawn
        #
        # Return values
        #   The default exit tag colour on improper arguments
        #   Otherwise, returns the colour with which to draw the exit tag

        my ($self, $exitObj, $check) = @_;

        # Local variables
        my $twinExitObj;

        # Check for improper arguments
        if (! defined $exitObj || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->getExitTagColour', @_);
            # Return the default colour
            return $self->worldModelObj->exitTagColour;
        }

        # Get the exit's twin, if there is one
        if ($exitObj->twinExit) {

            $twinExitObj = $self->worldModelObj->ivShow('exitModelHash', $exitObj->twinExit);
        }

        # Decide which colour to use
        if (
            ($self->selectedExitTag && $self->selectedExitTag eq $exitObj)
            || $self->ivExists('selectedExitTagHash', $exitObj->number)
        ) {
            # Selected exit tag
            return $self->worldModelObj->selectExitTagColour;

        } else {

            # Default exit tag colour
            return $self->worldModelObj->exitTagColour;
        }
    }

    sub getLabelColour {

        # Returns the colour in which a label should be drawn (which depends on several factors)
        #
        # Expected arguments
        #   $labelObj   - The GA::Obj::MapLabel being drawn
        #
        # Return values
        #   The default label colour on improper arguments
        #   Otherwise, returns the colour with which to draw the label

        my ($self, $labelObj, $check) = @_;

        # Check for improper arguments
        if (! defined $labelObj || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->getLabelColour', @_);
            # Return the default colour
            return $self->worldModelObj->mapLabelColour;
        }

        # Decide which colour to use
        if (
            ($self->selectedLabel && $self->selectedLabel eq $labelObj)
            || $self->ivExists('selectedLabelHash', $labelObj->number)
        ) {
            # Selected label
            return $self->worldModelObj->selectMapLabelColour;

        } else {

            # Default label colour
            return $self->worldModelObj->mapLabelColour;
        }
    }

    # Other lookup functions

    sub findRegion {

        # Called by $self->newRegionCallback or by any other function
        # Given a region's name, looks up its world model number
        #
        # Expected arguments
        #   $name   - The region's name (matches GA::ModelObj::Region->name)
        #
        # Return values
        #   'undef' on improper arguments or if no matching region is found
        #   Otherwise returns model number (matches GA::ModelObj::Region->number) of the matching
        #       region

        my ($self, $name, $check) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->findRegion', @_);
        }

        foreach my $regionObj ($self->worldModelObj->ivValues('regionModelHash')) {

            if ($regionObj->name eq $name) {

                # Matching region found
                return $regionObj->number;
            }
        }

        # Matching region not found
        return undef;
    }

    sub findRegionmap {

        # Called by $self->setCurrentRegion or by any other function
        # Given a region's name, looks up the corresponding GA::Obj::Regionmap
        #
        # Expected arguments
        #   $name   - The region's name (matches GA::Obj::Regionmap->name)
        #
        # Return values
        #   'undef' on improper arguments or if no matching regionmap is found
        #   Otherwise returns the matching GA::Obj::Regionmap

        my ($self, $name, $check) = @_;

        # Check for improper arguments
        if (! defined $name || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->findRegionmap', @_);
        }

        foreach my $regionmapObj ($self->worldModelObj->ivValues('regionmapHash')) {

            if ($regionmapObj->name eq $name) {

                # Matching region found
                return $regionmapObj;
            }
        }

        # Matching region not found
        return undef;
    }

    sub findGridBlock {

        # Called by $self->mouseClickEvent or any other function
        # Given (x, y) coordinates on the map (in pixels), finds the (x, y) coordinates of the
        #   corresponding gridblock (in blocks)
        #
        # Expected arguments
        #   $xPosPixels, $yPosPixels    - The coordinates of a pixel
        #
        # Return values
        #   An empty list on improper argument or if no corresponding block exists (because the
        #       pixel coordinates are invalid)
        #   Otherwise, a list containing coordinates of the gridblock, in the form (x, y)

        my ($self, $xPosPixels, $yPosPixels, $check) = @_;

        # Local variables
        my (
            $xPosBlocks, $yPosBlocks,
            @emptyList,
        );

        # Check for improper arguments
        if (! defined $xPosPixels || ! defined $yPosPixels || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->findGridBlock', @_);
            return @emptyList;
        }

        # Check that ($xPosPixels, $yPosPixels) is a set of coordinates that's actually on the
        #   pixmap
        if (
            $xPosPixels < 0
            || $yPosPixels < 0
            || $xPosPixels >= (
                    $self->currentRegionmap->gridWidthBlocks
                        * $self->currentRegionmap->blockWidthPixels
                )
            || $yPosPixels >= (
                    $self->currentRegionmap->gridHeightBlocks
                        * $self->currentRegionmap->blockHeightPixels
                )
        ) {
            # Mouse click didn't occur in a gridblock
            return @emptyList;
        }

        # Find the block
        $xPosBlocks = int ($xPosPixels / $self->currentRegionmap->blockWidthPixels);
        $yPosBlocks = int ($yPosPixels / $self->currentRegionmap->blockHeightPixels);

        return ($xPosBlocks, $yPosBlocks);
    }

    sub findGridCentre {

        # Called by $self->addFirstRoom or any other function
        # Finds the grid coordinates of the centre of the grid
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   The list (0, 0, 0) on improper arguments
        #   Otherwise, returns a list containing the coordinates of the centre of the grid, in the
        #       form (x, y, z)

        my ($self, $check) = @_;

        # Local variables
        my ($xPosBlocks, $yPosBlocks, $zPosBlocks);

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->findGridCentre', @_);
            return (0, 0, 0);
        }

        $xPosBlocks = int (($self->currentRegionmap->gridWidthBlocks) / 2);
        $yPosBlocks = int (($self->currentRegionmap->gridHeightBlocks) / 2);
        $zPosBlocks = 0;    # 'Ground' level

        return ($xPosBlocks, $yPosBlocks, $zPosBlocks);
    }

    sub findClickedExit {

        # Called by $self->mouseClickEvent or any other function
        # Given a block in the grid (which contains a GA::ModelObj::Room) and the coordinates of a
        #   pixel somewhere in the same block, see if any of the exits were drawn underneath (or
        #   almost underneath) the clicked pixel
        # $self->exitSensitivity sets a minimum standard for how close the mouse click must be to
        #   the drawn exit. Of all the exits that meet this minimum standard, we return the
        #   best-fitting one
        #
        # Expected arguments
        #   $clickXPosPixels, $clickYPosPixels
        #               - The coordinates of the clicked pixel
        #   $roomObj    - The GA::ModelObj::Room in this gridblock
        #
        # Return values
        #   'undef' on improper arguments, or if the click isn't on (or very near) an exit
        #   Otherwise, returns the blessed reference of the closest GA::Obj::Exit

        my ($self, $clickXPosPixels, $clickYPosPixels, $roomObj, $check) = @_;

        # Local variables
        my (
            $blockCornerXPosPixels, $blockCornerYPosPixels, $blockCentreXPosPixels,
            $blockCentreYPosPixels, $borderCornerXPosPixels, $borderCornerYPosPixels,
            $startXPosPixels, $startYPosPixels, $stopXPosPixels, $stopYPosPixels, $angleBAC,
            $anglePAD, $gapAngle, $bestExitObj, $bestExitGapAngle,
        );

        # Check for improper arguments
        if (
            ! defined $clickXPosPixels || ! defined $clickYPosPixels || ! defined $roomObj
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->findClickedExit', @_);
        }

        # Get some coordinates that we'll need to pass to the functions that tell us where the exits
        #   are drawn

        # Find the coordinates on the pixmap of the pixel occupying the top-left corner of the
        #   gridblock specified by $roomObj
        ($blockCornerXPosPixels, $blockCornerYPosPixels) = $self->getBlockCorner(
            $roomObj->xPosBlocks,
            $roomObj->yPosBlocks,
        );

        # Find the coordinates of the pixel occupying the centre of the block (and of the room)
        ($blockCentreXPosPixels, $blockCentreYPosPixels) = $self->getBlockCentre(
            $roomObj->xPosBlocks,
            $roomObj->yPosBlocks,
            $blockCornerXPosPixels,
            $blockCornerYPosPixels,
        );
        # Find the coordinates of the pixel at the top-left corner of the room's border
        ($borderCornerXPosPixels, $borderCornerYPosPixels) = $self->getBorderCorner(
            $roomObj->xPosBlocks,
            $roomObj->yPosBlocks,
            $blockCornerXPosPixels,
            $blockCornerYPosPixels,
        );

        # Now, for each exit in this room...
        OUTER: foreach my $number ($roomObj->ivValues('exitNumHash')) {

            my ($exitObj, $mapDir);

            $exitObj = $self->worldModelObj->ivShow('exitModelHash', $number);
            if (! $exitObj) {

                next OUTER;
            }

            # Get the direction in which the exit is drawn. If not set, move on to the next exit
            $mapDir = $exitObj->mapDir;
            if (! $mapDir) {

                next OUTER;
            }

            # We can't click 'up' and 'down', just the sixteen cardinal directions
            if ($mapDir eq 'up' || $mapDir eq 'down') {

                next OUTER;
            }

            # For unallocated exits allocated a temporary primary direction, for broken/region exits
            #   and for exits with the impassable ornament, we have a very definite square within
            #   which the user has to click - no need for any trigonometry
            if (
                $exitObj->drawMode eq 'temp_alloc'
                || $exitObj->brokenFlag
                || $exitObj->regionFlag
                || $exitObj->impassFlag
            ) {
                # Get the coordinates of two corners of the square occupied by the exit
                # NB ($startXPosPixels, $startYPosPixels) is lower and further to the left than
                #   ($stopXPosPixels, $stopYPosPixels)
                ($startXPosPixels, $startYPosPixels, $stopXPosPixels, $stopYPosPixels)
                    = $self->findSquareExit(
                        $roomObj, $exitObj, $mapDir,
                        $blockCentreXPosPixels, $blockCentreYPosPixels,
                    );

                if (
                    $clickXPosPixels >= $startXPosPixels
                    && $clickXPosPixels <= $stopXPosPixels
                    && $clickYPosPixels <= $startYPosPixels
                    && $clickYPosPixels >= $stopYPosPixels
                ) {
                    # This is the selected exit
                    return $exitObj;

                } else {

                    next OUTER;
                }
            }

            # Get the coordinates of the pixel at each end of the line that forms the drawn exit
            # NB This line repeats below. Change one, change both!
            if (
                $exitObj->destRoom
                && ($exitObj->twinExit || $exitObj->oneWayFlag)
            ) {
                # It's a two-way exit or one-way exit (more than one gridblock)
                ($startXPosPixels, $startYPosPixels, $stopXPosPixels, $stopYPosPixels)
                    = $self->findLongExit(
                        $roomObj, $exitObj, $mapDir,
                        $blockCentreXPosPixels,
                        $blockCentreYPosPixels,
                        $borderCornerXPosPixels,
                        $borderCornerYPosPixels,
                    );

            } else {

                # It's an incomplete or uncertain exit (confined to the same gridblock as its room)
                # It's a two-way exit or one-way exit (more than one gridblock)
                ($startXPosPixels, $startYPosPixels, $stopXPosPixels, $stopYPosPixels)
                    = $self->findShortExit(
                        $roomObj, $exitObj, $mapDir,
                        $blockCentreXPosPixels,
                        $blockCentreYPosPixels,
                    );
            }

            # The mouse click must be vaguely near the exit, to count...
            if (
                ! $self->checkCredibleExit(
                    $clickXPosPixels, $clickYPosPixels,
                    $startXPosPixels, $startYPosPixels,
                    $stopXPosPixels, $stopYPosPixels,
                )
            ) {
                # Mouse click is too far away
                next OUTER;
            }

            #          x B                  x P
            #         xx                   xx
            #        x x                  x x
            #       P  x                 x  x
            #      x   x                x   x
            #     x @  x               x N  x
            #  A xxxxDxx C          A xxxxxxx D
            #
            # ABC is a right-angled triangle.
            # A is the start of the exit (coordinates $startXPosPixels, $startYPosPixels), B is the
            #   end of the exit (coordinates $stopXPosPixels, $stopYPosPixels).
            # The clicked pixel P (coordinates $clickXPosPixels, $clickYPosPixels) is somewhere
            #   along the line AB - or close to it. It is nearer to A than it is to B
            #
            # Now check the gradient of the line AP. If it is close enough to the gradient of AB, we
            #   can consider that this exit was the one that was clicked
            # (Actually, we check the angle @ in the triangle ABC, and the angle N in the triangle
            #   APD. The two triangles are not drawn to scale. If the angles are close enough - that
            #   is to say, within $self->exitSensitivity radians of each other, it's the right exit
            $angleBAC = abs(
                $self->findExitAngles(
                    $startXPosPixels,
                    $startYPosPixels,
                    $stopXPosPixels,
                    $stopYPosPixels,
                )
            );

            $anglePAD = abs(
                $self->findExitAngles(
                    $startXPosPixels,
                    $startYPosPixels,
                    $clickXPosPixels,
                    $clickYPosPixels,
                )
            );

            # Find the difference between the two angles
            $gapAngle = abs($anglePAD - $angleBAC);
            if ($gapAngle < $self->exitSensitivity) {

                # The exit is acceptably close to the mouse click. If it is closer than any
                #   previously found acceptable exit, use it
                if (! defined $bestExitObj || $bestExitGapAngle > $gapAngle) {

                    $bestExitObj = $exitObj;
                    $bestExitGapAngle = $gapAngle;
                }
            }
        }

        # Return the best-fitting exit we found (if none were found acceptably close to the exit,
        #   return 'undef')
        return $bestExitObj;
    }

    sub findClosestPoint {

        # Called by $self->findClickedExit
        # Given the coordinates of two pixels on the map, work out which one of them is closer to
        #   the point of a mouse click, and return the coordinates of the closer point
        #
        # Expected arguments
        #   $clickXPosPixels, $clickYPosPixels
        #           - The coordinates of the clicked pixel
        #   $startXPosPixels, $startYPosPixels
        #           - The coordinates of the first point
        #   $stopXPosPixels, $stopYPosPixels
        #           - The coordinates of the second point
        #
        # Return values
        #   On improper arguments, returns the list ($startXPosPixels, $startYPosPixels)
        #   Otherwise, returns the coordinates of the closer pixel as a list in the form (x, y)

        my (
            $self, $clickXPosPixels, $clickYPosPixels, $startXPosPixels, $startYPosPixels,
            $stopXPosPixels, $stopYPosPixels, $check
        ) = @_;

        # Local variables
        my ($lengthAD, $lengthCD, $lengthAC, $lengthBD, $lengthBC);

        # Check for improper arguments
        if (
            ! defined $clickXPosPixels || ! defined $clickYPosPixels || ! defined $startXPosPixels
            || ! defined $startYPosPixels || ! defined $stopXPosPixels || ! defined $stopYPosPixels
            || defined $check
        ) {
            $axmud::CLIENT->writeImproper($self->_objClass . '->findClosestPoint', @_);
            return ($startXPosPixels, $startYPosPixels);
        }

        #       x C
        #      xx
        #     x x
        #    x  x
        # A xxxxx D
        #
        # A is ($startXPosPixels, $startYPosPixels), C is the position of the mouse click
        # Get the length of AC
        $lengthAD = abs($clickXPosPixels - $startXPosPixels);
        $lengthCD = abs($clickYPosPixels - $startYPosPixels);
        $lengthAC = sqrt (($lengthAD ** 2) + ($lengthCD ** 2));

        #       x C
        #      xx
        #     x x
        #    x  x
        # B xxxxx D
        #
        # B is ($stopXPosPixels, $stopYPosPixels), C is the position of the mouse click
        # Get the length of BC
        $lengthBD = abs($clickXPosPixels - $stopXPosPixels);
        $lengthCD = abs($clickYPosPixels - $stopYPosPixels);
        $lengthBC = sqrt (($lengthBD ** 2) + ($lengthCD ** 2));

        if ($lengthAC <= $lengthBC) {

            # Pixel A is closer (or both pixels are equidistant)
            return ($startXPosPixels, $startYPosPixels);

        } else {

            # Pixel B is closer
            return ($stopXPosPixels, $stopYPosPixels);
        }
    }

    sub findExitAngles {

        # Called by $self->findClickedExit to find the gradients of a line (as described in the
        #   comments for that function)
        # Actually, we take the line as the hypotenuse of a right-angled triangle, and return the
        #   angle (in degrees) between the hypotenuse and the adjacent
        #
        # Expected arguments
        #   $startXPosPixels, $startYPosPixels
        #       - Coordinates of the pixel at one end of the line
        #   $stopXPosPixels, $stopYPosPixels
        #       - Coordinates of the pixel at the other end of the line
        #
        # Return values
        #   'undef' on improper arguments
        #   Otherwise, the angle described above (in the range 0-90)

        my (
            $self, $startXPosPixels, $startYPosPixels, $stopXPosPixels, $stopYPosPixels, $check
        ) = @_;

        # Local variables
        my ($lengthAC, $lengthBC, $angleBAC);

        # Check for improper arguments
        if (
            ! defined $startXPosPixels || ! defined $startYPosPixels || ! defined $stopXPosPixels
            || ! defined $stopYPosPixels || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->findExitAngles', @_);
        }

        # Get the length of the adjacent
        $lengthAC = abs($stopXPosPixels - $startXPosPixels);
        # Get the length of the opposite
        $lengthBC = abs($stopYPosPixels - $startYPosPixels);

        # Get the angle between the hypotenuse and the adjacent, in degrees
        if (! $lengthBC) {
            $angleBAC = 0;
        } elsif (! $lengthAC) {
            $angleBAC = 90;
        } else {
            $angleBAC = Math::Trig::rad2deg(Math::Trig::atan($lengthBC / $lengthAC));
        }

        return $angleBAC;
    }

    sub findLongExit {

        # Called by $self->drawOneWayExit, ->drawTwoWayExit and ->findClickedExit
        # Returns the coordinates of the pixels at each end of a straight line between the points at
        #   which an exit meets a room (on a pixel occupied by the exit, not by the room's border)
        # This gives us the position of a one-way or two-way exit
        #
        # Expected arguments
        #   $roomObj        - Blessed reference of the GA::ModelObj::Room from which the exit
        #                       starts
        #   $exitObj        - Blessed reference of the GA::Obj::Exit
        #   $cardinalDir    - One of the sixteen cardinal directions (e.g. 'north') in which the
        #                       exit is drawn
        #
        # Optional arguments
        #   $blockCentreXPosPixels, $blockCentreYPosPixels
        #               - Coordinates of the pixel occupying the centre of the block (and of the
        #                   room) (if 'undef', calculated by this function)
        #   $borderCornerXPosPixels, $borderCornerYPosPixels
        #               - Coordinates of the pixel at the top-left corner of the room's border (if
        #                   'undef', calculated by this function)
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise, returns the coordinates of the two pixels as a list in the form
        #       (startX, startY, stopX, stopY)

        my (
            $self, $roomObj, $exitObj, $cardinalDir, $blockCentreXPosPixels,
            $blockCentreYPosPixels, $borderCornerXPosPixels, $borderCornerYPosPixels, $check
        ) = @_;

        # Local variables
        my (
            $blockCornerXPosPixels, $blockCornerYPosPixels, $destRoomObj, $vectorRef,
            $oppCardinalDir, $oppVectorRef, $exitStartXPosPixels, $exitStartYPosPixels,
            $newBlockCornerXPosPixels, $newBlockCornerYPosPixels, $newBlockCentreXPosPixels,
            $newBlockCentreYPosPixels, $newBorderCornerXPosPixels, $newBorderCornerYPosPixels,
            $exitStopXPosPixels, $exitStopYPosPixels,
            @emptyList,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || ! defined $cardinalDir || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->findLongExit', @_);
            return @emptyList;
        }

        # If the four optional arguments weren't supplied, get them now
        if (! defined $borderCornerXPosPixels || ! defined $blockCentreXPosPixels) {

            # Find the coordinates on the pixmap of the pixel occupying the top-left corner of the
            #   gridblock specified by $roomObj
            ($blockCornerXPosPixels, $blockCornerYPosPixels) = $self->getBlockCorner(
                $roomObj->xPosBlocks,
                $roomObj->yPosBlocks,
            );

            # Find the coordinates of the pixel occupying the centre of the block (and of the room)
            ($blockCentreXPosPixels, $blockCentreYPosPixels) = $self->getBlockCentre(
                $roomObj->xPosBlocks,
                $roomObj->yPosBlocks,
                $blockCornerXPosPixels,
                $blockCornerYPosPixels,
            );

            # Find the coordinates of the pixel at the top-left corner of the room's border
            ($borderCornerXPosPixels, $borderCornerYPosPixels) = $self->getBorderCorner(
                $roomObj->xPosBlocks,
                $roomObj->yPosBlocks,
                $blockCornerXPosPixels,
                $blockCornerYPosPixels,
            );
        }

        # Find the exit's destination room
        $destRoomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->destRoom);

        # Find the exit's vector - a reference to a list of 3d coordinates, (x, y, z)
        #   e.g. northeast > (1, 1, 0)
        $vectorRef = $self->ivShow('constVectorHash', $cardinalDir);
        # Get the opposite direction of $cardinalDir (e.g. 'north' > 'south')
        $oppCardinalDir = $axmud::CLIENT->ivShow('constOppDirHash', $cardinalDir);
        # Get the exit's vector from the centre of the arrival room (should be equal and opposite to
        #   $vectorRef)
        $oppVectorRef = $self->ivShow('constVectorHash', $oppCardinalDir);

        # Find the coordinates of the pixel at the edge of the room, just outside the border, from
        #   which a cardinal exit starts
        ($exitStartXPosPixels, $exitStartYPosPixels) = $self->getExitStart(
            $roomObj->xPosBlocks,
            $roomObj->yPosBlocks,
            $vectorRef,
            $blockCentreXPosPixels,
            $blockCentreYPosPixels,
            $borderCornerXPosPixels,
            $borderCornerYPosPixels,
        );

        # Find the coordinates on the pixmap of the pixel occupying the top-left corner of the
        #   gridblock occupied by the arrival room
        ($newBlockCornerXPosPixels, $newBlockCornerYPosPixels) = $self->getBlockCorner(
            $destRoomObj->xPosBlocks,
            $destRoomObj->yPosBlocks,
        );

        # Find the coordinates of the pixel occupying the centre of this block (and of its room)
        ($newBlockCentreXPosPixels, $newBlockCentreYPosPixels) = $self->getBlockCentre(
            $destRoomObj->xPosBlocks,
            $destRoomObj->yPosBlocks,
            $newBlockCornerXPosPixels,
            $newBlockCornerYPosPixels,
        );

        # Find the coordinates of the pixel occupying the top-left corner of the room's border
        ($newBorderCornerXPosPixels, $newBorderCornerYPosPixels) = $self->getBorderCorner(
            $destRoomObj->xPosBlocks,
            $destRoomObj->yPosBlocks,
        );

        # Find the coordinates of the pixel at the edge of the arrival room, just outside the
        #   border, at which the cardinal exit stops
        ($exitStopXPosPixels, $exitStopYPosPixels) = $self->getExitStart(
            $destRoomObj->xPosBlocks,
            $destRoomObj->yPosBlocks,
            $oppVectorRef,
            $newBlockCentreXPosPixels,
            $newBlockCentreYPosPixels,
            $newBorderCornerXPosPixels,
            $newBorderCornerYPosPixels,
        );

        # Return the coordinates of each end of the exit
        return (
            $exitStartXPosPixels,
            $exitStartYPosPixels,
            $exitStopXPosPixels,
            $exitStopYPosPixels,
        );
    }

    sub findShortExit {

        # Called by $self->findClickedExit (only)
        # Returns the coordinates of the pixels at each end of a straight line between the start of
        #   an exit, and the edge of a gridblock where it intersects
        # This gives us the (approximate) position of an incomplete or uncertain exit
        #
        # Expected arguments
        #   $roomObj        - Blessed reference of the GA::ModelObj::Room from which the exit
        #                       starts
        #   $exitObj        - Blessed reference of the GA::Obj::Exit
        #   $cardinalDir    - One of the sixteen cardinal directions (e.g. 'north') in which the
        #                       exit is drawn
        #
        # Optional arguments
        #   $blockCentreXPosPixels, $blockCentreYPosPixels
        #       - Coordinates of the pixel occupying the centre of the block (and of the room) (if
        #           'undef', calculated by this function)
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise, returns the coordinates of the two pixels as a list in the form
        #       (startX, startY, stopX, stopY)

        my (
            $self, $roomObj, $exitObj, $cardinalDir, $blockCentreXPosPixels, $blockCentreYPosPixels,
            $check
        ) = @_;

        # Local variables
        my (
            $blockCornerXPosPixels, $blockCornerYPosPixels, $exitStartXPosPixels,
            $exitStartYPosPixels, $blockEdgeXPosPixels, $blockEdgeYPosPixels, $vectorRef,
            $borderCornerXPosPixels, $borderCornerYPosPixels,
            @emptyList,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || ! defined $cardinalDir || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->findShortExit', @_);
            return @emptyList;
        }

        # If the two optional arguments weren't supplied, get them now
        if (! defined $blockCentreXPosPixels) {

            # Find the coordinates on the pixmap of the pixel occupying the top-left corner of the
            #   gridblock specified by $roomObj
            ($blockCornerXPosPixels, $blockCornerYPosPixels) = $self->getBlockCorner(
                $roomObj->xPosBlocks,
                $roomObj->yPosBlocks,
            );

            # Find the coordinates of the pixel occupying the centre of the block (and of the room)
            ($blockCentreXPosPixels, $blockCentreYPosPixels) = $self->getBlockCentre(
                $roomObj->xPosBlocks,
                $roomObj->yPosBlocks,
                $blockCornerXPosPixels,
                $blockCornerYPosPixels,
            );
        }

        # Find the exit's vector - a reference to a list of 3d coordinates, (x, y, z)
        #   e.g. northeast > (1, 1, 0)
        $vectorRef = $self->ivShow('constVectorHash', $cardinalDir);

        # Find the coordinates of the pixel occupying the top-left corner of the room's border
        ($borderCornerXPosPixels, $borderCornerYPosPixels) = $self->getBorderCorner(
            $roomObj->xPosBlocks,
            $roomObj->yPosBlocks,
        );

        # Find the coordinates of the pixel at the edge of the room, just outside the border, from
        #   which a cardinal exit starts
        ($exitStartXPosPixels, $exitStartYPosPixels) = $self->getExitStart(
            $roomObj->xPosBlocks,
            $roomObj->yPosBlocks,
            $vectorRef,
            $blockCentreXPosPixels,
            $blockCentreYPosPixels,
            $borderCornerXPosPixels,
            $borderCornerYPosPixels,
        );

        # Find the coordinates of the pixel at the edge of the gridblock, which is intersected by a
        #   cardinal exit
        ($blockEdgeXPosPixels, $blockEdgeYPosPixels) = $self->getBlockEdge(
            $roomObj->xPosBlocks,
            $roomObj->yPosBlocks,
            $vectorRef,
            $blockCentreXPosPixels,
            $blockCentreYPosPixels,
        );

        # Return the coordinates of each end of the exit
        return (
            $exitStartXPosPixels,
            $exitStartYPosPixels,
            $blockEdgeXPosPixels,
            $blockEdgeYPosPixels,
        );
    }

    sub findSquareExit {

        # Called by $self->drawUnallocatedExit (etc) and $self->findClickedExit
        # Returns the coordinates of the pixels at opposite corners of a square in which an
        #   unallocated (or similar) exit is drawn
        #
        # Expected arguments
        #   $roomObj        - Blessed reference of the GA::ModelObj::Room from which the exit
        #                       starts
        #   $exitObj        - Blessed reference of the GA::Obj::Exit
        #   $cardinalDir    - One of the sixteen cardinal directions (e.g. 'north') in which the
        #                       exit is drawn
        #
        # Optional arguments
        #   $blockCentreXPosPixels, $blockCentreYPosPixels
        #               - Coordinates of the pixel occupying the centre of the block (and of the
        #                   room) (if 'undef', calculated by this function)
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise, returns the coordinates of the two pixels at opposite corners as a list in
        #       the form (startX, startY, stopX, stopY).
        #   NB (startX, startY) always tends to be lower and further to the left on the map than
        #       (stopX, stopY)

        my (
            $self, $roomObj, $exitObj, $cardinalDir, $blockCentreXPosPixels, $blockCentreYPosPixels,
            $check
        ) = @_;

        # Local variables
        my (
            $vectorRef, $exitStartXPosPixels, $exitStartYPosPixels, $blockEdgeXPosPixels,
            $blockEdgeYPosPixels, $halfHorizontalLength, $halfVerticalLength,
            $squareStartXPosPixels, $squareStartYPosPixels, $squareStopXPosPixels,
            $squareStopYPosPixels, $borderCornerXPosPixels, $borderCornerYPosPixels,
            @emptyList,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || ! defined $cardinalDir || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->findSquareExit', @_);
            return @emptyList;
        }

        # Find the exit's vector - a reference to a list of 3d coordinates, (x, y, z)
        #   e.g. northeast > (1, 1, 0)
        $vectorRef = $self->ivShow('constVectorHash', $cardinalDir);

        # Find the coordinates of the pixel occupying the top-left corner of the room's border
        ($borderCornerXPosPixels, $borderCornerYPosPixels) = $self->getBorderCorner(
            $roomObj->xPosBlocks,
            $roomObj->yPosBlocks,
        );

        # Find the coordinates of the pixel at the edge of the room, just outside the border, from
        #   which a cardinal exit starts
        ($exitStartXPosPixels, $exitStartYPosPixels) = $self->getExitStart(
            $roomObj->xPosBlocks,
            $roomObj->yPosBlocks,
            $vectorRef,
            $blockCentreXPosPixels,
            $blockCentreYPosPixels,
            $borderCornerXPosPixels,
            $borderCornerYPosPixels,
        );

        # Find the coordinates of the pixel near (but not at) the edge of the gridblock, which is
        #   intersected by a cardinal exit
        ($blockEdgeXPosPixels, $blockEdgeYPosPixels) = $self->getNearBlockEdge(
            $roomObj->xPosBlocks,
            $roomObj->yPosBlocks,
            $vectorRef,
            $blockCentreXPosPixels,
            $blockCentreYPosPixels,
        );

        # Adjust the coordinates of the two pixels at either end of the exit, so that it's easier to
        #   draw '+' exits (and so that '+' are the same size as 'x')

        # The line (or the square for which the ends of the line are in opposite corners) must have
        #   an odd-numbered length, so that the '+' and 'x' are symmetrical
        if (
            $exitStartXPosPixels != $blockEdgeXPosPixels
            && int(($blockEdgeXPosPixels - $exitStartXPosPixels) / 2)
                != (($blockEdgeXPosPixels - $exitStartXPosPixels) / 2)
        ) {
            # Adjust the end of the line, to make it slightly bigger
            $blockEdgeXPosPixels += $$vectorRef[0];
        }

        if (
            $exitStartYPosPixels != $blockEdgeYPosPixels
            && int(($blockEdgeYPosPixels - $exitStartYPosPixels) / 2)
                != (($blockEdgeYPosPixels - $exitStartYPosPixels) / 2)
        ) {
            # Adjust the end of the line, to make it slightly bigger
            $blockEdgeYPosPixels += $$vectorRef[1];
        }

        # Adjust the coordinates so that the exit's start is below and to the left of the block's
        #   edge, by reversing coordinates where necessary
        if ($exitStartXPosPixels > $blockEdgeXPosPixels) {

            ($exitStartXPosPixels, $blockEdgeXPosPixels)
                = ($blockEdgeXPosPixels, $exitStartXPosPixels);
        }
        if ($exitStartYPosPixels < $blockEdgeYPosPixels) {

            ($exitStartYPosPixels, $blockEdgeYPosPixels)
                = ($blockEdgeYPosPixels, $exitStartYPosPixels);
        }

        # North/east/south/west
        if (
            $exitStartXPosPixels eq $blockEdgeXPosPixels
            || $exitStartYPosPixels eq $blockEdgeYPosPixels
        ) {

            # North/south
            if ($exitStartXPosPixels eq $blockEdgeXPosPixels) {

                # Get half the length of the exit
                $halfHorizontalLength = abs(int(($blockEdgeYPosPixels - $exitStartYPosPixels) / 2));
                # Find the coordinates of the one corner of the square occupied by the 'x'
                $squareStartXPosPixels = $exitStartXPosPixels - $halfHorizontalLength;
                $squareStartYPosPixels = $exitStartYPosPixels;
                # Find the coordinates of the opposite corner of the square
                $squareStopXPosPixels = $blockEdgeXPosPixels + $halfHorizontalLength;
                $squareStopYPosPixels = $blockEdgeYPosPixels;

            # East/west
            } else {

                # Get half the length of the exit
                $halfVerticalLength = abs(int(($blockEdgeXPosPixels - $exitStartXPosPixels) / 2));
                # Find the coordinates of the one corner of the square occupied by the 'x'
                $squareStartXPosPixels = $exitStartXPosPixels;
                $squareStartYPosPixels = $exitStartYPosPixels + $halfVerticalLength;
                # Find the coordinates of the opposite corner of the square
                $squareStopXPosPixels = $blockEdgeXPosPixels;
                $squareStopYPosPixels = $blockEdgeYPosPixels - $halfVerticalLength;
            }

            return (
                $squareStartXPosPixels, $squareStartYPosPixels,
                $squareStopXPosPixels, $squareStopYPosPixels
            );

        } else {

            # We already have the coordinates of opposite ends of a square
            return (
                $exitStartXPosPixels, $exitStartYPosPixels,
                $blockEdgeXPosPixels, $blockEdgeYPosPixels
            );
        }
    }

    sub findPerpendicular {

        # Called by $self->drawPickableOrnament, etc
        # Finds the coordinates of the pixels at each end of a line, drawn perpendicular
        #   to an exit line (whose vector we know)
        # The perpendicular line runs parallel with another perpendicular line, close to it
        #
        # Expected arguments
        #   $vectorRef      - The vector of the exit line - a list reference, from a value in
        #                       $self->constVectorHash
        #   $perpVectorRef  - The vector of the perpendicular - a list reference, from a value in
        #                       $self->constPerpVectorHash
        #   $factor         - Set to 1 or -1, which helps us to draw two perpendicular lines,
        #                       parallel to each other and slightly separated
        #   $exitMiddleXPosPixels, $exitMiddleYPosPixels
        #                   - The coordinates of the pixel in the middle of the exit line
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise, returns the coordinates of the two pixels as a list in the form
        #       (startX, startY, stopX, stopY)

        my (
            $self, $vectorRef, $perpVectorRef, $factor, $exitMiddleXPosPixels,
            $exitMiddleYPosPixels, $check
        ) = @_;

        # Local variables
        my (
            $startXPosPixels, $startYPosPixels, $stopXPosPixels, $stopYPosPixels,
            @emptyList,
        );

        # Check for improper arguments
        if (
            ! defined $vectorRef || ! defined $perpVectorRef || ! defined $factor
            || ! defined $exitMiddleXPosPixels || ! defined $exitMiddleXPosPixels || defined $check
        ) {
            $axmud::CLIENT->writeImproper($self->_objClass . '->findPerpendicular', @_);
            return @emptyList;
        }

        # Find the ends of the perpendicular line
        $startXPosPixels =
            $exitMiddleXPosPixels + ($$vectorRef[0] * $factor) + (
                $$perpVectorRef[0] * int(
                    ($self->currentRegionmap->blockWidthPixels
                        - $self->currentRegionmap->roomWidthPixels
                    ) / 3
                )
            );

        $startYPosPixels =
            $exitMiddleYPosPixels + ($$vectorRef[1] * $factor) + (
                $$perpVectorRef[1] * int(
                    ($self->currentRegionmap->blockHeightPixels
                        - $self->currentRegionmap->roomHeightPixels
                    ) / 3
                )
            );

        $stopXPosPixels =
            $exitMiddleXPosPixels + ($$vectorRef[0] * $factor) + (
                $$perpVectorRef[2] * int(
                    ($self->currentRegionmap->blockWidthPixels
                        - $self->currentRegionmap->roomWidthPixels
                    ) / 3
                )
            );

        $stopYPosPixels =
            $exitMiddleYPosPixels + ($$vectorRef[1] * $factor) + (
                $$perpVectorRef[3] * int(
                    ($self->currentRegionmap->blockHeightPixels
                        - $self->currentRegionmap->roomHeightPixels
                    ) / 3
                )
            );

        return ($startXPosPixels, $startYPosPixels, $stopXPosPixels, $stopYPosPixels);
    }

    sub findExitMiddle {

        # Can be called by anything
        # Finds the coordinates of the pixel at the middle of an exit line, given the coordinates of
        #   each end of the line
        #
        # Expected arguments
        #   $exitStartXPosPixels, $exitStartYPosPixels
        #       - The coordinates of one end of the line
        #   $exitStopXPosPixels, $exitStopYPosPixels
        #       - The coordinates of the other end of the line
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise, returns the coordinates of the pixels as a list in the form
        #       (middleX, middleY)

        my (
            $self, $exitStartXPosPixels, $exitStartYPosPixels, $exitStopXPosPixels,
            $exitStopYPosPixels, $check
        ) = @_;

        # Local variables
        my (
            $exitMiddleXPosPixels, $exitMiddleYPosPixels,
            @emptyList,
        );

        # Check for improper arguments
        if (
            ! defined $exitStartXPosPixels || ! defined $exitStartYPosPixels
            || ! defined $exitStopXPosPixels || ! defined $exitStopYPosPixels || defined $check
        ) {
            $axmud::CLIENT->writeImproper($self->_objClass . '->findExitMiddle', @_);
            return @emptyList;
        }

        # Find the coordinates of the middle of the exit
        if ($exitStartXPosPixels != $exitStopXPosPixels) {

            $exitMiddleXPosPixels = $exitStartXPosPixels + int(
                ($exitStopXPosPixels - $exitStartXPosPixels) / 2
            );

        } else {

            $exitMiddleXPosPixels = $exitStartXPosPixels;
        }

        if ($exitStartYPosPixels != $exitStopYPosPixels) {

            $exitMiddleYPosPixels = $exitStartYPosPixels + int(
                ($exitStopYPosPixels - $exitStartYPosPixels) / 2
            );

        } else {

            $exitMiddleYPosPixels = $exitStartYPosPixels;
        }

        return ($exitMiddleXPosPixels, $exitMiddleYPosPixels);
    }

    sub findMouseOverRoom {

        # Called by $self->stopDrag (only)
        # When a draggable exit has been dragged somewhere (and the mouse button has been released),
        #   work out whether the mouse was released over a room that wasn't the draggable exit's
        #   parent room (if so, we can connect the exit to room) or its existing destination room
        #
        # Expected arguments
        #   $clickXPosPixels, $clickYPosPixels
        #               - The coordinates of the mouse over the map when the mouse button was
        #                   released
        #   $exitObj    - The GA::Obj::Exit that was being dragged
        #
        # Return values
        #   'undef' on improper arguments, if the drag operation didn't stop over a room, or if the
        #       room is the exit's parent room
        # Otherwise, returns the blessed reference of the room to which the exit can be connected

        my ($self, $clickXPosPixels, $clickYPosPixels, $exitObj, $check) = @_;

        # Local variables
        my (
            $parentRoomObj, $existDestRoomObj, $xBlocks, $yBlocks, $destRoomNum, $destRoomObj,
            $mode, $startX, $startY, $stopX, $stopY,
        );

        # Check for improper arguments
        if (
            ! defined $clickXPosPixels || ! defined $clickYPosPixels || ! defined $exitObj
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->findMouseOverRoom', @_);
        }

        # Get the exit's parent room and existing destination room
        $parentRoomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->parent);
        if ($exitObj->destRoom) {

            $existDestRoomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->destRoom);
        }

        # Get the gridblock over which the mouse is positioned
        ($xBlocks, $yBlocks) = $self->findGridBlock($clickXPosPixels, $clickYPosPixels);
        # Fetch the room at that gridblock, if any
        $destRoomNum = $self->currentRegionmap->fetchRoom(
            $xBlocks,
            $yBlocks,
            $self->currentRegionmap->currentLevel,
        );

        if (! $destRoomNum) {

            # No room at this location
            return undef;

        } else {

            $destRoomObj = $self->worldModelObj->ivShow('modelHash', $destRoomNum);
            if (
                $destRoomObj eq $parentRoomObj
                || ($existDestRoomObj && $destRoomObj eq $existDestRoomObj)
            ) {
                # Can't connect a draggable exit to its own room or to its existing destination
                #   room (if any)
                return undef;
            }
        }

        # Get the current exit drawing mode. GA::Obj::WorldModel->drawExitMode is one of the values
        #   'ask_regionmap', 'no_exit', 'simple_exit' and 'complex_exit'
        if ($self->worldModelObj->drawExitMode eq 'ask_regionmap') {
            $mode = $self->currentRegionmap->drawExitMode;
        } else {
            $mode = $self->worldModelObj->drawExitMode;
        }

        # Find the coordinates of the pixel at the top-left corner of the room's border
        if ($mode eq 'no_exit') {

            # Draw exit mode 'no_exit': The room takes up the whole gridblock
            ($startX, $startY) = (0, 0);

        } else {

            # Draw exit mode 'simple_exit'/'complex_exit': The room takes up the central part of the
            #   gridblock
            ($startX, $startY) = $self->getBorderCorner(
                0,  # $roomObj->xPosBlocks,
                0,  # $roomObj->yPosBlocks,
                0,  # $blockCornerXPosPixels,
                0,  # $blockCornerYPosPixels,
            );
        }

        # Find the coordinates of the pixel at the bottom-right of the room's border
        if ($mode eq 'no_exit') {

            # Delete 2 pixels to allow a 1-pixel border on each side of the room box; otherwise,
            #   the room's borders touch and will look like double-width lines
            $stopX = $startX + $self->currentRegionmap->blockWidthPixels - 3;
            $stopY = $startY + $self->currentRegionmap->blockHeightPixels - 3;

        # In draw exit modes 'simple_exit'/'complex_exit', the room takes up the middle part of the
        #   gridblock
        } else {

            $stopX = $startX + $self->currentRegionmap->roomWidthPixels - 1;
            $stopY = $startY + $self->currentRegionmap->roomHeightPixels - 1;
        }

        # The mouse button was released over a gridblock containing a suitable room. Was the mouse
        #   over the room itself, or in the surrounding empty space?
        if (
            $clickXPosPixels >= (
                $startX + ($xBlocks * $self->currentRegionmap->blockWidthPixels)
            ) && $clickXPosPixels <= (
                $stopX + ($xBlocks * $self->currentRegionmap->blockWidthPixels)
            ) && $clickYPosPixels >= (
                $startY + ($yBlocks * $self->currentRegionmap->blockHeightPixels)
            ) && $clickYPosPixels <= (
                $stopY + ($yBlocks * $self->currentRegionmap->blockHeightPixels)
            )
        ) {
            # Mouse button was released over a suitable destination room
            return $destRoomObj;

        } else {

            # Mouse button was not release over a suitable destination room
            return undef;
        }
    }

    sub findExitClick {

        # Called by $self->addBendCallback and ->findExitBend
        # After a right-click on an exit, when the user has selected 'add bend' in the popup menu,
        #   find the position of the click relative to the start of the middle (bending) section of
        #   the drawn exit (the bending section starts at the same position, that an uncertain exit
        #   would end). Returns the relative position of the click, as well as the absolute
        #   position of the start and end of the bending section
        # (When called by ->findExitBend, process a left-mouse click)
        #
        # Expected arguments
        #   $exitObj                - The exit that was right-clicked
        #   $clickXPos, $clickYPos  - The position of the mouse click on the canvas
        #
        # Return values
        #   An empty list on improper arguments or if the relative coordinates can't be found
        #   Otherwise returns a list of coordinates in the form
        #       (
        #           start_section_x, start_section_y,
        #           mouse_click_x, mouse_click_y,
        #           end_section_x, end_section_y,
        #           result_type,
        #       )
        #   ...where 'start_section_x', 'start_section_y' are absolute coordinates of the start
        #       of the bending section, and where 'mouse_click_x', 'mouse_click_y', 'end_section_x'
        #       and 'end_section_y' are coordinates relative to the start, and 'result_type' gives
        #       more information about the click: 1 if the click took place in the parent room's
        #       gridblock, 2 if the click took place in the destination room's gridblock, 3 if the
        #       click took place close enough to an existing bend, that no additional bend can be
        #       added there, or 0 otherwise

        my ($self, $exitObj, $clickXPos, $clickYPos, $check) = @_;

        # Local variables
        my (
            $mapDir, $mode, $roomObj, $xPos, $yPos, $posnListRef, $destRoomObj, $destXPos,
            $destYPos, $oppDir, $oppPosnListRef, $resultType, $bendSize,
            @emptyList, @returnList, @offsetList,
        );

        # Check for improper arguments
        if (! defined $exitObj || ! defined $clickXPos || ! defined $clickYPos || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->findExitClick', @_);
            return @emptyList;
        }

        # Fetch the equivalent primary direction (the direction in which the exit is drawn on the
        #   map)
        $mapDir = $exitObj->mapDir;
        if (! $mapDir) {

            return @emptyList;
        }

        # Get the current exit drawing mode. GA::Obj::WorldModel->drawExitMode is one of the values
        #   'ask_regionmap', 'no_exit', 'simple_exit' and 'complex_exit'. The regionmap's
        #   ->drawExitMode is any of those values except 'ask_regionmap'
        if ($self->worldModelObj->drawExitMode eq 'ask_regionmap') {
            $mode = $self->currentRegionmap->drawExitMode;
        } else {
            $mode = $self->worldModelObj->drawExitMode;
        }

        # Get the exit's parent room
        $roomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->parent);
        # Get the position of $roomObj's gridblock
        $xPos = $roomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
        $yPos = $roomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

        # Get the coordinates of this exit, if it were drawn as an uncertain exit (to make sure the
        #   coordinates are correct for this $mode, call ->preDrawExits)
        $self->preDrawExits($mode);
        $posnListRef = $self->ivShow('preDrawnUncertainExitHash', $mapDir);

        push (@returnList,
            ($xPos + $$posnListRef[2]),                      # start_section_x
            ($yPos + $$posnListRef[3]),                      # start_section_y
            ($clickXPos - ($xPos + $$posnListRef[2])),       # mouse_click_x
            ($clickYPos - ($yPos + $$posnListRef[3])),       # mouse_click_y
        );

        # Get the destination room and its gridblock's position
        $destRoomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->destRoom);
        $destXPos = $destRoomObj->xPosBlocks * $self->currentRegionmap->blockWidthPixels;
        $destYPos = $destRoomObj->yPosBlocks * $self->currentRegionmap->blockHeightPixels;

        # Now find the absolute coordinates of the end of the bending section of the exit
        $oppDir = $axmud::CLIENT->ivShow('constOppDirHash', $exitObj->mapDir);
        $oppPosnListRef = $self->ivShow('preDrawnUncertainExitHash', $oppDir);

        push (@returnList,
            (($destXPos + $$oppPosnListRef[0]) - ($xPos + $$posnListRef[2])),
            (($destYPos + $$oppPosnListRef[1]) - ($yPos + $$posnListRef[3])),
        );

        # Find out if the click took place inside the parent or destination rooms' gridblocks, and
        #   if the click is close to an existing exit bend
        if (
            $clickXPos >= $xPos
            && $clickXPos < ($xPos + $self->currentRegionmap->blockWidthPixels)
            && $clickYPos >= $yPos
            && $clickYPos < ($yPos + $self->currentRegionmap->blockHeightPixels)
        ) {
            # Click took place inside the parent room's gridblock
            $resultType = 1;

        } elsif (
            $clickXPos >= $destXPos
            && $clickXPos < ($destXPos + $self->currentRegionmap->blockWidthPixels)
            && $clickYPos >= $destYPos
            && $clickYPos < ($destYPos + $self->currentRegionmap->blockHeightPixels)
        ) {
            # Click took place inside the destination room's gridblock
            $resultType = 2;

        } else {

            # Check the click against the position of any of the exit's existing bends
            if ($exitObj->bendOffsetList) {

                @offsetList = $exitObj->bendOffsetList;
                $bendSize = $self->exitBendSize;

                do {

                    my ($offsetXPos, $offsetYPos);

                    # Convert the bend's relative coordinates, stored in @offsetList in the form
                    #   (x, y, x, y...), into absolute coordinates
                    $offsetXPos = (shift @offsetList) + $returnList[0];
                    $offsetYPos = (shift @offsetList) + $returnList[1];

                    if (
                        $clickXPos >= ($offsetXPos - ($bendSize * 3))
                        && $clickXPos < ($offsetXPos + ($bendSize * 3))
                        && $clickYPos >= ($offsetYPos - ($bendSize * 3))
                        && $clickYPos < ($offsetYPos + ($bendSize * 3))
                    ) {
                        # Click took place close enough to a bend, that no additional bend can be
                        #   added at this point
                        $resultType = 3;
                        # Don't check any remaining bends
                        @offsetList = ();
                    }

                } until (! @offsetList);
            }

            if (! defined $resultType) {

                $resultType = 0;
            }
        }

        push (@returnList, $resultType);

        # Return the coordinates
        return @returnList;
    }

    sub findExitBend {

        # Called by $self->removeBendCallback and ->startDrag
        # After a mouse click on an exit, find the bend at (or very close) to the mouse click, and
        #   return its number. The first bend in the list is #0, the second is #1, etc
        #
        #
        # Expected arguments
        #   $exitObj        - The clicked GA::Obj::Exit
        #   $xPos, $yPos    - The position of the mouse click on the canvas
        #
        # Return values
        #   'undef' on improper arguments, if the exit has no bends or if the mouse click was not
        #       close enough to a bend
        #   Otherwise, returns the number of the bend (the bend nearest to the start of the exit
        #       is numbered 0)

        my ($self, $exitObj, $xPos, $yPos, $check) = @_;

        # Local variables
        my (
            $startXPos, $startYPos, $clickXPos, $clickYPos,  $number, $count,
            @offsetList,
        );

        # Check for improper arguments
        if (! defined $exitObj || ! defined $xPos || ! defined $yPos || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->findExitBend', @_);
        }

        if (! $exitObj->bendOffsetList) {

            # Exit has no bends
            return undef;
        }

        # Get the absolute coordinates of the start of the middle (bending) section of the
        #   exit, and the coordinates of the mouse click relative to the start
        ($startXPos, $startYPos, $clickXPos, $clickYPos)
            = $self->findExitClick($exitObj, $xPos, $yPos);

        # Check every drawn bend on the exit, looking for one which is close enough to the mouse
        #   click
        @offsetList = $exitObj->bendOffsetList;
        $number = scalar @offsetList;
        $count = -1;
        do {

            my ($xPos, $yPos);

            $xPos = shift @offsetList;
            $yPos = shift @offsetList;
            $count++;

            if (
                (abs ($clickXPos - $xPos)) <= ($self->exitBendSize * 2)
                && (abs ($clickYPos - $yPos)) <= ($self->exitBendSize * 2)
            ) {
                # Bend found
                return $count;
            }

        } until (! @offsetList);

        # No bend found
        return undef;
    }

    # Compilation/check functions

    sub compileSelectedRooms {

        # Called by several menu/callback functions
        # Returns a list of selected rooms, combining the contents of $self->selectedRoom and
        #   ->selectedRoomHash
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   An empty list on improper arguments, if there is no current regionmap or if
        #       $self->selectedRoom and ->selectedRoomHash are both empty
        #   Otherwise, returns a list containing blessed references to the GA::ModelObj::Room
        #       objects stored in $self->selectedRoom or $self->selectedRoomHash

        my ($self, $check) = @_;

        # Local variables
        my (@emptyList, @returnList);

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->compileSelectedRooms', @_);
            return @emptyList;
        }

        # No selected rooms if there's no regionmap
        if (! $self->currentRegionmap) {

            return @emptyList;
        }

        # Compile selected rooms
        if ($self->selectedRoom) {

            push (@returnList, $self->selectedRoom);
        }

        if ($self->selectedRoomHash) {

            push (@returnList, $self->ivValues('selectedRoomHash'));
        }

        return @returnList;
    }

    sub compileSelectedRoomTags {

        # Called by several menu/callback functions
        # Returns a list of selected room tags, combining the contents of $self->selectedRoomTag
        #   and ->selectedRoomTagHash
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   An empty list on improper arguments, if there is no current regionmap or if
        #       $self->selectedRoomTag and ->selectedRoomTagHash are both empty
        #   Otherwise, returns a list containing blessed references to the GA::ModelObj::Room
        #       objects (all of which have room tags) stored in $self->selectedRoomTag or
        #       $self->selectedRoomTagHash

        my ($self, $check) = @_;

        # Local variables
        my (@emptyList, @returnList);

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->compileSelectedRoomTags', @_);
            return @emptyList;
        }

        # No selected room tags if there's no regionmap
        if (! $self->currentRegionmap) {

            return @emptyList;
        }

        # Compile selected room tags
        if ($self->selectedRoomTag) {

            push (@returnList, $self->selectedRoomTag);
        }

        if ($self->selectedRoomTagHash) {

            push (@returnList, $self->ivValues('selectedRoomTagHash'));
        }

        return @returnList;
    }

    sub compileSelectedRoomGuilds {

        # Called by several menu/callback functions
        # Returns a list of selected room guilds, combining the contents of $self->selectedRoomGuild
        #   and ->selectedRoomGuildHash
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   An empty list on improper arguments, if there is no current regionmap or if
        #       $self->selectedRoomGuild and ->selectedRoomGuildHash are both empty
        #   Otherwise, returns a list containing blessed references to the GA::ModelObj::Room
        #       objects (all of which have room guilds) stored in $self->selectedRoomGuild or
        #       $self->selectedRoomGuildHash

        my ($self, $check) = @_;

        # Local variables
        my (@emptyList, @returnList);

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->compileSelectedRoomGuilds', @_);
            return @emptyList;
        }

        # No selected room guilds if there's no regionmap
        if (! $self->currentRegionmap) {

            return @emptyList;
        }

        # Compile selected room guilds
        if ($self->selectedRoomGuild) {

            push (@returnList, $self->selectedRoomGuild);
        }

        if ($self->selectedRoomGuildHash) {

            push (@returnList, $self->ivValues('selectedRoomGuildHash'));
        }

        return @returnList;
    }

    sub compileSelectedExits {

        # Called by several menu/callback functions
        # Returns a list of selected exits, combining the contents of $self->selectedExit and
        #   ->selectedExitHash
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   An empty list on improper arguments, if there is no current regionmap or if
        #       $self->selectedExit and ->selectedExitHash are both empty
        #   Otherwise, returns a list containing blessed references to the GA::Obj::Exit objects
        #       stored in $self->selectedExit or $self->selectedExitHash

        my ($self, $check) = @_;

        # Local variables
        my (@emptyList, @returnList);

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->compileSelectedExits', @_);
            return @emptyList;
        }

        # No selected exits if there's no regionmap
        if (! $self->currentRegionmap) {

            return @emptyList;
        }

        # Compile selected exits
        if ($self->selectedExit) {

            push (@returnList, $self->selectedExit);
        }

        if ($self->selectedExitHash) {

            push (@returnList, $self->ivValues('selectedExitHash'));
        }

        return @returnList;
    }

    sub compileSelectedExitTags {

        # Called by several menu/callback functions
        # Returns a list of selected exit tags, combining the contents of $self->selectedExitTag
        #   and ->selectedExitTagHash
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   An empty list on improper arguments, if there is no current regionmap or if
        #       $self->selectedExitTag and ->selectedExitTagHash are both empty
        #   Otherwise, returns a list containing blessed references to the GA::Obj::Exit objects
        #       (all of which have exit tags) stored in $self->selectedExitTag or
        #       $self->selectedExitTagHash

        my ($self, $check) = @_;

        # Local variables
        my (@emptyList, @returnList);

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->compileSelectedExitTags', @_);
            return @emptyList;
        }

        # No selected exit tags if there's no regionmap
        if (! $self->currentRegionmap) {

            return @emptyList;
        }

        # Compile selected exit tags
        if ($self->selectedExitTag) {

            push (@returnList, $self->selectedExitTag);
        }

        if ($self->selectedExitTagHash) {

            push (@returnList, $self->ivValues('selectedExitTagHash'));
        }

        return @returnList;
    }

    sub compileSelectedLabels {

        # Called by several menu/callback functions
        # Returns a list of selected labels, combining the contents of $self->selectedLabel and
        #   ->selectedLabelHash
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   An empty list on improper arguments, if there is no current regionmap or if
        #       $self->selectedLabel and ->selectedLabelHash are both empty
        #   Otherwise, returns a list containing blessed references to the GA::Obj::MapLabel
        #       objects stored in $self->selectedLabel or $self->selectedLabelHash

        my ($self, $check) = @_;

        # Local variables
        my (@emptyList, @returnList);

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->compileSelectedLabels', @_);
            return @emptyList;
        }

        # No selected labels if there's no regionmap
        if (! $self->currentRegionmap) {

            return @emptyList;
        }

        # Compile selected labels
        if ($self->selectedLabel) {

            push (@returnList, $self->selectedLabel);
        }

        if ($self->selectedLabelHash) {

            push (@returnList, $self->ivValues('selectedLabelHash'));
        }

        return @returnList;
    }

    sub compileExitList {

        # Called by several functions
        # Returns a list containing the selected exit, its twin exit (if it has one), and any exits
        #   for which those two are the shadow exits
        # Also returns a hash, ready for display in a combobox, so that the user can select the
        #   exit(s) on which to operate
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise the list containing two elements, a reference to @stringList and a reference
        #       to %exitHash
        #   %exitHash is in the form
        #       $exitHash{long_string_describing_exit} = blessed_ref_of_exit_object
        #   @string contains a list of keys in %exitHash, in the order in which they were added

        my ($self, $check) = @_;

        # Local variables
        my (
            $twinExitObj,
            @exitList, @stringList,
            %exitHash,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->compileExitList', @_);
        }

        # Compile a list of exits. Start with the selected exit, and its twin exit
        #   (if there is one)
        push (@exitList, $self->selectedExit);

        if ($self->selectedExit->twinExit) {

            $twinExitObj
                = $self->worldModelObj->ivShow('exitModelHash', $self->selectedExit->twinExit);

            if ($twinExitObj) {

                push (@exitList, $twinExitObj);
            }
        }

        # Add entries to a hash that we use to populate a combobox
        foreach my $exitObj (@exitList) {

            my $string = '#' . $exitObj->number . ' ' . $exitObj->dir . ' (room #'
                . $exitObj->parent . ')';

            push (@stringList, $string);
            $exitHash{$string} = $exitObj;
        }

        # Check the parent room(s) of the exit (and its twin exit, if there is one)
        # If either of these rooms have exits whose shadow exits are already on @objList, add those
        #   exits, too
        foreach my $exitObj (@exitList) {

            my $parentRoomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->parent);

            foreach my $otherExitNumber ($parentRoomObj->ivValues('exitNumHash')) {

                my $otherExitObj = $self->worldModelObj->ivShow('exitModelHash', $otherExitNumber);

                if (
                    $otherExitObj ne $exitObj
                    && $otherExitObj->shadowExit
                    && $otherExitObj->shadowExit eq $exitObj->number
                ) {
                    my $string = '#' . $otherExitObj->number . ' ' . $otherExitObj->dir
                        . ' (shadow of exit #' . $exitObj->number . ') (room #'
                        . $parentRoomObj->number . ')';

                    push (@stringList, $string);
                    $exitHash{$string} = $otherExitObj;
                }
            }
        }

        return (\@stringList, \%exitHash);
    }

    sub checkCredibleExit {

        # Called by $self->findClickedExit and $self->findOverlayingExit to check whether a mouse
        #   click is credibly near an exit (and not miles and miles away)
        # If successful, the calling function can go to the trouble of making more precise checks
        #
        # Expected arguments
        #   $clickXPosPixels, $clickYPosPixels
        #           - The coordinates of the clicked pixel
        #   $startXPosPixels, $startYPosPixels,
        #           - The coordinates of one end of the exit
        #   $stopXPosPixels, $stopYPosPixels
        #           - The coordinates of the other end of the exit
        #
        # Return values
        #   'undef' on improper arguments or if the mouse click is not credibly near an exit
        #   1 if the mouse click is credibly near an exit

        my (
            $self, $clickXPosPixels, $clickYPosPixels, $startXPosPixels, $startYPosPixels,
            $stopXPosPixels, $stopYPosPixels, $check
        ) = @_;

        # Check for improper arguments
        if (
            ! defined $clickXPosPixels || ! defined $clickYPosPixels || ! defined $startXPosPixels
            || ! defined $startYPosPixels || ! defined $stopXPosPixels || ! defined $stopYPosPixels
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->checkCredibleExit', @_);
        }

        # Do the credibility check
        if (
            (
                $startXPosPixels < $stopXPosPixels && (
                    $clickXPosPixels < $startXPosPixels || $clickXPosPixels > $stopXPosPixels
                )
            ) || (
                $startXPosPixels > $stopXPosPixels && (
                    $clickXPosPixels > $startXPosPixels || $clickXPosPixels < $stopXPosPixels
                )
            ) || (
                $startYPosPixels < $stopYPosPixels && (
                    $clickYPosPixels < $startYPosPixels || $clickYPosPixels > $stopYPosPixels
                )
            ) || (
                $startYPosPixels > $stopYPosPixels && (
                    $clickYPosPixels > $startYPosPixels || $clickYPosPixels < $stopYPosPixels
                )
            )
        ) {
            # The mouse click is too far away from the exit to be worth checking more precisely
            return undef;

        } else {

            # The mouse click is credibly close to the exit to be worth checking more preciselhy
            return 1;
        }
    }

    # Prompt functions ('dialogue' windows uniqe to the Automapper window)

    sub promptGridBlock {

        # Called by $self->addRoomAtBlockCallback or ->addLabelAtBlockCallback
        # Prompts the user to enter a gridblock via a 'dialogue' window, and returns the gridblock
        #   entered
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   An empty list on improper arguments or if the user clicks the 'cancel' button
        #   Otherwise returns a list in the form (x, y, z)

        my ($self, $check) = @_;

        # Local variables
        my (
            $response, $xPosBlocks, $yPosBlocks, $zPosBlocks,
            @emptyList,
        );

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->promptGridBlock', @_);
            return @emptyList;
        }

        # Show the 'dialogue' window
        my $dialogueWin = Gtk2::Dialog->new(
            'Select gridblock',
            $self->winWidget,          # Parent window is this window
            [qw/modal destroy-with-parent/],
            'gtk-cancel' => 'reject',
            'gtk-ok'     => 'accept',
        );

        $dialogueWin->set_position('center-always');
        $dialogueWin->set_icon_list($axmud::CLIENT->desktopObj->dialogueWinIconList);

        $dialogueWin->signal_connect('delete-event' => sub {

            $dialogueWin->destroy();
            $self->restoreFocus();

            return @emptyList;
        });

        # Add widgets to the 'dialogue' window
        my $vBox = $dialogueWin->vbox;
        # The call to ->addDialogueIcon splits $vBox in two, with an icon on the left, and a new
        #   Gtk2::VBox on the right, into which we put everything
        my $vBox2 = $self->addDialogueIcon($vBox);

        # X co-ordinate
        my $labelX = Gtk2::Label->new();
        $vBox2->pack_start($labelX, 0, 0, 5);
        $labelX->set_alignment(0, 0);
        $labelX->set_markup(
            'X co-ordinate (0-' . ($self->currentRegionmap->gridWidthBlocks - 1) . ')'
        );

        my $entryX = Gtk2::Entry->new();
        $vBox2->pack_start($entryX, 0, 0, 5);

        # Y co-ordinate
        my $labelY = Gtk2::Label->new;
        $vBox2->pack_start($labelY, 0, 0, 5);
        $labelY->set_alignment(0, 0);
        $labelY->set_markup(
            'Y co-ordinate (0-' . ($self->currentRegionmap->gridHeightBlocks - 1) . ')'
        );

        my $entryY = Gtk2::Entry->new();
        $vBox2->pack_start($entryY, 0, 0, 5);

        # Z co-ordinate
        my $labelZ = Gtk2::Label->new;
        $vBox2->pack_start($labelZ, 0, 0, 5);
        $labelZ->set_alignment(0, 0);
        $labelZ->set_markup(
            'Z co-ordinate (any value)'
        );

        my $entryZ = Gtk2::Entry->new();
        $vBox2->pack_start($entryZ, 0, 0, 5);

        # As a timesaver, enter the currently displayed level (if any)
        if ($self->currentRegionmap) {

            $entryZ->set_text($self->currentRegionmap->currentLevel);
        }

        # Display the dialogue
        $vBox->show_all();

        # Get the response. If the user clicked 'cancel', $response will be 'reject'
        # Otherwise, user clicked 'ok', and we need to get the contents of the three boxes
        $response = $dialogueWin->run();
        if ($response eq 'accept') {

            $xPosBlocks = $entryX->get_text();
            $yPosBlocks = $entryY->get_text();
            $zPosBlocks = $entryZ->get_text();

            $dialogueWin->destroy();
            $self->restoreFocus();

            return ($xPosBlocks, $yPosBlocks, $zPosBlocks);

        } else {

            $dialogueWin->destroy();
            $self->restoreFocus();

            return @emptyList;
        }
    }

    sub promptNewExit {

        # Called by $self->addExitCallback, ->changeDirCallback or ->setAssistedMoveCallback
        # Prompts the user to enter a primary (and nominal) direction for an exit, and returns the
        #   directions entered
        #
        # Expected arguments
        #   $roomObj    - The GA::ModelObj::Room to which a new exit is about to be added
        #   $title      - The 'dialogue' window title - 'Add exit' or 'Change exit'
        #
        # Optional arguments
        #   $exitObj    - When not called by $self->addExitCallback, the existing GA::Obj::Exit
        #   $mode       - Set to 'change_dir' when called by $self->changeDirCallback, set to
        #                   'set_assist' when called by $self->setAssistedMoveCallback (set to
        #                   'undef' when called by ->addExitCallback)
        #
        # Return values
        #   On improper arguments or if the user clicks the cancel button, returns an empty list
        #   Otherwise returns a list of two or four elements, some of which may be 'undef':
        #       (nominal_direction, map_direction, profile, assisted_move_for_this_profile)

        my ($self, $roomObj, $title, $exitObj, $mode, $check) = @_;

        # Local variables
        my (
            $standardDir, $nominalDir, $assistedProf, $assistedMove, $match, $response,
            @emptyList, @shortList, @longList, @extraList, @dirList, @availableList, @profList,
            %checkHash,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! defined $title || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->promptNewExit', @_);
            return @emptyList;
        }

        # Prepare a list of standard primary directions. Whether we include 'northnortheast', etc,
        #   depends on the current value of $self->worldModelObj->showAllPrimaryFlag
        @shortList = qw(north northeast east southeast south southwest west northwest up down);
        # (For convenience, put the longest directions at the end)
        @longList = qw(
            north northeast east southeast south southwest west northwest up down
            northnortheast eastnortheast eastsoutheast southsoutheast
            southsouthwest westsouthwest westnorthwest northnorthwest
        );
        # If none of the eight usual cardinal directions are available, then the user must be
        #   offered 'northnortheast', etc. These IVs are used to check for that possibility
        @extraList = qw(
            northnortheast eastnortheast eastsoutheast southsoutheast
            southsouthwest westsouthwest westnorthwest northnorthwest
        );
        %checkHash = (
            'north'         => undef,
            'northeast'     => undef,
            'east'          => undef,
            'southeast'     => undef,
            'south'         => undef,
            'southwest'     => undef,
            'west'          => undef,
            'northwest'     => undef,
        );

        if ($self->worldModelObj->showAllPrimaryFlag) {
            @dirList = @longList;
        } else {
            @dirList = @shortList;
        }

        # Extract all the available (standard) primary directions (those not in use by other exits)
        OUTER: foreach my $dir (@dirList) {

            INNER: foreach my $number ($roomObj->ivValues('exitNumHash')) {

                my $thisExitObj = $self->worldModelObj->ivShow('exitModelHash', $number);

                if (
                    $thisExitObj->mapDir
                    && $thisExitObj->mapDir eq $dir
                    && $thisExitObj->drawMode ne 'temp_alloc'
                    && $thisExitObj->drawMode ne 'temp_unalloc'
                ) {
                    # $dir isn't available
                    if (exists $checkHash{$dir}) {

                        delete $checkHash{$dir};
                    }

                    next OUTER;
                }
            }

            # $dir is available
            push (@availableList, $dir);
        }

        # If the eight usual cardinal directions are not available, then the user must be offered
        #   'northnortheast', etc, even if ->showAllPrimaryFlag is not set
        if (! $self->worldModelObj->showAllPrimaryFlag && ! %checkHash) {

            OUTER: foreach my $dir (@extraList) {

                INNER: foreach my $number ($roomObj->ivValues('exitNumHash')) {

                    my $thisExitObj = $self->worldModelObj->ivShow('exitModelHash', $number);

                    if (
                        $thisExitObj->mapDir
                        && $thisExitObj->mapDir eq $dir
                        && $thisExitObj->drawMode ne 'temp_alloc'
                        && $thisExitObj->drawMode ne 'temp_unalloc'
                    ) {
                        # $dir isn't available
                        next OUTER;
                    }
                }

                # $dir is available
                push (@availableList, $dir);
            }
        }

        # Get a sorted list of all non-world profiles...
        foreach my $profObj ($self->session->ivValues('profHash')) {

            if ($profObj->category ne 'world') {

                push (@profList, $profObj->name);
            }
        }

        @profList = sort {lc($a) cmp lc($b)} (@profList);

        # ...and put the current world profile at the top of the list
        unshift (@profList, $self->session->currentWorld->name);

        # That completes the setup. Now, show the 'dialogue' window
        my $dialogueWin = Gtk2::Dialog->new(
            $title,
            $self->winWidget,
            [qw/modal destroy-with-parent/],
            'gtk-cancel' => 'reject',
            'gtk-ok'    => 'accept',
        );

        $dialogueWin->set_position('center-always');
        $dialogueWin->set_icon_list($axmud::CLIENT->desktopObj->dialogueWinIconList);

        $dialogueWin->signal_connect('delete-event' => sub {

            $dialogueWin->destroy();
            $self->restoreFocus();

            return @emptyList;
        });

        # Add widgets to the 'dialogue' window
        my $vBox = $dialogueWin->vbox;
        # The call to ->addDialogueIcon splits $vBox in two, with an icon on the left, and a new
        #   Gtk2::VBox on the right, into which we put everything
        my $vBox2 = $self->addDialogueIcon($vBox);

        # Add widgets
        my ($entry, $entry2, $entry3, $entry4, $comboBox, $comboBox2);

        # Called by $self->addExitCallback or ->changeDirCallback
        if (! $exitObj || ($exitObj && $mode eq 'change_dir')) {

            # Work out which map directon should be displayed in the first combobox (corresponds
            #   to GA::Obj::Exit->mapDir, if defiend)
            if ($exitObj && $exitObj->mapDir && $mode eq 'change_dir') {

                # Use the existing exit's map direction, ->mapDir (if defined). Does this direction
                #   already exist in @availableList?
                if (@availableList) {

                    OUTER: for (my $count = 0; $count < @availableList; $count++) {

                        if ($availableList[$count] eq $exitObj->mapDir) {

                            $match = $count;
                            last OUTER;
                        }
                    }
                }

                if (! defined $match) {

                    # The existing map direction is missing from the list; add it to
                    #   the beginning
                    unshift(@availableList, $exitObj->mapDir);
                    $match = 0;
                }

            } else {

                # Use the first map direction in the list initially
                $match = 0;
            }

            # Add a combobox to choose the map direction
            my $label = Gtk2::Label->new();
            $vBox2->pack_start($label, 0, 0, 5);
            $label->set_alignment(0, 0);
            $label->set_markup('Standard map direction, e.g. <i>\'north\' </i>');

            $comboBox = Gtk2::ComboBox->new_text;
            $vBox2->pack_start($comboBox, 0, 0, 5);
            foreach my $dir (@availableList) {

                $comboBox->append_text($dir);
            }
            $comboBox->set_active($match);

            # Add an entry box to choose the exit's nominal direction, if different
            my $label2 = Gtk2::Label->new();
            $vBox2->pack_start($label2, 0, 0, 5);
            $label2->set_alignment(0, 0);
            $label2->set_markup('Matches named exit, e.g. <i>\'north\', \'portal\' </i>');

            $entry = Gtk2::Entry->new();
            $vBox2->pack_start($entry, 0, 0, 5);

            if ($exitObj && $mode eq 'change_dir') {

                # The entry should contain the existing exit object's nominal direction
                $entry->set_text($exitObj->dir);

            } else {

                # The entry should contain the same direction selected in the combobox, unless the
                #   user manually types a different value
                if (@availableList) {

                    $entry->set_text(
                        $self->session->currentDict->ivShow(
                            'primaryDirHash',
                            $availableList[0],
                        ),
                    );
                }
            }

            # When the user selects a new direction in the combobox, automatically update the entry
            #   (unless it's a call from ->changeDirCallback, in which case we only update the entry
            #   if it originally matched the map direction - this prevents the unfortunate situation
            #   of an exit whose ->mapDir is 'north' and whose ->dir is 'in' from having both set to
            #   'northwest', when the user only meant to change the map direction
            if ($exitObj && $mode eq 'change_dir') {

                # (This function called by ->changeDirCallback)
                $comboBox->signal_connect('changed' => sub {

                    my ($text, $customDir);

                    $text = $comboBox->get_active_text();

                    if ($exitObj->mapDir) {

                        $customDir = $self->session->currentDict->ivShow(
                            'primaryDirHash',
                            $exitObj->mapDir,
                        );

                        if ($customDir eq $exitObj->dir) {

                            $entry->set_text($comboBox->get_active_text());
                        }
                    }
                });

            } else {

                # (This function not called by ->changeDirCallback)
                $comboBox->signal_connect('changed' => sub {

                    my $text = $comboBox->get_active_text();

                    $entry->set_text($self->session->currentDict->ivShow('primaryDirHash', $text));
                });
            }

            my $line = Gtk2::HSeparator->new();
            $vBox2->pack_start($line, 0, 0, 5);
        }

        # Called by ->setAssistedMoveCallback
        if ($exitObj && $mode eq 'set_assist') {

            # Add two entry boxes, which can't be modified, to inform the user which exit they're
            #   modifying. The format is the same as for the add exit 'dialogue' window
            my $label = Gtk2::Label->new();
            $vBox2->pack_start($label, 0, 0, 5);
            $label->set_alignment(0, 0);
            $label->set_markup('Direction drawn on map, e.g. <i>\'north\' </i>');

            $entry3 = Gtk2::Entry->new();
            $vBox2->pack_start($entry3, 0, 0, 5);
            if ($exitObj->mapDir) {
                $entry3->set_text($exitObj->mapDir);
            } else {
                $entry3->set_text('unallocatable');
            }
            $entry3->set_sensitive(FALSE);

            my $label2 = Gtk2::Label->new();
            $vBox2->pack_start($label2, 0, 0, 5);
            $label2->set_alignment(0, 0);
            $label2->set_markup('Matches named exit, e.g. <i>\'north\', \'portal\' </i>');

            $entry4 = Gtk2::Entry->new();
            $vBox2->pack_start($entry4, 0, 0, 5);
            $entry4->set_text($exitObj->dir);
            $entry4->set_sensitive(FALSE);
        }

        # Called by $self->addExitCallback or ->setAssistedMoveCallback
        if (! $exitObj || ($exitObj && $mode eq 'set_assist')) {

            # Add a combobox and an entry to add an optional key-value pair to ->assistedMoveHash
            my $label4 = Gtk2::Label->new();
            $vBox2->pack_start($label4, 0, 0, 5);
            $label4->set_alignment(0, 0);

            if (! $exitObj) {
                $label4->set_markup('Optional: Assisted move for the profile');
            } else {
                $label4->set_markup('Assisted move for the profile');
            }


            $comboBox2 = Gtk2::ComboBox->new_text();
            $vBox2->pack_start($comboBox2, 0, 0, 5);
            foreach my $profile (@profList) {

                $comboBox2->append_text($profile);
            }

            $comboBox2->set_active(FALSE);

            my $label5 = Gtk2::Label->new();
            $vBox2->pack_start($label5, 0, 0, 5);
            $label5->set_alignment(0, 0);
            $label5->set_markup(
                'Command sequence, e.g. <i>\'push button' . $axmud::CLIENT->cmdSep . 'north\' </i>',
            );

            $entry2 = Gtk2::Entry->new();
            $vBox2->pack_start($entry2, 0, 0, 5);

            # When called by ->setAssistedMoveCallback, the entry box should contain the assisted
            #   move for the profile displayed in $comboBox2, if any
            if ($exitObj) {

                $assistedProf = $comboBox2->get_active_text();
                if ($assistedProf && $exitObj->ivExists('assistedHash', $assistedProf)) {

                    $entry2->set_text($exitObj->ivShow('assistedHash', $assistedProf));
                }
            }

            # When the user changes the profile in $comboBox2, the assisted move displayed in
            #   $entry2 should be updated
            $comboBox2->signal_connect('changed' => sub {

                $assistedProf = $comboBox2->get_active_text();
                if (
                    $assistedProf
                    && $exitObj
                    && $exitObj->ivExists('assistedHash', $assistedProf)
                ) {
                    $entry2->set_text($exitObj->ivShow('assistedHash', $assistedProf));

                } else {

                    # Clear the previously displayed text, if any
                    $entry2->set_text('');
                }
            });
        }

        # Display the dialogue
        $vBox->show_all();

        # Get the response
        $response = $dialogueWin->run();

        # If the user clicked 'cancel', $response will be 'reject'
        if ($response ne 'accept') {

            $dialogueWin->destroy();
            # Restore focus to the Automapper window
            $self->restoreFocus();

            return @emptyList;

        # Otherwise, user clicked 'ok', and we need to get the coordinates typed
        } else {

            if ($comboBox && $entry) {

                $standardDir = $comboBox->get_active_text();
                $nominalDir = $entry->get_text();

                # If the entry box is empty, the nominal direction is the same as the standard
                #   direction (it shouldn't be empty, but the user might try to empty it anyway)
                if (! $nominalDir) {

                    $nominalDir = $self->session->currentDict->ivShow(
                        'primaryDirHash',
                        $standardDir,
                    );

                # If a nominal direction was supplied, make sure it's not an abbreviated primary/
                #   secondary direction
                } else {

                    $nominalDir = $self->session->currentDict->unabbrevDir($nominalDir);
                }
            }

            if ($comboBox2 && $entry2) {

                $assistedProf = $comboBox2->get_active_text();
                $assistedMove = $entry2->get_text();
            }

            # Destroy the 'dialogue' window
            $dialogueWin->destroy();
            # Restore focus to the Automapper window
            $self->restoreFocus();

            # Return the specified data
            return ($nominalDir, $standardDir, $assistedProf, $assistedMove);
        }
    }

    sub promptMultipleExits {

        # Called by $self->addMultipleExitsCallback
        # Prompts the user to add one or more exits in primary directions to the currently selected
        #   room. Any primary directions which aren't already in use by an exit can be selected
        #
        # Expected arguments
        #   $roomObj    - The GA::ModelObj::Room to which a new exit is being added
        #
        # Return values
        #   On improper arguments, if the user clicks the cancel button or if there are no available
        #       primary directions, returns an empty list
        #   Otherwise returns a list of (custom) primary directions to use in new exits

        my ($self, $roomObj, $check) = @_;

        # Local variables
        my (
            $response,
            @emptyList, @shortList, @longList, @dirList, @availableList, @widgetList, @returnList,
            %useDirHash,
        );

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->promptMultipleExits', @_);
            return @emptyList;
        }

        # Prepare a list of standard primary directions. Whether we include 'northnortheast', etc,
        #   depends on the current value of $self->worldModelObj->showAllPrimaryFlag
        @shortList = qw(north northeast east southeast south southwest west northwest up down);
        # (For convenience, put the longest directions at the end)
        @longList = qw(
            north northeast east southeast south southwest west northwest up down
            northnortheast eastnortheast eastsoutheast southsoutheast
            southsouthwest westsouthwest westnorthwest northnorthwest
        );

        if ($self->worldModelObj->showAllPrimaryFlag) {
            @dirList = @longList;
        } else {
            @dirList = @shortList;
        }

        # Extract all the available primary directions (those not in use by other exits)
        OUTER: foreach my $standardDir (@dirList) {

            # Get the custom primary direction
            my $customDir = $self->session->currentDict->ivShow('primaryDirHash', $standardDir);

            INNER: foreach my $number ($roomObj->ivValues('exitNumHash')) {

                my $exitObj = $self->worldModelObj->ivShow('exitModelHash', $number);

                if (
                    $exitObj->mapDir
                    && $exitObj->mapDir eq $standardDir
                    && $exitObj->drawMode ne 'temp_alloc'
                    && $exitObj->drawMode ne 'temp_unalloc'
                ) {
                    # The primary direction isn't available
                    next OUTER;
                }
            }

            # $customDir is available
            push (@availableList, $customDir);
        }

        if (! @availableList) {

            $self->showMsgDialogue(
                'Add multiple exits',
                'warning',
                'The selected room already has exits drawn in all primary directions',
                'ok',
            );

            return @emptyList;
        }

        # That completes the setup. Now, show the 'dialogue' window
        my $dialogueWin = Gtk2::Dialog->new(
            'Add multiple exits',
            $self->winWidget,
            [qw/modal destroy-with-parent/],
            'gtk-cancel' => 'reject',
            'gtk-ok'     => 'accept',
        );

        $dialogueWin->set_position('center-always');
        $dialogueWin->set_icon_list($axmud::CLIENT->desktopObj->dialogueWinIconList);

        # Destroy the window, when required
        $dialogueWin->signal_connect('delete-event' => sub {

            $dialogueWin->destroy();
            $self->restoreFocus();

            return undef;
        });

        # Add widgets to the 'dialogue' window
        my $vBox = $dialogueWin->vbox;
        # The call to ->addDialogueIcon splits $vBox in two, with an icon on the left, and a new
        #   Gtk2::VBox on the right, into which we put everything
        my $vBox2 = $self->addDialogueIcon($vBox);

        # Add widgets - a checkbutton and label inside a Gtk2::HBox; one for every available primary
        #   direction
        foreach my $dir (@availableList) {

            my $hBox = Gtk2::HBox->new(FALSE, 0);
            $vBox2->pack_start($hBox, 1, 1, 0);

            my $checkButton = Gtk2::CheckButton->new_with_label($dir);
            $hBox->pack_start($checkButton, 0, 0, 0);
            $checkButton->signal_connect('toggled' => sub {

                if ($checkButton->get_active()) {

                    $useDirHash{$dir} = undef;

                } elsif (exists $useDirHash{$dir}) {

                    delete $useDirHash{$dir};
                }
            });
            push (@widgetList, $checkButton);
        }

        # Add two buttons at the bottom to select all/no available directions
        my $hBox = Gtk2::HBox->new(FALSE, 0);
        $vBox2->pack_start($hBox, 0, 0, 0);

        my $button = Gtk2::Button->new('Select all');
        $hBox->pack_start($button, 1, 1, 0);
        $button->signal_connect('clicked' => sub {

            foreach my $checkButton (@widgetList) {

                $checkButton->set_active(TRUE);
            }
        });

        my $button2 = Gtk2::Button->new('Select none');
        $hBox->pack_end($button2, 1, 1, 0);
        $button2->signal_connect('clicked' => sub {

            foreach my $checkButton (@widgetList) {

                $checkButton->set_active(FALSE);
            }
        });

        # Add a seperator above the OK/Cancel buttons
        my $separator = Gtk2::HSeparator->new();
        $vBox2->pack_start($separator, 0, 0, 10);

        # Display the dialogue
        $vBox->show_all();

        # Get the response
        $response = $dialogueWin->run();

        # If the user clicked 'cancel', $response will be 'reject'
        if ($response ne 'accept') {

            $dialogueWin->destroy();
            $self->restoreFocus();

            return @emptyList;

        # Otherwise, user clicked 'ok', and we need to get an ordered list of selected directions
        } else {

            foreach my $dir (@dirList) {

                if (exists $useDirHash{$dir}) {

                    push (@returnList, $dir);
                }
            }

            $dialogueWin->destroy();
            $self->restoreFocus();

            return @returnList;
        }
    }

    sub promptSpecifyExit {

        # Called by $self->changeDirCallback and $self->setAssistedMoveCallback
        # If the selected exit has a twin and/or shadow exits, the user needs to specify the exit
        #   to modify
        #
        # Expected arguments
        #   $text           - e.g. 'Select which exit to change'
        #
        # Return values
        #   'undef' on improper arguments, or if the user declines to specify an exit
        #   If the selected exit doesn't have either twin or shadow exits, simply returns the
        #       blessed reference of the selected exit
        #   Otherwise prompts the user to choose an exit, and returns it

        my ($self, $text, $check) = @_;

        # Local variables
        my ($stringListRef, $exitHashRef, $choice);

        # Check for improper arguments
        if (! defined $text || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->promptSpecifyExit', @_);
        }

        # Get lists of the exits linked to the selected one
        ($stringListRef, $exitHashRef) = $self->compileExitList();
        if (! defined $stringListRef) {

            return undef;
        }

        # If there is only one exit in the list, we don't need to prompt the user to specify which
        #   one to choose
        if (@$stringListRef == 1) {

            return $$exitHashRef{$$stringListRef[0]};
        }

        # Prompt the user to choose an exit
        $choice = $self->showComboDialogue(
            'Select exit',
            $text,
            FALSE,
            $stringListRef,
        );

        if (! $choice) {

            return undef;

        } else {

            # Return the blessed reference of the chosen exit
            return $$exitHashRef{$choice};
        }
    }

    sub promptFilePath {

        # Called by $self->setFilePathCallback
        # Prompts the user to enter a file path for the selected room and (optionally) the path
        #   to the virtual area, if the room is in one
        #
        # Expected arguments
        #   $roomObj    - The room for which to set a file path
        #
        # Return values
        #   On improper arguments or if the user clicks the cancel button, returns an empty list
        #   Otherwise returns a list of two elements, in the form
        #       (file_path, virtual_area_path)
        #   If the user doesn't enter text into one or the other boxes, one (or both) elements
        #       will be empty strings

        my ($self, $roomObj, $check) = @_;

        # Local variables
        my (
            $msg, $msg2, $response, $filePath, $virtualPath,
            @emptyList,
        );

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->promptFilePath', @_);
            return @emptyList;
        }

        # Create the 'dialogue' window
        my $dialogueWin = Gtk2::Dialog->new(
            'Set file path',
            $self->winWidget,
            [qw/modal destroy-with-parent/],
            'gtk-cancel' => 'reject',
            'gtk-ok' => 'accept',
        );

        $dialogueWin->set_position('center-always');
        $dialogueWin->set_icon_list($axmud::CLIENT->desktopObj->dialogueWinIconList);

        $dialogueWin->signal_connect('delete-event' => sub {

            $dialogueWin->destroy();
            $self->restoreFocus();

            return @emptyList;
        });

        # Add widgets to the 'dialogue' window
        my $vBox = $dialogueWin->vbox;
        # The call to ->addDialogueIcon splits $vBox in two, with an icon on the left, and a new
        #   Gtk2::VBox on the right, into which we put everything
        my $vBox2 = $self->addDialogueIcon($vBox);

        # First label and entry
        $msg = "Enter the path to the room\'s source code file in the mudlib (if known)\n";
        if ($roomObj->sourceCodePath) {
            $msg .= "<i>(current value: " . $roomObj->sourceCodePath . ")</i>";
        } else {
            $msg .= "<i>(current value: not set)</i>";
        }

        my $label = Gtk2::Label->new();
        $vBox2->pack_start($label, 0, 0, 5);
        $label->set_alignment(0, 0);
        $label->set_markup($msg);

        my $entry = Gtk2::Entry->new();
        $vBox2->pack_start($entry, 0, 0, 5);
        # Use the room's existing path, if it has one; otherwise, copy the last path entered by
        #   the user, so they can edit it without re-typing the whole thing
        if ($roomObj->sourceCodePath) {
            $entry->append_text($roomObj->sourceCodePath);
        } elsif ($self->worldModelObj->lastFilePath) {
            $entry->append_text($self->worldModelObj->lastFilePath);
        }

        # Second label and entry
        $msg2 = "(Optional) Enter a path to the virtual area, if the room is in one\n";
        if ($roomObj->virtualAreaPath) {
            $msg2 .= "<i>(current value: " . $roomObj->virtualAreaPath . ")</i>";
        } else {
            $msg2 .= "<i>(current value: not set)</i>";
        }

        my $label2 = Gtk2::Label->new();
        $vBox2->pack_start($label2, 0, 0, 5);
        $label2->set_alignment(0, 0);
        $label2->set_markup($msg2);

        my $hBox = Gtk2::HBox->new(FALSE, 0);
        $vBox->pack_start($hBox, 0, 0, 5);

        my $entry2 = Gtk2::Entry->new();
        $hBox->pack_start($entry2, 1, 1, 0);
        # Use the room's existing virtual area path, if it has one; otherwise use the last path
        #   entered by the user
        if ($roomObj->virtualAreaPath) {
            $entry2->append_text($roomObj->virtualAreaPath);
        } elsif ($self->worldModelObj->lastVirtualAreaPath) {
            $entry2->append_text($self->worldModelObj->lastVirtualAreaPath);
        }

        # 2nd entry box starts insensitive if the 1st entry box doesn't contain text
        if (! $entry->get_text()) {

            $entry2->set_sensitive(FALSE);
        }

        my $tooltips = Gtk2::Tooltips->new();

        my $button = Gtk2::Button->new('Copy');
        $hBox->pack_start($button, 1, 1, 5);
        $button->signal_connect('clicked' => sub {

            # Copy the contents of the first entry into the second
            $entry2->set_text($entry->get_text());
        });
        $tooltips->set_tip($button, 'Copy the file path into this entry box');

        # The second entry can only be edited when the first contains text
        $entry->signal_connect('changed' => sub {

            if (! $entry->get_text()) {

                # First entry is now empty, so empty the second entry too
                $entry2->set_text('');
                $entry2->set_sensitive(FALSE);

            } else {

                # Sensitise the second entry
                $entry2->set_sensitive(TRUE);
            }
        });

        # Display the dialogue
        $vBox->show_all();

        # If the user clicked 'cancel', $response will be 'reject'
        # Otherwise, user clicked 'ok', and we need to get the coordinates typed
        $response = $dialogueWin->run();
        if ($response eq 'accept') {

            $filePath = $entry->get_text();
            $virtualPath = $entry2->get_text();
        }

        $dialogueWin->destroy();
        $self->restoreFocus();

        return ($filePath, $virtualPath);
    }

    sub promptVisits {

        # Called by $self->resetVisitsCallback
        # Prompts the user to ask the character(s) and region(s) in which character visit counts
        #   should be reset
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise returns a list containing three elements:
        #       $result         - Set to 'undef' if no characters/regions selected, 1 if a character
        #                           name matching an existing character profile is selected, 2 if a
        #                           character name that no longer matches an existing character
        #                           profile is selected
        #       $charListRef    - Reference to a list of selected character names
        #       $regionListRef  - Reference to a list of selected regionmaps

        my ($self, $check) = @_;

        # Local variables
        my (
            $currentCharString, $allCharString, $unCharString, $thisRegionString, $allRegionString,
            $charChoice, $regionChoice, $response, $result,
            @emptyList, @charNameList, @charStringList, @regionStringList, @charList, @regionList,
        );

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->promptVisits', @_);
            return @emptyList;
        }

        # Prepare a list of character strings for a combobox
        foreach my $profObj ($self->session->ivValues('profHash')) {

            if (
                $profObj->category eq 'char'
                && (
                    ! $self->session->currentChar
                    || $self->session->currentChar ne $profObj
                )
            ) {
                push @charNameList, $profObj->name;
            }
        }

        if ($self->session->currentChar) {

            $currentCharString = 'Current character (' . $self->session->currentChar->name . ')';
            push (@charStringList, $currentCharString);
        }

        $allCharString = 'All character profiles';
        $unCharString = 'All characters without profiles';
        push (@charStringList, $allCharString, $unCharString, @charNameList);

        # Prepare region strings for a second combobox
        if ($self->currentRegionmap) {

            $thisRegionString = 'Current region (' . $self->currentRegionmap->name . ')';
            push (@regionStringList, $thisRegionString);
        }

        $allRegionString = 'All regions';
        push (@regionStringList, $allRegionString);

        # Show the 'dialogue' window
        my $dialogueWin = Gtk2::Dialog->new(
            'Reset character visits',
            $self->winWidget,
            [qw/modal destroy-with-parent/],
            'gtk-cancel' => 'reject',
            'gtk-ok'     => 'accept',
        );

        $dialogueWin->set_position('center-always');
        $dialogueWin->set_icon_list($axmud::CLIENT->desktopObj->dialogueWinIconList);

        $dialogueWin->signal_connect('delete-event' => sub {

            $dialogueWin->destroy();
            $self->restoreFocus();

            return @emptyList;
        });

        # Add widgets to the 'dialogue' window
        my $vBox = $dialogueWin->vbox;
        # The call to ->addDialogueIcon splits $vBox in two, with an icon on the left, and a new
        #   Gtk2::VBox on the right, into which we put everything
        my $vBox2 = $self->addDialogueIcon($vBox);

        # First label and combo
        my $label = Gtk2::Label->new();
        $vBox2->pack_start($label, 0, 0, 5);
        $label->set_alignment(0, 0);
        $label->set_markup('Select a character (or characters)');

        my $comboBox = Gtk2::ComboBox->new_text();
        $vBox2->pack_start($comboBox, 0, 0, 5);
        # Fill the combo box with the specified lines, and display the first line
        foreach my $string (@charStringList) {

            $comboBox->append_text($string);
        }
        $comboBox->set_active(FALSE);

        # Second label and combo
        my $label2 = Gtk2::Label->new;
        $vBox2->pack_start($label2, 0, 0, 5);
        $label2->set_alignment(0, 0);
        $label2->set_markup('Select a region (or regions)');

        my $comboBox2 = Gtk2::ComboBox->new_text();
        $vBox2->pack_start($comboBox2, 0, 0, 5);
        # Fill the combo box with the specified lines, and display the first line
        foreach my $string (@regionStringList) {

            $comboBox2->append_text($string);
        }
        $comboBox2->set_active(FALSE);

        # Display the dialogue
        $vBox->show_all();

        # If the user clicked 'cancel', $response will be 'reject'
        $response = $dialogueWin->run();

        if ($response ne 'accept') {

            $dialogueWin->destroy();
            $self->restoreFocus();

            return @emptyList;

        # Otherwise, user clicked 'ok', and we need to get the contents of the two comboboxes
        } else {

            $charChoice = $comboBox->get_active_text();
            $regionChoice = $comboBox2->get_active_text();

            # Analyse the results
            if ($charChoice ne $unCharString) {

                # Deal with a profile character
                $result = 1;

            } else {

                # Deal with a non-profile character
                $result = 2;
            }

            # Set the character(s)
            if (defined $currentCharString && $charChoice eq $currentCharString) {

                # Use the current character
                push (@charList, $self->session->currentChar->name);

            } elsif ($charChoice eq $allCharString) {

                # Use all character profiles - including the current character (if there is one),
                #   which isn't in @charNameList
                push (@charList, @charNameList);
                if ($self->session->currentChar) {

                    push (@charList, $self->session->currentChar->name);
                }

            } else {

                # Use the specified character
                push (@charList, $charChoice);
            }

            # Set the region(s)
            if (defined $thisRegionString && $regionChoice eq $thisRegionString) {

                push (@regionList, $self->currentRegionmap);

            } elsif ($regionChoice eq $allRegionString) {

                push (@regionList, $self->worldModelObj->ivValues('regionmapHash'));
            }

            $dialogueWin->destroy();
            $self->restoreFocus();

            return ($result, \@charList, \@regionList);
        }
    }

    # IV setting functions

    sub setMode {

        # Can be called by anything
        # Sets the automapper's operating mode and updates other IVs/widgets
        # NB If the Locator isn't running, a call to this function always sets the mode to 'wait'
        #
        # Expected arguments
        #   $mode   - The new mode:
        #               'wait'      - The automapper isn't doing anything
        #               'follow'    - The automapper is following the character's position, but not
        #                               updating the world model
        #               'update'    - The automapper is updating the world model as the character
        #                               moves around
        #
        # Return values
        #   'undef' on improper arguments, or if an attempt to switch to 'update' mode fails because
        #       the Locator task is expecting room descriptions
        #   1 otherwise

        my ($self, $mode, $check) = @_;

        # Local variables
        my ($taskObj, $title, $menuItemName, $radioMenuItem, $toolbarButtonName, $toolbarButton);

        # Check for improper arguments
        if (
            ! defined $mode || ($mode ne 'wait' && $mode ne 'follow' && $mode ne 'update')
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->setMode', @_);
        }

        # Import the current session's Locator task
        $taskObj = $self->session->locatorTask;

        # If the Locator isn't running or if there is no current regionmap, the mode must be set to
        #   'wait'
        if (! $taskObj || ! $self->currentRegionmap) {

            $mode = 'wait';

        } elsif ($self->mode eq 'update' && $self->worldModelObj->disableUpdateModeFlag) {

            # This function is called just after GA::Obj::WorldModel->toggleDisableUpdateModeFlag
            #   has set ->disableUpdateModeFlag to TRUE. Now that update mode has been disabled,
            #   switch to 'follow' mode
            $mode = 'follow';

        } elsif (
            ($self->mode ne 'update' && $mode eq 'update')
            || ($self->mode eq 'wait' && $mode eq 'follow')
        ) {
            # Don't switch to update mode if it is disabled, or if the session is in 'connect
            #   offline' mode
            if (
                $mode eq 'update'
                && (
                    $self->worldModelObj->disableUpdateModeFlag
                    || $self->session->status eq 'offline'
                )
            ) {
                # Retain the current mode ('wait' or 'follow')
                $mode = $self->mode;

            # If we're trying to switch from 'wait' to 'follow' mode, or from 'wait/'follow' to
            #   'update' mode, the Locator task must not be expecting room descriptions (doing this
            #   prevents the map from adding rooms based on junk data, or from getting lost
            #   immediately because it expected the wrong room)
            # If the Locator is expecting descriptions, refuse to switch mode
            } elsif ($taskObj->moveList) {

                if ($taskObj->moveList == 1) {
                    $title = 'Set mode (1 missing room statement)';
                } else {
                    $title = 'Set mode (' . scalar $taskObj->moveList . ' missing room statements)';
                }

                $self->showMsgDialogue(
                    $title,
                    'warning',
                    'The automapper can\'t switch to \'' . $mode . '\' mode until the Locator task'
                    . ' is no longer expecting any rooms (try: Rooms - Locator task - Reset'
                    . ' Locator)',
                    'ok',
                );

                # Retain the current mode ('wait' or 'follow')
                $mode = $self->mode;
            }
        }

        # Set the automapper's new operating mode
        $self->ivPoke('mode', $mode);

        # Even if $self->mode hasn't changed, it might not match the menu items and the toolbar
        #   button; so we must make sure the right ones are activated. Use
        #   $self->ignoreMenuUpdateFlag so that toggling a menu item doesn't toggle a toolbar icon
        #   (and vice-versa)
        $self->ivPoke('ignoreMenuUpdateFlag', TRUE);

        # Update radio buttons in the menu (if the menu is visible)
        $menuItemName = 'set_'. $mode . '_mode';
        if ($self->menuBar && $self->ivExists('menuToolItemHash', $menuItemName)) {

            $radioMenuItem = $self->ivShow('menuToolItemHash', $menuItemName);
            $radioMenuItem->set_active(TRUE);
        }

        # Update toolbar buttons in the toolbar (if the toolbar is visible)
        $toolbarButtonName = 'icon_set_'. $mode . '_mode';
        if ($self->toolbar && $self->ivExists('menuToolItemHash', $toolbarButtonName)) {

            $toolbarButton = $self->ivShow('menuToolItemHash', $toolbarButtonName);
            $toolbarButton->set_active(TRUE);
        }

        # Make sure that the radio/toolbar buttons for 'update mode' are sensitive, or not
        $self->restrictUpdateMode();
        $self->ivPoke('ignoreMenuUpdateFlag', FALSE);

        # In case the automapper object's ghost room gets set to the wrong room, switching to
        #   'wait' mode temporarily must reset it
        if ($self->mode eq 'wait' && $self->mapObj->ghostRoom) {

            $self->mapObj->setGhostRoom();      # Automatically redraws the room
        }

        if ($self->mapObj->currentRoom) {

            # Redraw the current room in its correct colour (default pink in 'wait' mode, default
            #   red in 'follow'/'update' mode)
            $self->doDraw('room', $self->mapObj->currentRoom);
        }

        return 1;
    }

    sub setCurrentRegion {

        # Called by $self->treeViewRowActivated and $self->newRegionCallback. Also called by
        #   GA::Obj::Map->setCurrentRoom
        # Sets the new current region, and draws it on the map
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $name       - The name of the region (matches a key in
        #                   GA::Obj::WorldModel->regionmapHash). If set to 'undef', there is no
        #                   current region (and an empty pixmap must be displayed)
        #   $forceFlag  - Set to TRUE when called by GA::Obj::Map->setCurrentRoom. Changes the
        #                   $name region's current level to show the current room, if there is one
        #                   (otherwise, the current level is only changed when the automapper is in
        #                   'follow' or 'update' mode)
        #
        # Return values
        #   'undef' on improper arguments or if a specified region $name doesn't match a known
        #       regionmap
        #   1 otherwise

        my ($self, $name, $forceFlag, $check) = @_;

        # Local variables
        my ($scrollXPos, $scrollYPos, $regionmapObj, $currentRoom);

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setCurrentRegion', @_);
        }

        # If the tooltips are visible, hide them
        $self->hideTooltips();

        if ($self->currentRegionmap) {

            # Remember the current position of the scrollbars so that, the next time this region is
            #   opened, it can be shown in the same position (and at the same magnification)
            ($scrollXPos, $scrollYPos) = $self->getMapPosn();
            $self->currentRegionmap->ivPoke('scrollXPos', $scrollXPos);
            $self->currentRegionmap->ivPoke('scrollYPos', $scrollYPos);
        }

        # If no region was specified...
        if (! defined $name) {

            # Reset the current region
            $self->ivUndef('currentRegionmap');

            # Change the window title back to its default
            $self->setWinTitle();
            # Redraw the drawing area widget, which will be without a map showing. Also redraw the
            #   menu bar and title bar, so that the various checkbuttons/radiobuttons are showing
            #   their neutral positions
            $self->redrawWidgets('menu_bar', 'toolbar', 'canvas');

            # Set the automapper's mode back to 'wait'
            $self->setMode('wait');
            # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
            $self->restrictWidgets();

            return 1;
        }

        # Otherwise, find the regionmap matching the specified region
        $regionmapObj = $self->findRegionmap($name);
        if ($regionmapObj) {

            # Set this regionmap as the current region
            $self->ivPoke('currentRegionmap', $regionmapObj);

            # If there's a current room and it is in this region, and assuming that we are in
            #   follow/update mode (or if the calling function set $forceFlag to TRUE), and if that
            #   room's map level isn't the one that's about to be displayed, change the current
            #   level to the one containing the room
            $currentRoom = $self->mapObj->currentRoom;
            if (
                ($self->mode eq 'follow' || $self->mode eq 'update' || $forceFlag)
                && $currentRoom
                && $currentRoom->parent       # Check that the room is in a region
                && $self->currentRegionmap->number == $currentRoom->parent
                && $self->currentRegionmap->currentLevel != $currentRoom->zPosBlocks
            ) {
                # Set the new current level to the same level as the current room
                $self->setCurrentLevel(
                    $currentRoom->zPosBlocks,
                    TRUE,                       # Don't call ->drawRegion yet
                );
            }

            # Change the window title to display the current region
            $self->setWinTitle();
            # Redraw the menu bar and title bar, so that the various checkbuttons/radiobuttons are
            #   showing settings for the new current region
            $self->redrawWidgets('menu_bar', 'toolbar');

            # Draw the new current region
            $self->drawRegion();
            # Move the scrollbars to their former position
            $self->setMapPosn(
                $self->currentRegionmap->scrollXPos,
                $self->currentRegionmap->scrollYPos,
            );

            # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
            $self->restrictWidgets();

            # Make sure the correct region is highlighted in the treeview region list
            $self->treeViewSelectLine($name);

            return 1;

        } else {

            # Region $name doesn't seem to exist
            return undef;
        }
    }

    sub setCurrentLevel {

        # Can be called by anything
        # Changes the current regionmap's currently level, and redraws the map to show this level
        #
        # Expected arguments
        #   $level          - The new current level, matching GA::Obj::Regionmap->currentLevel
        #
        # Optional arguments
        #   $noDrawFlag     - If set to TRUE, this function doesn't call ->drawRegion, because the
        #                       calling function is going to call it anyway (and we don't want to
        #                       draw the region twice). Otherwise set to FALSE (or 'undef')
        #
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $level, $noDrawFlag, $check) = @_;

        # Check for improper arguments
        if (! defined $level || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setCurrentLevel', @_);
        }

        # Set the new level
        $self->currentRegionmap->ivPoke('currentLevel', $level);

        # Change the window's title
        $self->setWinTitle();

        if (! $noDrawFlag) {

            # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
            $self->restrictWidgets();

            # Redraw the map to show this level
            return $self->drawRegion();

        } else {

            # Assume that the calling function will call ->drawRegion
            return 1;
        }
    }

    sub setSelectedObj {

        # Called by $self->mouseClickEvent or any other function
        # Sets the currently selected object(s) - rooms, room tags, room guilds, exits, exit tags
        #   and labels - or adds the object(s) to the existing hashes of selected objects
        # Redraws the object(s) object along with any objects that are being unselected
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Optional arguments
        #   $listRef        - If set, a reference to a list containing pairs of elements, each
        #                       representing a room, room tag, room guild, exit, exit tag or label
        #                   - The list is in the form (blessed_ref, mode, blessed_ref, mode...)
        #                       where 'mode' is one of the strings 'room', 'exit', 'label',
        #                       'room_tag', 'room_guild' or 'exit_tag'
        #   $multipleFlag   - If set to TRUE, the object(s) were selected while holding down the
        #                       CTRL key, so the object(s) are added to any existing selected
        #                       objects
        #                   - If set to FALSE (or 'undef'), all existing selected objects are
        #                       unselected, before the first object in $listRef is selected (the
        #                       others are ignored)
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $listRef, $multipleFlag, $check) = @_;

        # Local variables
        my (
            $pairedRoomObj, $thisObj, $thisMode, $shadowExitObj,
            @objList, @drawList,
            %selectedRoomHash, %selectedRoomTagHash, %selectedRoomGuildHash, %selectedExitHash,
            %selectedExitTagHash, %selectedLabelHash, %drawHash,
        ) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->setSelectedObj', @_);
        }

        # Selected broken/region exits have a paired twin exit and its parent room, which are drawn
        #   a different colour. Whatever the new selected object(s) is going to be, a paired exit &
        #   room must be reset and marked to be redrawn, if they exist
        if ($self->pairedTwinRoom) {

            # (Redrawing the paired room also redraws its paired exit, if there is one)
            $pairedRoomObj = $self->pairedTwinRoom;
            $self->ivUndef('pairedTwinRoom');
            $self->ivUndef('pairedTwinExit');

            # Mark the room to be redrawn
            $self->markObjs('room', $pairedRoomObj);
        }

        # For quick lookup, import the hashes of selected rooms, room tags, room guilds, exits,
        #   exit tags and labels
        # NB No need to import the scalar IVs ->selectedRoom, ->selectedExit, etc
        %selectedRoomHash = $self->selectedRoomHash;
        %selectedRoomTagHash = $self->selectedRoomTagHash;
        %selectedRoomGuildHash = $self->selectedRoomGuildHash;
        %selectedExitHash = $self->selectedExitHash;
        %selectedExitTagHash = $self->selectedExitTagHash;
        %selectedLabelHash = $self->selectedLabelHash;

        # Get the list referenced by $listRef (if it was specified - if not, use an empty list)
        if ($listRef) {

            @objList = @$listRef;
        }

        # If $multipleFlag isn't set to TRUE, $$listRef[0] is the only object which should now be
        #   drawn as selected
        if (! $multipleFlag) {

            # @objList should contain 0 or 2 elements, representing 0 or 1 selected objects. If
            #   there are more selected objects in the list, ignore all but the first one
            if (@objList > 2) {

                $thisObj = shift @objList;
                $thisMode = shift @objList;

                @objList = ($thisObj, $thisMode);
            }

            # Mark all existing selected objects as no longer selected, and add them to the redraw
            #   list
            if ($self->selectedRoom) {

                push (@drawList, 'room', $self->selectedRoom);
                $self->ivUndef('selectedRoom');
            }

            if (%selectedRoomHash) {

                foreach my $number (keys %selectedRoomHash) {

                    push (@drawList, 'room', $selectedRoomHash{$number});
                }

                %selectedRoomHash = ();
            }

            if ($self->selectedRoomTag) {

                push (@drawList, 'room_tag', $self->selectedRoomTag);
                $self->ivUndef('selectedRoomTag');
            }

            if (%selectedRoomTagHash) {

                foreach my $number (keys %selectedRoomTagHash) {

                    push (@drawList, 'room_tag', $selectedRoomTagHash{$number});
                }

                %selectedRoomTagHash = ();
            }

            if ($self->selectedRoomGuild) {

                push (@drawList, 'room_guild', $self->selectedRoomGuild);
                $self->ivUndef('selectedRoomGuild');
            }

            if (%selectedRoomGuildHash) {

                foreach my $number (keys %selectedRoomGuildHash) {

                    push (@drawList, 'room_guild', $selectedRoomGuildHash{$number});
                }

                %selectedRoomGuildHash = ();
            }

            if ($self->selectedExit) {

                push (@drawList, 'exit', $self->selectedExit);

                # If the selected exit has a shadow exit, the shadow exit will be drawn a different
                #   colour, so it must also be redrawn
                if ($self->selectedExit->shadowExit) {

                    $shadowExitObj = $self->worldModelObj->ivShow(
                        'exitModelHash',
                        $self->selectedExit->shadowExit,
                    );

                    if ($shadowExitObj) {

                        push (@drawList, 'exit', $shadowExitObj);
                    }
               }

                $self->ivUndef('selectedExit');
            }

            if (%selectedExitHash) {

                foreach my $number (keys %selectedExitHash) {

                    push (@drawList, 'exit', $selectedExitHash{$number});
                }

                %selectedExitHash = ();
            }

            if ($self->selectedExitTag) {

                push (@drawList, 'exit_tag', $self->selectedExitTag);
                $self->ivUndef('selectedExitTag');
            }

            if (%selectedExitTagHash) {

                foreach my $number (keys %selectedExitTagHash) {

                    push (@drawList, 'exit_tag', $selectedExitTagHash{$number});
                }

                %selectedExitTagHash = ();
            }

            if ($self->selectedLabel) {

                push (@drawList, 'label', $self->selectedLabel);
                $self->ivUndef('selectedLabel');
            }

            if (%selectedLabelHash) {

                foreach my $number (keys %selectedLabelHash) {

                    push (@drawList, 'label', $selectedLabelHash{$number});
                }

                %selectedLabelHash = ();
            }
        }

        # Now, select each object in turn (if any were specified), and add them to the redraw list
        if (@objList) {

            do {

                my ($obj, $mode, $noSelectedFlag, $twinExitObj, $twinRoomObj);

                $obj = shift @objList;
                $mode = shift @objList;

                # If there is already a single selected object, it must be moved from one IV to
                #   another before a second object is selected
                if ($self->selectedRoom) {

                    $selectedRoomHash{$self->selectedRoom->number} = $self->selectedRoom;
                    $self->ivUndef('selectedRoom');

                } elsif ($self->selectedRoomTag) {

                    $selectedRoomTagHash{$self->selectedRoomTag->number} = $self->selectedRoomTag;
                    $self->ivUndef('selectedRoomTag');

                } elsif ($self->selectedRoomGuild) {

                    $selectedRoomGuildHash{$self->selectedRoomGuild->number}
                        = $self->selectedRoomGuild;
                    $self->ivUndef('selectedRoomGuild');

                } elsif ($self->selectedExit) {

                    $selectedExitHash{$self->selectedExit->number} = $self->selectedExit;
                    $self->ivUndef('selectedExit');

                } elsif ($self->selectedExitTag) {

                    $selectedExitTagHash{$self->selectedExitTag->number} = $self->selectedExitTag;
                    $self->ivUndef('selectedExitTag');

                } elsif ($self->selectedLabel) {

                    $selectedLabelHash{$self->selectedLabel->number} = $self->selectedLabel;
                    $self->ivUndef('selectedLabel');

                } elsif (
                    ! %selectedRoomHash
                    && ! %selectedRoomTagHash
                    && ! %selectedRoomGuildHash
                    && ! %selectedExitHash
                    && ! %selectedExitTagHash
                    && ! %selectedLabelHash
                ) {
                    # There are currently no selected objects at all
                    $noSelectedFlag = TRUE;
                }

                # Rooms
                if ($mode eq 'room') {

                    push (@drawList, 'room', $obj);

                    if (! $noSelectedFlag) {
                        $selectedRoomHash{$obj->number} = $obj;
                    } else {
                        $self->ivPoke('selectedRoom', $obj);
                    }

                # Room tags
                } elsif ($mode eq 'room_tag') {

                    push (@drawList, 'room_tag', $obj);

                    if (! $noSelectedFlag) {
                        $selectedRoomTagHash{$obj->number} = $obj;
                    } else {
                        $self->ivPoke('selectedRoomTag', $obj);
                    }

                # Room guilds
                } elsif ($mode eq 'room_guild') {

                    push (@drawList, 'room_guild', $obj);

                    if (! $noSelectedFlag) {
                        $selectedRoomGuildHash{$obj->number} = $obj;
                    } else {
                        $self->ivPoke('selectedRoomGuild', $obj);
                    }

                # Exits
                } elsif ($mode eq 'exit') {

                    push (@drawList, 'exit', $obj);

                    if (! $noSelectedFlag) {

                        $selectedExitHash{$obj->number} = $obj;

                    } else {

                        $self->ivPoke('selectedExit', $obj);

                        # If the selected exit is a broken or region exit, we need to mark the
                        #   destination room (and the twin exit, if there is one) to be drawn a
                        #   different colour
                        # (NB Doesn't apply to bent broken exits)
                        if (
                            ($obj->brokenFlag && ! $obj->bentFlag)
                            || $obj->regionFlag
                        ) {
                            $twinRoomObj
                                = $self->worldModelObj->ivShow('modelHash', $obj->destRoom);
                            if ($twinRoomObj) {

                                $self->ivPoke('pairedTwinRoom', $twinRoomObj);
                            }

                            if ($obj->twinExit) {

                                # Since the twin exit exists, it gets painted the same colour
                                $twinExitObj = $self->worldModelObj->ivShow(
                                        'exitModelHash',
                                        $obj->twinExit,
                                    );

                                if ($twinExitObj) {

                                    $self->ivPoke('pairedTwinExit', $twinExitObj);
                                }

                            } else {

                                # Make sure the IV has been reset
                                $self->ivUndef('pairedTwinExit');
                            }

                            # Redrawing the room redraws its exit
                            push (@drawList, 'room', $twinRoomObj);
                       }
                    }

                # Exit tags
                } elsif ($mode eq 'exit_tag') {

                    push (@drawList, 'exit_tag', $obj);

                    if (! $noSelectedFlag) {
                        $selectedExitTagHash{$obj->number} = $obj;
                    } else {
                        $self->ivPoke('selectedExitTag', $obj);
                    }

                # Labels
                } elsif ($mode eq 'label') {

                    push (@drawList, 'label', $obj);

                    if (! $noSelectedFlag) {
                        $selectedLabelHash{$obj->number} = $obj;
                    } else {
                        $self->ivPoke('selectedLabel', $obj);
                    }
                }

            } until (! @objList);
        }

        # Store the selected object hashes (any or all of which may be empty)
        $self->ivPoke('selectedRoomHash', %selectedRoomHash);
        $self->ivPoke('selectedRoomTagHash', %selectedRoomTagHash);
        $self->ivPoke('selectedRoomGuildHash', %selectedRoomGuildHash);
        $self->ivPoke('selectedExitHash', %selectedExitHash);
        $self->ivPoke('selectedExitTagHash', %selectedExitTagHash);
        $self->ivPoke('selectedLabelHash', %selectedLabelHash);

        if (@drawList) {

            # Redraw all objects that must be redrawn ($self->doDraw will eliminate any duplicates
            #   in @drawList)
            $self->doDraw(@drawList);
        }

        # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
        $self->restrictWidgets();

        return 1;
    }

    sub unselectObj {

        # Called by $self->canvasEventHandler, ->canvasObjEventHandler and $self->deleteCanvasObj
        # When an object is to be unselected, we need to be careful about updating the IVs. This
        #   function is called to see whether a room, room tag, room guild, exit, exit tag or label
        #   is selected and, if so, unselects it
        #
        # Expected arguments
        #   $obj    - The object to be unselected. For rooms, exits and labels, a blessed reference
        #               of a GA::ModelObj::Room, GA::Obj::Exit or GA::Obj::MapLabel. For room tags
        #               and room guilds, the blessed reference of the GA::ModelObj::Room to which
        #               they belong. For exit tags, the blessed reference of the GA::Obj::Exit to
        #               which they belong
        #
        # Optional arguments
        #   $mode   - Set to 'room_tag', 'room_guild', 'exit_tag' or 'undef' for everything else
        #               (i.e. rooms, exits and labels; so that we know that when $obj is a room
        #               object, whether it's the room itself, the room's room tag or its room guild
        #               which is to be selected; or so that we know whether to select an exit or an
        #               exit tag)
        #   $noRedrawFlag
        #           - Set to TRUE if the object shouldn't be redrawn (because it is about to be
        #               deleted)
        #
        # Return values
        #   'undef' on improper arguments or if the object isn't already selected
        #   1 otherwise

        my ($self, $obj, $mode, $noRedrawFlag, $check) = @_;

        # Local variables
        my (
            $singleObj, $singleType, $pairedRoomObj,
            @drawList, @importList,
        );

        # Check for improper arguments
        if (! defined $obj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->unselectObj', @_);
        }

        # Selected broken/region exits have a paired twin exit which is drawn a different colour,
        #   as is its parent room. Whatever objects are going to be unselected, a paired exit & room
        #   must be reset and redrawn, if they exist
        if ($self->pairedTwinExit) {

            # (Redrawing the paired room also redraws its paired exit, if there is one)
            $pairedRoomObj = $self->pairedTwinRoom;
            $self->ivUndef('pairedTwinRoom');
            $self->ivUndef('pairedTwinExit');

            # Mark the room to be redrawn
            $self->markObjs('room', $pairedRoomObj);
        }

        # Unselect the object, and add it to the redraw list

        if ($obj->_objClass eq 'Games::Axmud::ModelObj::Room') {

            # Rooms
            if (! $mode) {

                push (@drawList, 'room', $obj);

                if ($self->selectedRoom && $self->selectedRoom eq $obj) {

                    $self->ivUndef('selectedRoom');

                } elsif ($self->ivExists('selectedRoomHash', $obj->number)) {

                    $self->ivDelete('selectedRoomHash', $obj->number);

                } else {

                    # This room wasn't already selected
                    return undef;
                }

            # Room tags
            } elsif ($mode eq 'room_tag') {

                push (@drawList, 'room_tag', $obj);

                if ($self->selectedRoomTag && $self->selectedRoomTag eq $obj) {

                    $self->ivUndef('selectedRoomTag');

                } elsif ($self->ivExists('selectedRoomTagHash', $obj->number)) {

                    $self->ivDelete('selectedRoomTagHash', $obj->number);

                } else {

                    # This room tag wasn't already selected
                    return undef;
                }

            # Room guilds
            } elsif ($mode eq 'room_guild') {

                push (@drawList, 'room_guild', $obj);

                if ($self->selectedRoomGuild && $self->selectedRoomGuild eq $obj) {

                    $self->ivUndef('selectedRoomGuild');

                } elsif ($self->ivExists('selectedRoomGuildHash', $obj->number)) {

                    $self->ivDelete('selectedRoomGuildHash', $obj->number);

                } else {

                    # This room guild wasn't already selected
                    return undef;
                }
            }

        } elsif ($obj->_objClass eq 'Games::Axmud::Obj::Exit') {

            # Exits
            if (! $mode) {

                push (@drawList, 'exit', $obj);

                if ($self->selectedExit && $self->selectedExit eq $obj) {

                    $self->ivUndef('selectedExit');

                } elsif ($self->ivExists('selectedExitHash', $obj->number)) {

                    $self->ivDelete('selectedExitHash', $obj->number);

                } else {

                    # This exit wasn't already selected
                    return undef;
                }

            # Exit tags
            } elsif ($mode eq 'exit_tag') {

                push (@drawList, 'exit_tag', $obj);

                if ($self->selectedExitTag && $self->selectedExitTag eq $obj) {

                    $self->ivUndef('selectedExitTag');

                } elsif ($self->ivExists('selectedExitTagHash', $obj->number)) {

                    $self->ivDelete('selectedExitTagHash', $obj->number);

                } else {

                    # This exit tag wasn't already selected
                    return undef;
                }
            }

        # Labels
        } elsif ($obj->_objClass eq 'Games::Axmud::Obj::MapLabel') {

            push (@drawList, 'label', $obj);

            if ($self->selectedLabel && $self->selectedLabel eq $obj) {

                $self->ivUndef('selectedLabel');

            } elsif ($self->ivExists('selectedLabelHash', $obj->number)) {

                $self->ivDelete('selectedLabelHash', $obj->number);

            } else {

                # This label wasn't already selected
                return undef;
            }
        }

        # Redraw the item removed (unless it's about to be deleted)
        if (! $noRedrawFlag) {

            $self->doDraw(@drawList);
        }

        # Now, we check these five hashes. If, between them, they contain a single selected object,
        #   then we need to move it out of the hashes and into the IVs $self->selectedRoom,
        #   ->selectedExit (etc)
        if ($self->ivPairs('selectedRoomHash') == 1) {

            @importList = $self->selectedRoomHash;
            $singleObj = $importList[1];
            $singleType = 'room';
        }

        if ($self->ivPairs('selectedRoomTagHash') == 1) {

            if ($singleObj) {

                # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
                $self->restrictWidgets();
                # More than one selected object. Give up
                return 1;

            } else {

                @importList = $self->selectedRoomTagHash;
                $singleObj = $importList[1];
                $singleType = 'room_tag';
            }
        }

        if ($self->ivPairs('selectedRoomGuildHash') == 1) {

            if ($singleObj) {

                # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
                $self->restrictWidgets();
                # More than one selected object. Give up
                return 1;

            } else {

                @importList = $self->selectedRoomGuildHash;
                $singleObj = $importList[1];
                $singleType = 'room_guild';
            }
        }

        if ($self->ivPairs('selectedExitHash') == 1) {

            if ($singleObj) {

                # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
                $self->restrictWidgets();
                # More than one selected object. Give up
                return 1;

            } else {

                @importList = $self->selectedExitHash;
                $singleObj = $importList[1];
                $singleType = 'exit';
            }
        }

        if ($self->ivPairs('selectedExitTagHash') == 1) {

            if ($singleObj) {

                # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
                $self->restrictWidgets();
                # More than one selected object. Give up
                return 1;

            } else {

                @importList = $self->selectedExitTagHash;
                $singleObj = $importList[1];
                $singleType = 'exit_tag';
            }
        }

        if ($self->ivPairs('selectedLabelHash') == 1) {

            if ($singleObj) {

                # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
                $self->restrictWidgets();
                # More than one selected object. Give up
                return 1;

            } else {

                @importList = $self->selectedLabelHash;
                $singleObj = $importList[1];
                $singleType = 'label';
            }
        }

        if ($singleObj) {

            # There is exactly one selected object left in the six hashes. Remove it from its hash,
            #   and set the single-object IV
            if ($singleType eq 'room') {

                $self->ivEmpty('selectedRoomHash');
                $self->ivPoke('selectedRoom', $singleObj);

            } elsif ($singleType eq 'room_tag') {

                $self->ivEmpty('selectedRoomTagHash');
                $self->ivPoke('selectedRoomTag', $singleObj);

            } elsif ($singleType eq 'room_guild') {

                $self->ivEmpty('selectedRoomGuildHash');
                $self->ivPoke('selectedRoomGuild', $singleObj);

            } elsif ($singleType eq 'exit') {

                $self->ivEmpty('selectedExitHash');
                $self->ivPoke('selectedExit', $singleObj);

            } elsif ($singleType eq 'exit_tag') {

                $self->ivEmpty('selectedExitTagHash');
                $self->ivPoke('selectedExitTag', $singleObj);

            } elsif ($singleType eq 'label') {

                $self->ivEmpty('selectedLabelHash');
                $self->ivPoke('selectedLabel', $singleObj);
            }
        }

        # Sensitise/desensitise menu bar/toolbar items, depending on current conditions
        $self->restrictWidgets();

        return 1;
    }

    # Canvas object functions

    sub createNewRoom {

        # Called by $self->addFirstRoomCallback, ->addRoomAtBlockCallback, ->moveKnownDirSeen,
        #   ->enableCanvasPopupMenu and ->canvasEventHandler
        # Creates a new room - adding a new object to the world model, and drawing the room on the
        #   map at the specified location
        # If the process fails, deletes the world model object (if already created) and displays an
        #   error
        #
        # Expected arguments
        #   $regionmapObj   - The GA::Obj::Regionmap in which the new room will be created. Usually
        #                       (but not always) matches $self->currentRegionmap
        #   $xPosBlocks, $yPosBlocks, $zPosBlocks
        #                   - The coordinates of the new room on the regionmap's grid
        #
        # Optional arguments
        #   $mode           - The new $self->mode after the room has been created. If 'undef',
        #                       $self->mode isn't changed
        #   $currentFlag    - If set to TRUE, the new room is made the current location (if FALSE
        #                       or 'undef', the current location isn't changed)
        #   $updateFlag     - If set to TRUE, the room's properties are updated to match those of
        #                       the Locator task's current room (depending on certain flags, and
        #                       only when $currentFlag is TRUE; otherwise set to FALSE or 'undef')
        #   $connectRoomObj - The room from which the character just departed ('undef' if not known)
        #   $dir            - The command used to move (e.g. 'n', 'enter well' - matches
        #                       GA::Obj::Exit->dir) ('undef' if unknown)
        #   $mapDir         - How the exit is drawn on the map - one of Axmud's standard primary
        #                       directions (e.g. 'north', 'south', 'up) ('undef' if unknown)
        #   $exitObj        - The GA::Obj::Exit through which the character moved (if 'undef', a new
        #                       exit object can be created)
        #
        # Return values
        #   'undef' on improper arguments or if the new room isn't created
        #   Otherwise returns the GA::ModelObj::Room created

        my (
            $self, $regionmapObj, $xPosBlocks, $yPosBlocks, $zPosBlocks, $mode, $currentFlag,
            $updateFlag, $connectRoomObj, $dir, $mapDir, $exitObj, $check,
        ) = @_;

        # Local variables
        my (
            $text, $result, $roomObj, $filePath, $virtualPath,
            @matchList, @selectList,
        );

        # Check for improper arguments
        if (
            ! defined $regionmapObj || ! defined $xPosBlocks || ! defined $yPosBlocks
            || ! defined $zPosBlocks || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->createNewRoom', @_);
        }

        if ($updateFlag && $self->worldModelObj->autoCompareFlag) {

            # Before creating the new room, compare the Locator's current room against all rooms
            #   already in the region, looking for matches - in case we're about to create a
            #   duplicate room
            @matchList = $self->autoCompareLocatorRoom($regionmapObj);
            if (@matchList) {

                # Select all the matching rooms. $self->setSelectedObj expects a list in the form
                #   (room_object, 'room', room_object, 'room', ...)
                foreach my $obj (@matchList) {

                    push (@selectList, $obj, 'room');
                }

                # First unselect all selected objects, then selecting just the matching rooms
                $self->setSelectedObj();
                $self->setSelectedObj(
                    \@selectList,
                    TRUE,           # Select multiple objects
                );

                # Show a 'dialogue' window as a warning
                if (@matchList == 1) {
                    $text = 'The Locator\'s current room matches 1 room';
                } else {
                    $text = 'The Locator\'s current room matches ' . scalar @matchList . ' rooms';
                }

                $text .= ' in the current region. Do you want to continue?';

                $result = $self->showMsgDialogue(
                    'Add new room',
                    'warning',
                    $text,
                    'yes-no',
                );

                if ($result ne 'yes') {

                    # If there is a current room, mark the automapper as lost
                    if ($self->mapObj->currentRoom) {

                        return $self->setCurrentRoom(
                            undef,
                            $self->_objClass . '->createNewRoom',    # Character now lost
                        );

                        # Display an explanatory message, if necessary
                        if ($self->worldModelObj && $self->worldModelObj->explainGetLostFlag) {

                            $self->session->writeText(
                                'MAP: Lost after user declined to create a new room',
                            );
                        }
                    }

                    # Don't create the new room
                    return undef;
                }
            }
        }

        # Create the room object. Tell the world model not to update automappers immediately - we
        #   might want to mark the room as current
        $roomObj = $self->worldModelObj->addRoom(
            $self->session,
            TRUE,               # Update Automapper windows now
            $regionmapObj->name,
            $xPosBlocks,
            $yPosBlocks,
            $zPosBlocks,
        );

        if (! $roomObj) {

            # Operation failed
            $self->showMsgDialogue(
                'New room',
                'error',
                'Could not create the new room',
                'ok',
            );

            return undef;
        }

        # Set the automapper's mode, if a new mode was specified
        if ($mode) {

            $self->setMode($mode);
        }

        # Set the current location (which causes both the previous current location, if there was
        #   one, and the new current location to be redrawn)
        if ($currentFlag) {

            # Paint the room, if required to do so
            if ($self->painterFlag) {

                $self->paintRoom($roomObj);
            }

            # Update the number of character visits to this room, if allowed
            $self->worldModelObj->updateVisitCount(
                $self->session,
                # Don't update Automapper windows now (the call to ->updateRoom will do that)
                FALSE,
                $roomObj->number,
            );

            # If necessary, give the new room the properties of the Locator task's current room
            if ($updateFlag) {

                $self->mapObj->updateRoom($roomObj, $connectRoomObj, $exitObj, $mapDir);
            }

            # If the previous room is known, connect it to this room
            if ($connectRoomObj && $dir) {

                $self->worldModelObj->connectRooms(
                    $self->session,
                    TRUE,       # Update Automapper windows now (for the benefit of $connectRoomObj)
                    $connectRoomObj,
                    $roomObj,
                    $dir,
                    $mapDir,
                    $exitObj,
                );
            }

            # Set the current location
            $self->mapObj->setCurrentRoom($roomObj);

        } else {

            # Just draw the new room (in every Automapper window)
            $self->worldModelObj->updateMaps('room', $roomObj);
        }

        return $roomObj;
    }

    sub connectExitToRoom {

        # Called by $self->canvasObjEventHandler and ->stopDrag
        # After the user chooses the 'connect to click' menu item and clicks on a room, connects the
        #   selected exit to the clicked room, marking it as a broken or region exit if necessary
        # Alternatively, after the user drags an exit over a room, connects the exit to the room
        # In both cases, also prompts the user to ask if they'd like to create an exit in the
        #   reverse direction
        #
        # Expected arguments
        #   $roomObj    - The clicked room (in a different region to the current one)
        #   $type       - What type of exit to create - set to 'broken' or 'region'. If set to
        #                   'broken', the function checks that it really is a broken exit (and
        #                   cancels the flag, if not)
        #
        # Optional arguments
        #   $exitObj    - When called by $self->stopDrag, the dragged exit to connect
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $roomObj, $type, $exitObj, $check) = @_;

        # Local variables
        my $destRoomObj;

        # Check for improper arguments
        if (
            ! defined $roomObj || ! defined $type || ($type ne 'broken' && $type ne 'region')
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->connectExitToRoom', @_);
        }

        # If an exit wasn't specified, use the selected exit
        if (! $exitObj) {

            $exitObj = $self->selectedExit;
        }

        # Special case: if the exit is a 'retracing' exit - it leads back to the same room - then
        #   it's obviously not a region or broken exit
        $destRoomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->parent);
        if ($destRoomObj && $destRoomObj eq $roomObj) {

            # The callback can take care of everything
            return $self->markRetracingExitCallback();
        }

        # Otherwise, connect the selected exit to the clicked room and instruct the world model to
        #   update its Automapper windows
        $self->worldModelObj->connectRegionBrokenExit(
            $self->session,
            TRUE,       # Update Automapper windows now
            $roomObj,
            $exitObj,
            $type,
        );

        # If the selected exit is an unallocated/unallocatable exit, or if it's an up/down exit (and
        #   assuming that it's not a region exit), nothing will appear to have happened. In this
        #   case, display a short confirmation message
        if (
            # If it's a region exit, the region will have changed
            ! $exitObj->regionFlag
            && (
                $exitObj->drawMode eq 'temp_alloc'
                || $exitObj->drawMode eq 'temp_unalloc'
                || $exitObj->mapDir eq 'up'
                || $exitObj->mapDir eq 'down'
            )
        ) {
            $self->showMsgDialogue(
                'Connect to click',
                'info',
                'Exit #' . $exitObj->number . ' now leads to room #' . $roomObj->number,
                'ok',
            );
        }

        return 1;
    }

    # Room movement functions

    sub moveRoomsInDir {

        # Called by $self->moveSelectedRoomsCallback
        # Moves the selected rooms (together with their room tags, room guilds, exits and exit tags)
        #   and any selected labels in a specified direction
        # NB This function won't move labels on their own - selected labels are only moved alongside
        #   selected rooms
        #
        # Expected arguments
        #   $distance   - The distance to move (in gridblocks)
        #   $dir        - The direction in which to move (a standard primary direction)
        #
        # Return values
        #   'undef' on improper arguments, if there is no current regionmap, if there are no
        #       selected rooms, if the selected rooms (and labels) aren't all in the same regionmap,
        #       or if there is a general error
        #   1 otherwise

        my ($self, $distance, $dir, $check) = @_;

        # Local variables
        my ($adjustXPos, $adjustYPos, $adjustZPos);

        # Check for improper arguments
        if (! defined $distance || ! defined $dir || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->moveRoomsInDir', @_);
        }

        # Don't do anything if there isn't a current regionmap, or if there are no selected rooms
        if (
            ! $self->currentRegionmap
            || (! $self->selectedRoom && ! $self->selectedRoomHash)
        ) {
            return undef;
        }

        # Check that the selected rooms and labels are all in the same region. We ignore selected
        #   room tags, room guilds, exits and exit tags for the moment
        if (! $self->checkSelectedInSameRegion()) {

            # Show a 'dialogue' window to explain the problem
            $self->showMsgDialogue(
                'Move selected rooms',
                'error',
                'The selected rooms (and labels) can\'t be moved because they are not all in the'
                . ' same region',
                'ok',
            );

            return undef;
        }

        # Work out by how many blocks along each axis the rooms' coordinates need to be changed
        ($adjustXPos, $adjustYPos, $adjustZPos) = $self->interpretDir($distance, $dir);
        if (! defined $adjustXPos) {

            $self->showMsgDialogue(
                'Move selected rooms',
                'error',
                'General error moving selected room(s) and label(s)',
                'ok',
            );

            return undef;
        }

        # Now check every selected room at the equivalent coordinates, checking that its proposed
        #   position isn't outside the boundaries of the map and isn't already occupied by another
        #   room
        # Also check the proposed position of any selected labels, which must not be outside the
        #   boundaries of the map
        if (
            ! $self->checkNewCoordinates($adjustXPos, $adjustYPos, $adjustZPos)
            || ! $self->checkNewLabelPositions($adjustXPos, $adjustYPos, $adjustZPos)
        ) {
            # Open a 'dialogue' window to explain the problem
            $self->showMsgDialogue(
                'Move selected rooms',
                'error',
                'The selected rooms (and labels) can\'t be moved to this position, either because'
                . ' the space is already occupied, or because at least one of them falls outside'
                . ' the boundaries of the map',
                'ok',
            );

            return undef;
        }

        # Move all of the selected rooms/labels to their new position
        if (! $self->moveSelectedObjs($adjustXPos, $adjustYPos, $adjustZPos)) {

            # This is a serious error, since half the rooms may be copied, and half not, so show an
            #   error message
            return $self->session->writeError(
                'General error moving selected room(s) and label(s)',
                $self->_objClass . '->moveRoomsInDir',
            );

        } else {

            # Operation complete
            return 1;
        }
    }

    sub moveRoomsToClick {

        # Called by $self->canvasEventHandler
        # Immediately after the user clicks on an empty region of the map, moves the selected rooms
        #   (together with their room tags, room guilds, exits and exit tags) and any selected
        #   labels to the clicked area of the map
        # NB This function won't move labels on their own - selected labels are only moved alongside
        #   selected rooms
        #
        # Expected arguments
        #   $clickXPosBlocks, $clickYPosBlocks
        #       - The grid coordinates on the map of the user's mouse click
        #
        # Return values
        #   'undef' on improper arguments, if there is no current regionmap, if there are no
        #       selected rooms, if the selected rooms (and labels) aren't all in the same regionmap,
        #       or if there is a general error
        #   1 otherwise

        my ($self, $clickXPosBlocks, $clickYPosBlocks, $check) = @_;

        # Local variables
        my (
            $left, $right, $top, $bottom, $up, $down, $roomObj, $newLeft, $newRight, $newTop,
            $newBottom, $newUp, $newDown, $adjustXPos, $adjustYPos, $adjustZPos,
        );

        # Check for improper arguments
        if (! defined $clickXPosBlocks || ! defined $clickYPosBlocks || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->moveRoomsToClick', @_);
        }

        # Don't do anything if there isn't a current regionmap, or if there are no selected rooms
        if (
            ! $self->currentRegionmap
            || (! $self->selectedRoom && ! $self->selectedRoomHash)
        ) {
            return undef;
        }

        # Check that the selected rooms and labels are all in the same region. We ignore selected
        #   exits and room tags for the moment
        if (! $self->checkSelectedInSameRegion()) {

            # Open a 'dialogue' window to explain the problem
            $self->showMsgDialogue(
                'Move selected rooms',
                'error',
                'The selected rooms (and labels) can\'t be moved because they are not all in the'
                . ' same region',
                'ok',
            );

            return undef;
        }

        # Find the boundaries of the area (in three dimensions) covered by the selected rooms
        #   (ignoring selected exits, room tags and labels for the moment)
        ($left, $right, $top, $bottom, $up, $down) = $self->findSelectedRoomBoundaries();
        if (! defined $left) {

            # Error in called function
            return undef;
        }

        # Check that the size of the area isn't physically bigger than the size of the regionmap in
        #   which the user has clicked
        if (
            $self->currentRegionmap->gridWidthBlocks < ($right - $left + 1)
            || $self->currentRegionmap->gridHeightBlocks < ($bottom - $top + 1)
        ) {
            # Open a 'dialogue' window to explain the problem
            $self->showMsgDialogue(
                'Move selected rooms',
                'error',
                'The selected rooms (and labels) occupy an area which is too big for the current'
                . ' regionmap',
                'ok',
            );

            return undef;
        }

        # Decide which room among the selected rooms is going to be the 'centre' - the room which is
        #   moved to the point on the regionmap, on which the user clicked
        $roomObj = $self->findMiddleSelectedRoom($left, $right, $top, $bottom, $up, $down);
        if (! $roomObj) {

            # Error in called function
            return undef;
        }

        # Work out the boundaries of the area covered by the selected rooms once they are moved, if
        #   we assume that the 'centre' room is moved to the position of the user's click
        (
            $newLeft, $newRight,
            $newTop, $newBottom,
            $newUp, $newDown
        ) = $self->refineSelectedArea(
            $left, $right,
            $top, $bottom,
            $up, $down,
            $clickXPosBlocks, $clickYPosBlocks,
            $roomObj,
        );

        if (! defined $newLeft) {

            # Error in called function
            return undef;
        }

        # Work out how many blocks along each axis the room's coordinates need to be changed by
        $adjustXPos = ($newLeft - $left);
        $adjustYPos = ($newTop - $top);
        $adjustZPos = ($newDown - $down);

        # Check each selected room at its proposed new position, to make sure that it won't occupy a
        #   position already occupied by another room
        # Also check the proposed position of any selected labels, which must not be outside the
        #   boundaries of the map
        if (
            ! $self->checkSelectedArea($adjustXPos, $adjustYPos, $adjustZPos)
            || ! $self->checkNewLabelPositions($adjustXPos, $adjustYPos, $adjustZPos)
        ) {
            # Open a 'dialogue' window to explain the problem
            $self->showMsgDialogue(
                'Move selected rooms',
                'error',
                'The selected rooms (and labels) can\'t be copied into an area already occupied'
                . ' by other rooms',
                'ok',
            );

            return undef;
        }

        # Move all of the selected rooms/labels to their new position
        if (! $self->moveSelectedObjs($adjustXPos, $adjustYPos, $adjustZPos)) {

            # This is a serious error, since half the rooms may be copied, and half not, so show an
            #   error message
            return $self->session->writeError(
                'General error moving selected room(s) and label(s)',
                $self->_objClass . '->moveRoomsToClick',
            );

        } else {

            # Operation complete
            return 1;
        }
    }

    sub moveRoomsToExit {

        # Called by $self->canvasObjEventHandler
        # Immediately after the user clicks on an existing room - and assuming the clicked room has
        #   an exit which leads to one of the selected rooms - moves all of the selected rooms
        #   (together with their room tags, room guilds, exits and exit tags) and any selected
        #   labels to join up with the clicked room
        # NB This function won't move labels on their own - selected labels are only moved alongside
        #   selected rooms
        #
        # Expected arguments
        #   $roomObj    - The clicked room
        #
        # Return values
        #   'undef' on improper arguments, if there is no current regionmap, if there are no
        #       selected rooms, if the selected rooms (and labels) aren't all in the same regionmap,
        #       if none of the clicked room's exits lead to one of the selected rooms, or if there
        #       is a general error
        #   1 otherwise

        my ($self, $roomObj, $check) = @_;

        # Local variables
        my ($exitObj, $destRoomObj, $xPos, $yPos, $zPos, $adjustXPos, $adjustYPos, $adjustZPos);

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->moveRoomsToExit', @_);
        }

        # Don't do anything if there isn't a current regionmap, or if there are no selected rooms
        if (
            ! $self->currentRegionmap
            || (! $self->selectedRoom && ! $self->selectedRoomHash)
        ) {
            return undef;
        }

        # Check that the selected rooms and labels are all in the same region. We ignore selected
        #   exits and room tags for the moment
        if (! $self->checkSelectedInSameRegion()) {

            # Open a 'dialogue' window to explain the problem
            $self->showMsgDialogue(
                'Move selected rooms',
                'error',
                'The selected rooms (and labels) can\'t be moved because they are not all in the'
                . ' same region',
                'ok',
            );

            return undef;
        }

        # Check that the clicked room has an exit leading to at least one of the selected rooms
        $exitObj = $self->checkSelectedIsConnected($roomObj);
        if (! $exitObj) {

            # Open a 'dialogue' window to explain the problem
            $self->showMsgDialogue(
                'Move selected rooms',
                'error',
                'The room on which you clicked doesn\'t have an exit leading to any of the'
                . ' selected rooms - try clicking on an empty region of the map, instead',
                'ok',
            );

            return undef;
        }

        # Get the destination room for $exitObj
        $destRoomObj = $self->worldModelObj->ivShow('modelHash', $exitObj->destRoom);

        # Work out the destination room's new grid coordinates after the move (e.g. if the exit
        #   leads 'east', then its x-coord will increase by 1, but its y-coord and z-coord will stay
        #   the same)
        ($xPos, $yPos, $zPos) = $self->findNewCoordinates($roomObj, $exitObj);
        if (! defined $xPos) {

            $self->showMsgDialogue(
                'Move selected rooms',
                'error',
                'General error moving selected room(s) and label(s)',
                'ok',
            );

            return undef;
        }

        # Work out by how many blocks along each axis the destination room's coordinates (and, by
        #   extension, the coordinates of every selected room) need to be changed
        $adjustXPos = ($xPos - $destRoomObj->xPosBlocks);
        $adjustYPos = ($yPos - $destRoomObj->yPosBlocks);
        $adjustZPos = ($zPos - $destRoomObj->zPosBlocks);

        # Now check every selected room at the equivalent coordinates, making sure that its proposed
        #   position isn't outside the boundaries of the map and isn't already occupied by another
        #   room
        # Also check the proposed position of any selected labels, which must not be outside the
        #   boundaries of the map
        if (
            ! $self->checkNewCoordinates($adjustXPos, $adjustYPos, $adjustZPos)
            || ! $self->checkNewLabelPositions($adjustXPos, $adjustYPos, $adjustZPos)
        ) {
            # Open a 'dialogue' window to explain the problem
            $self->showMsgDialogue(
                'Move selected rooms',
                'error',
                'The selected rooms (and labels) can\'t be moved to this position, either because'
                . ' the space is already occupied, or because at least one of them falls outside'
                . ' the boundaries of the map',
                'ok',
            );

            return undef;
        }

        # Move all of the selected rooms/labels to their new position
        if (! $self->moveSelectedObjs($adjustXPos, $adjustYPos, $adjustZPos)) {

            # This is a serious error, since half the rooms may be copied, and half not, so show an
            #   error message
            return $self->session->writeError(
                'General error moving selected room(s) and label(s)',
                $self->_objClass . '->moveRoomsToExit',
            );

        } else {

            # Operation complete
            return 1;
        }
    }

    # Room movement support functions

    sub moveSelectedObjs {

        # Called by $self->moveRoomsInDir, ->moveRoomsToClick, ->moveRoomsToExit and ->stopDrag
        # Moves all selected rooms (along with their room tags, room guilds, exits and exit tags)
        #   and all selected labels to their new position
        #
        # Expected arguments
        #   $adjustXPos, $adjustYPos, $adjustZPos
        #       - Describes a vector between the coordinates of the selected rooms in their old
        #           position, and their coordinates in the new position
        #
        # Return values
        #   'undef' on improper arguments or if the move fails
        #   1 otherwise

        my ($self, $adjustXPos, $adjustYPos, $adjustZPos, $check) = @_;

        # Local variables
        my (
            $roomObj, $oldRegionObj, $oldRegionmapObj,
            @list,
            %roomHash, %labelHash
        );

        # Check for improper arguments
        if (
            ! defined $adjustXPos || ! defined $adjustYPos || ! defined $adjustZPos
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->moveSelectedObjs', @_);
        }

        # Import a list of all the selected rooms, and copy them into a hash
        if ($self->selectedRoom) {
            $roomHash{$self->selectedRoom->number} = $self->selectedRoom;
        } else {
            %roomHash = $self->selectedRoomHash;
        }

        # Also get a list of selected labels, and copy them into a hash
        if ($self->selectedLabel) {
            $labelHash{$self->selectedLabel->number} = $self->selectedLabel;
        } else {
            %labelHash = $self->selectedLabelHash;
        }

        # Using the first room in %roomHash, get the regionmap from which the rooms are moving (may
        #   be the same as the regionmap to which they are moving, $self->currentRegionmap)
        @list = values %roomHash;
        $roomObj = $list[0];
        $oldRegionObj = $self->worldModelObj->ivShow('modelHash', $roomObj->parent);
        $oldRegionmapObj = $self->worldModelObj->ivShow('regionmapHash', $oldRegionObj->name);

        # Move the selected rooms and labels
        return $self->worldModelObj->moveRoomsLabels(
            $self->session,
            TRUE,                       # Update Automapper windows now
            $oldRegionmapObj,           # Move from this region...
            $self->currentRegionmap,    # ...to this one...
            $adjustXPos,                # ...using this vector
            $adjustYPos,
            $adjustZPos,
            \%roomHash,
            \%labelHash,
        );
    }

    sub checkSelectedInSameRegion {

        # Called by $self->moveRoomsInDir, ->moveRoomsToClick and ->moveRoomsToExit
        # Checks that the selected room(s) and label(s) are in the same regionmap (ignores selected
        #   room tags, room guilds, exits and exit tags)
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if there is an error extracting regions, or if the
        #       selected room(s) and label(s) aren't all in the same region
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my $firstRegionObj;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->checkSelectedInSameRegion',
                @_,
            );
        }

        # If there is just a single selected room or a single selected label, then there is no need
        #   to check whether things are in different regions
        if ($self->selectedRoom || $self->selectedLabel) {

            return 1;
        }

        # Go through every selected room, returning 'undef' for failure if we find any selected
        #   rooms which aren't in the same regionmap as the first one
        foreach my $roomObj ($self->ivValues('selectedRoomHash')) {

            my $regionObj = $self->worldModelObj->ivShow('modelHash', $roomObj->parent);

            if (! $firstRegionObj) {

                # This is the first selected room. Records its region
                $firstRegionObj = $regionObj;

            } elsif ($regionObj ne $firstRegionObj) {

                # The selected rooms are in more than one regionmap
                return undef;
            }
        }

        # Do the same for every selected label
        foreach my $labelObj ($self->ivValues('selectedLabelHash')) {

            my $regionNum = $self->findRegion($labelObj->region);

            if ($regionNum ne $firstRegionObj->number) {

                # This label is in a different regionmap to the selected room(s)
                return undef;
            }
        }

        # All the selected rooms and labels are in the same region
        return 1;
    }

    sub checkSelectedIsConnected {

        # Called by $self->moveRoomsToExit
        # Checks that the clicked room has an exit which leads to at least one of the selected rooms
        #
        # Expected arguments
        #   $roomObj    - The GA::ModelObj::Room on which the user clicked
        #
        # Return values
        #   'undef' on improper arguments or if none of the clicked room's exits leads to any of the
        #       selected rooms
        #   Otherwise, returns the GA::Obj::Exit which connects to one of the selected rooms

        my ($self, $roomObj, $check) = @_;

        # Local variables
        my %roomHash;

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->checkSelectedIsConnected',
                @_,
            );
        }

        # Compile a hash of selected rooms
        if ($self->selectedRoom) {

            $roomHash{$self->selectedRoom->number} = $self->selectedRoom;

        } else {

            %roomHash = $self->selectedRoomHash;
        }

        # Check each of the clicked room's exits in turn
        foreach my $exitNum ($roomObj->ivValues('exitNumHash')) {

            my $exitObj = $self->worldModelObj->ivShow('exitModelHash', $exitNum);

            if ($exitObj->destRoom && exists $roomHash{$exitObj->destRoom}) {

                # Success! The clicked room connects to one of the selected rooms via this exit
                return $exitObj;
            }
        }

        # The clicked room doesn't connect to any of the selected rooms
        return undef;
    }

    sub interpretDir {

        # Called by $self->moveRoomsInDir
        # Convert a distance (in gridblocks) and a standard primary direction into a vector in the
        #   form (x, y, z)
        #
        # Expected arguments
        #   $distance   - A distance in gridblocks
        #   $dir        - A standard primary direction, e.g. north
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise a list in the form
        #       ($adjustXPos, $adjustYPos, $adjustZPos);

        my ($self, $distance, $dir, $check) = @_;

        # Local variables
        my (
            $adjustXPos, $adjustYPos, $adjustZPos, $listRef,
            @emptyList,
        );

        # Check for improper arguments
        if (! defined $distance || ! defined $dir || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->interpretDir', @_);
            return @emptyList;
        }

        # Extract from $self->constVectorHash the list corresponding to the primary direction
        #   (e.g. $dir = 'north', $listRef = [0, -1, 0] )
        $listRef = $self->ivShow('constVectorHash', $dir);

        # Work out the vector
        $adjustXPos = $$listRef[0] * $distance;
        $adjustYPos = $$listRef[1] * $distance;
        $adjustZPos = $$listRef[2] * $distance;

        return ($adjustXPos, $adjustYPos, $adjustZPos);
    }

    sub findSelectedRoomBoundaries {

        # Called by $self->moveRoomsToClick
        # Finds the boundaries of the area (in three dimensions) covered by the selected rooms
        # Ignores selected room tags, room guilds, exits, exit tags and labels
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise a list of map coordinates, representing the boundaries of the smallest
        #       three-dimensional area containing all the selected rooms, in the form
        #       ($left, $right, $top, $bottom, $up, $down)

        my ($self, $check) = @_;

        # Local variables
        my (
            $left, $right, $top, $bottom, $up, $down,
            @emptyList,
        );

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->findSelectedRoomBoundaries', @_);
            return @emptyList;
        }

        # If there is a single selected room, the job is easy
        if ($self->selectedRoom) {

            return (
                $self->selectedRoom->xPosBlocks,    # $left
                $self->selectedRoom->xPosBlocks,    # $right
                $self->selectedRoom->yPosBlocks,    # $top
                $self->selectedRoom->yPosBlocks,    # $bottom
                $self->selectedRoom->zPosBlocks,    # $up
                $self->selectedRoom->zPosBlocks,    # $down
            );

        } else {

            # Otherwise, go through each selected room in turn
            foreach my $roomObj ($self->ivValues('selectedRoomHash')) {

                if (! defined $left) {

                    # This is the first room in the list
                    $left = $roomObj->xPosBlocks;
                    $right = $roomObj->xPosBlocks;
                    $top = $roomObj->yPosBlocks;
                    $bottom = $roomObj->yPosBlocks;
                    $up = $roomObj->zPosBlocks;
                    $down = $roomObj->zPosBlocks;

                } else {

                    if ($roomObj->xPosBlocks < $left) {

                        $left = $roomObj->xPosBlocks;
                    }

                    if ($roomObj->xPosBlocks > $right) {

                        $right = $roomObj->xPosBlocks;
                    }

                    if ($roomObj->yPosBlocks < $top) {

                        $top = $roomObj->yPosBlocks;
                    }

                    if ($roomObj->yPosBlocks > $bottom) {

                        $bottom = $roomObj->yPosBlocks;
                    }

                    if ($roomObj->zPosBlocks < $down) {

                        $down = $roomObj->zPosBlocks;
                    }

                    if ($roomObj->zPosBlocks > $up) {

                        $up = $roomObj->zPosBlocks;
                    }
                }
            }

            # Operation complete
            return ($left, $right, $top, $bottom, $up, $down);
        }
    }

    sub findNewCoordinates {

        # Called by $self->moveRoomsToExit
        # Given the clicked room and one of its exits, find the coordinates where we would normally
        #   expect to find the destination room
        #
        # Expected arguments
        #   $roomObj    - The GA::ModelObj::Room on which the user clicked
        #   $exitObj    - Its GA::Obj::Exit which leads to one of the selected rooms
        #
        # Return values
        #   An empty list on improper arguments or for a general error
        #   Otherwise returns a list of map coordinates in the form (xPos, $yPos, $zPos)

        my ($self, $roomObj, $exitObj, $check) = @_;

        # Local variables
        my (
            $standardDir, $xPos, $yPos, $zPos, $exitLength, $listRef,
            @emptyList,
            %vectorHash,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! defined $exitObj || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->findNewCoordinates', @_);
            return @emptyList;
        }

        # For unallocatable exits, there are no expected coordinates
        if (! $exitObj->mapDir) {

            return @emptyList;
        }

        # Extract from $self->constVectorHash the list corresponding to the exit's map direction
        #   (e.g. $exitObj->mapDir = 'north', $listRef = [0, -1, 0] )
        $listRef = $self->ivShow('constVectorHash', $exitObj->mapDir);

        # Work out the coordinates of $exitObj's destination room after the move
        $exitLength = $self->worldModelObj->getExitLength($exitObj);
        $xPos = $roomObj->xPosBlocks + ($$listRef[0] * $exitLength);
        $yPos = $roomObj->yPosBlocks + ($$listRef[1] * $exitLength);
        $zPos = $roomObj->zPosBlocks + ($$listRef[2] * $exitLength);

        return ($xPos, $yPos, $zPos);
    }

    sub checkNewCoordinates {

        # Called by $self->moveRoomsInDir and ->moveRoomsToExit
        # Given the direction that all the selected rooms need to be moved - described by a vector
        #   (x, y, z) - check that the proposed positions of each selected rooms is not already
        #   occupied by another room and is not outside the boundaries of the map
        #
        # Expected arguments
        #   $adjustXPos, $adjustYPos, $adjustZPos
        #       - The vector that describes the direction in which all selected rooms must be moved
        #
        # Return values
        #   'undef' on improper arguments, or if any of the proposed positions are occupied by other
        #       rooms or are outside the boundaries of the map
        #   1 otherwise

        my ($self, $adjustXPos, $adjustYPos, $adjustZPos, $check) = @_;

        # Local variables
        my (
            @roomList,
            %roomHash,
        );

        # Check for improper arguments
        if (
            ! defined $adjustXPos || ! defined $adjustYPos || ! defined $adjustZPos
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->checkNewCoordinates', @_);
        }

        # Compile a hash of selected rooms
        if ($self->selectedRoom) {

            $roomHash{$self->selectedRoom->number} = $self->selectedRoom;

        } else {

            %roomHash = $self->selectedRoomHash;
        }

        # Also use a list of selected rooms
        @roomList = values %roomHash;

        foreach my $roomObj (@roomList) {

            my ($xPos, $yPos, $zPos, $posn);

            # Get the proposed position of the room
            $xPos = $roomObj->xPosBlocks + $adjustXPos;
            $yPos = $roomObj->yPosBlocks + $adjustYPos;
            $zPos = $roomObj->zPosBlocks + $adjustZPos;

            # Check that the gridblock isn't outside the boundaries of the map
            if (
                (($xPos < 0) || ($xPos >= $self->currentRegionmap->gridWidthBlocks))
                || (($yPos < 0) || ($yPos >= $self->currentRegionmap->gridHeightBlocks))
            ) {
                # The proposed position is outside the map's boundary
                return undef;
            }

            # Check that the gridblock isn't already occupied by another room that's not one of the
            #   selected rooms (which is also being moved)
            $posn = $xPos . '_' . $yPos . '_' . $zPos;
            if (
                $self->currentRegionmap->ivExists('gridRoomHash', $posn)
                && (! exists $roomHash{$self->currentRegionmap->ivShow('gridRoomHash', $posn)})
            ) {
                # The gridblock is occupied
                return undef;
            }
        }

        # All of the selected rooms can be moved to their new positions
        return 1;
    }

    sub findMiddleSelectedRoom {

        # Called by $self->moveRoomsToClick
        # Using the selected rooms which cover a three-dimensional area described by the supplied
        #   arguments, find the room which is closest to the middle, and return it
        # However, if the automapper's current room is a selected room, return that instead
        #
        # Expected arguments
        #   $left, $right, $top, $bottom, $up, $down
        #       - The boundaries of the three-dimensional area containing the selected rooms
        #
        # Return values
        #   'undef' on improper arguments
        #   Otherwise returns the blessed reference of the GA::ModelObj::Room closest to the centre
        #       of the area (or the blessed reference of the current room, if it is a selected room)

        my ($self, $left, $right, $top, $bottom, $up, $down, $check) = @_;

        # Local variables
        my ($xPos, $yPos, $zPos, $nearestRoomObj, $nearestLength);

        # Check for improper arguments
        if (
            ! defined $left || ! defined $right || ! defined $top || ! defined $bottom
            || ! defined $up || ! defined $down || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->findMiddleSelectedRoom', @_);
        }

        # If there is only a single selected room, then that's the one in the middle
        if ($self->selectedRoom) {

            return $self->selectedRoom;

        # If the automapper's current room is selected, then that's the one in the middle
        #   (regardless of its position)
        } elsif (
            $self->mapObj->currentRoom
            && $self->ivExists('selectedRoomHash', $self->mapObj->currentRoom->number)
        ) {
            return $self->mapObj->currentRoom;

        } else {

            # Find the middle of the three-dimensional area
            $xPos = $left + (($right - $left) / 2);
            $yPos = $top + (($bottom - $top) / 2);
            $zPos = $down + (($up - $down) / 2);

            foreach my $roomObj ($self->ivValues('selectedRoomHash')) {

                my $length;

                if (! $nearestRoomObj) {

                    # This is the first room in the list
                    $nearestRoomObj = $roomObj;
                    # Find its distance from the middle using Euclydian geometry...
                    $nearestLength = sqrt (
                        (($xPos - $roomObj->xPosBlocks) ** 2)
                        + (($yPos - $roomObj->yPosBlocks) ** 2)
                        + (($zPos - $roomObj->zPosBlocks) ** 2)
                    );

                } else {

                    $length = sqrt(
                        (($xPos - $roomObj->xPosBlocks) ** 2)
                        + (($yPos - $roomObj->yPosBlocks) ** 2)
                        + (($zPos - $roomObj->zPosBlocks) ** 2)
                    );

                    if ($length < $nearestLength) {

                        # This room is nearer to the middle
                        $nearestRoomObj = $roomObj;
                        $nearestLength = $length;
                    }
                }
            }

            return $nearestRoomObj;
        }
    }

    sub refineSelectedArea {

        # Called by $self->moveRoomsToClick
        # Given the boundaries of the three-dimensional area covered by the selected rooms, and
        #   given the blessed reference of the room which is to be moved to the position on the map
        #   where the user clicked, return the boundaries of the new area
        # At the same time, if those boundaries would mean selected rooms being outside the physical
        #   limits of the map, adjust the boundaries so that all the rooms are on the map (which
        #   will mean that the 'centre' room is no longer placed at the position where the user
        #   clicked)
        #
        # Expected arguments
        #   $left, $right, $top, $bottom, $up, $down
        #       - The boundaries of the three-dimensional area containing the selected rooms
        #   $clickXPosBlocks, $clickYPosBlocks
        #       - The grid coordinates on the map of the user's click
        #   $roomObj
        #       - The RoomModelObj of the 'centre' room
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise returns the boundaries of the refined area, in the form
        #       ($newLeft, $newRight, $newTop, $newBottom, $newUp, $newDown)

        my (
            $self, $left, $right, $top, $bottom, $up, $down, $clickXPosBlocks, $clickYPosBlocks,
            $roomObj, $check
        ) = @_;

        # Local variables
        my (
            $xLength, $yLength, $zLength, $newLeft, $newRight, $newTop, $newBottom, $newUp,
            $newDown,
        );

        # Check for improper arguments
        if (
            ! defined $left || ! defined $right || ! defined $top || ! defined $bottom
            || ! defined $up || ! defined $down || ! defined $roomObj
            || ! defined $clickXPosBlocks || ! defined $clickYPosBlocks || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->refineSelectedArea', @_);
        }

        # First, work out the grid coordinates of the 'centre' room, relative to the top-left corner
        #   of the area containing all the selected rooms
        $xLength = $roomObj->xPosBlocks - $left;
        $yLength = $roomObj->yPosBlocks - $top;
        $zLength = $roomObj->zPosBlocks - $down;

        # Next, find the top-left corner of the area that will contain all the selected rooms when
        #   they are moved, with the 'centre' room over the gridblock clicked by the user
        $newLeft = $clickXPosBlocks - $xLength;
        $newTop = $clickYPosBlocks - $yLength;
        $newDown = $self->currentRegionmap->currentLevel - $zLength;
        # Find also the bottom-right corner
        $newRight = $newLeft + ($right - $left);
        $newBottom = $newTop + ($bottom - $top);
        $newUp = $newDown + ($up - $down);

        # Now we need to check whether all the rooms will still fit on the map, if we were to move
        #   the selected rooms to the new area (with $roomObj moved to the position of the click)
        # If not, we need to change the clicked gridblock until the rooms do fit
        if ($newLeft < 0) {

            $newLeft = 0;
            $newRight = ($right - $left);

        } elsif ($newRight >= $self->currentRegionmap->gridWidthBlocks) {

            $newLeft = $self->currentRegionmap->gridWidthBlocks - ($right - $left - 1);
            $newRight = $self->currentRegionmap->gridWidthBlocks - 1;
        }

        if ($newTop < 0) {

            $newTop = 0;
            $newBottom = ($bottom - $top);

        } elsif ($newBottom >= $self->currentRegionmap->gridHeightBlocks) {

            $newTop = $self->currentRegionmap->gridHeightBlocks - ($bottom - $top - 1);
            $newBottom = $self->currentRegionmap->gridHeightBlocks - 1;
        }

        # Operation complete
        return ($newLeft, $newRight, $newTop, $newBottom, $newUp, $newDown);
    }

    sub checkSelectedArea {

        # Called by $self->moveRoomsToClick
        # Checks the gridblocks into which the selected rooms will be copied, looking for any blocks
        #   which are already occupied by other rooms
        #
        # Expected arguments
        #   $adjustXPos, $adjustYPos, $adjustZPos
        #       - Describes a vector between the coordinates of the selected rooms in their
        #           old position, and their coordinates at their new position
        #
        # Return values
        #   'undef' on improper arguments, or if any of the selected rooms is about to be copied
        #       into an occupied gridblock
        #   1 otherwise

        my ($self, $adjustXPos, $adjustYPos, $adjustZPos, $check) = @_;

        # Local variables
        my %roomHash;

        # Check for improper arguments
        if (
            ! defined $adjustXPos || ! defined $adjustYPos || ! defined $adjustZPos
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->checkSelectedArea', @_);
        }

        # Import a list of all the selected rooms, and copy them into a hash
        if ($self->selectedRoom) {

            $roomHash{$self->selectedRoom->number} = $self->selectedRoom;

        } else {

            %roomHash = $self->selectedRoomHash;
        }

        # Check each one in turn
        foreach my $roomObj (values %roomHash) {

            my ($xPos, $yPos, $zPos, $posn);

            # Get the room's proposed coordinates
            $xPos = $roomObj->xPosBlocks + $adjustXPos;
            $yPos = $roomObj->yPosBlocks + $adjustYPos;
            $zPos = $roomObj->zPosBlocks + $adjustZPos;

            # The current regionmap stores rooms in a hash, in the form
            #   ->gridRoomHash{'x_y_z'} = model_number_of_room_at_these_coordinates
            # Create the key that would correspond to this room
            $posn = $xPos . '_' . $yPos . '_' . $zPos;

            # Is that gridblock occupied (i.e. does the key exist in the hash?)
            if ($self->currentRegionmap->ivExists('gridRoomHash', $posn)) {

                # The gridblock is occupied; but is the occupying room one of the selected rooms
                #   that is about to be moved anyway?
                if (! exists $roomHash{$self->currentRegionmap->ivShow('gridRoomHash', $posn)}) {

                    # This gridblock is occupied by a room that's not going to be moved.
                    return undef;
                }
            }
        }

        # Operation complete. All the necessary gridblocks are empty.
        return 1;
    }

    sub checkNewLabelPositions {

        # Called by $self->moveRoomsInDir, ->moveRoomsToClick and ->moveRoomsToExit
        # Checks that the new positions of any selected labels (if any) are within the boundaries of
        #   the map
        #
        # Expected arguments
        #   $adjustXPos, $adjustYPos, $adjustZPos
        #       - Describes a vector between the coordinates of the selected rooms in their old
        #           position, and their coordinates in the new position (in gridblocks)
        #
        # Return values
        #   'undef' on improper arguments or if any of the selected labels will be moved outside the
        #       boundaries of the map
        #   1 otherwise

        my ($self, $adjustXPos, $adjustYPos, $adjustZPos, $check) = @_;

        # Local variables
        my (
            $mapWidthPixels, $mapHeightPixels,
            %labelHash,
        );

        # Check for improper arguments
        if (
            ! defined $adjustXPos || ! defined $adjustYPos || ! defined $adjustZPos
            || defined $check
        ) {
            return $axmud::CLIENT->writeImproper($self->_objClass . '->checkNewLabelPositions', @_);
        }

        # Get a list of selected labels, and copy them into a hash
        if ($self->selectedLabel) {

            $labelHash{$self->selectedLabel->number} = $self->selectedLabel;

        } else {

            %labelHash = $self->selectedLabelHash;
        }

        # Work out the current size of the map, in pixels
        $mapWidthPixels = (
            $self->currentRegionmap->blockWidthPixels
            * $self->currentRegionmap->gridWidthBlocks
        );

        $mapHeightPixels = (
            $self->currentRegionmap->blockHeightPixels
            * $self->currentRegionmap->gridHeightBlocks
        );

        # Check each label in turn
        foreach my $labelObj (values %labelHash) {

            my ($xPosPixels, $yPosPixels, $level);

            # Work out the x/y coordinates, in pixels, of the label after it is moved
            $xPosPixels = $labelObj->xPosPixels
                            + ($adjustXPos * $self->currentRegionmap->blockWidthPixels);
            $yPosPixels = $labelObj->yPosPixels
                            + ($adjustYPos * $self->currentRegionmap->blockHeightPixels);
            # Work out the label's new level
            $level = $labelObj->level + $adjustZPos;

            # Check the label's position
            if (
                ($xPosPixels < 0)
                || ($xPosPixels >= ($mapWidthPixels - 1))
                || ($yPosPixels < 0)
                || ($yPosPixels >= ($mapHeightPixels - 1))
            ) {
                # The label is outside the boundaries of the map
                return undef;
            }
        }

        # All labels (if there are any) are within the boundaries of the map
        return 1;
    }

    # Other functions

    sub trackPosn {

        # Called by GA::Obj::Map->setCurrentRoom
        # Examines the current room's position in the visible map and, if the room is too close to
        #   the edge of the visible map, moves the window's scrollbars to centre the visible map
        #   over the current room
        # Does nothing if the world model's tracking flag isn't set
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   'undef' on improper arguments, if the tracking flag isn't set or if there is no current
        #       room
        #   1 otherwise

        my ($self, $check) = @_;

        # Local variables
        my (
            $roomObj, $sensitivity, $startXBlocks, $startYBlocks, $widthBlocks, $heightBlocks,
            $centreXBlocks, $centreYBlocks, $insideStartXBlocks, $insideStartYBlocks,
            $insideStopXBlocks, $insideStopYBlocks,
        );

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->trackPosn', @_);
        }

        # Import the current room
        $roomObj = $self->mapObj->currentRoom;

        # Don't do anything if there is no current room, or if the tracking flag isn't set
        if (! $roomObj || ! $self->worldModelObj->trackPosnFlag) {

            return undef;
        }

        # Import the tracking sensitivity, a value from 0-1
        # 0 means 'always centre the map on the current room', 1 means 'centre the map only when the
        #   current room is outside the visible window'
        # 0.5 means that the room must be halfway between the centre of the visible map, before the
        #   map is centred. 0.66 means the room must be two-thirds of the distance away from the
        #   centre, and 0.9 means that the room must be 90% of the distance away from the centre
        $sensitivity = $self->worldModelObj->trackingSensitivity;

        # Deal with special case of sensitivity = 0
        if (! $sensitivity) {

            # Don't bother checking the current room's position in the visible map - go ahead and
            #   centre it
            $self->centreMapOverRoom($roomObj);

        } else {

            # Get the size and position of the visible map
            ($startXBlocks, $startYBlocks, $widthBlocks, $heightBlocks)
                = $self->getMapPosnInBlocks();
            # Find the visible map's centre
            $centreXBlocks = ($startXBlocks + ($widthBlocks / 2) - 0.5);
            $centreYBlocks = ($startYBlocks + ($heightBlocks / 2) - 0.5);

            # Now, get the limits of a rectangle inside the visible window. The larger the
            #   sensitivity, the larger this rectangle. If the current room is outside this
            #   rectangle, the map needs to be centred
            if ($sensitivity == 1) {

                $insideStartXBlocks = $startXBlocks;
                $insideStartYBlocks = $startYBlocks;
                $insideStopXBlocks = ($startXBlocks + $widthBlocks - 1);
                $insideStopYBlocks = ($startYBlocks + $heightBlocks - 1);

            } else {

                $insideStartXBlocks = ($centreXBlocks - (($widthBlocks / 2) * $sensitivity));
                $insideStartYBlocks = ($centreYBlocks - (($heightBlocks / 2) * $sensitivity));
                $insideStopXBlocks = ($centreXBlocks + (($widthBlocks / 2) * $sensitivity));
                $insideStopYBlocks = ($centreYBlocks + (($heightBlocks / 2) * $sensitivity));
            }

            # Centre the map if the room is outside this rectangle
            if (
                $roomObj->xPosBlocks < $insideStartXBlocks
                || $roomObj->yPosBlocks < $insideStartYBlocks
                || $roomObj->xPosBlocks > $insideStopXBlocks
                || $roomObj->yPosBlocks > $insideStopYBlocks
            ) {
                $self->centreMapOverRoom($roomObj);
            }
        }

        return 1;
    }

    sub paintRoom {

        # Called by $self->enableModeColumn, ->repaintSelectedRoomsCallback and ->createNewRoom
        # Also called by GA::Obj::Map->autoProcessNewRoom and ->useExistingRoom
        #
        # 'Paints' a room by transfering the values of certain IVs from the world model's painter
        #   object (a non-model GA::ModelObj::Room object) to the specified room (which IS in the
        #   world model)
        # Scalar and list IVs are copied, replacing existing values; but hashes are merged. If the
        #   same key exists in both the specified room and the painter, the painter's key-value pair
        #   is used.
        #
        # Expected arguments
        #   $roomObj        - The room model object to 'paint'
        #
        # Optional arguments
        #   $updateFlag     - If set to TRUE, calls the world model to tell it to redraw the room in
        #                       every Automapper window; otherwise set to FALSE (or 'undef')
        #
        # Return values
        #   'undef' on improper arguments
        #   1 otherwise

        my ($self, $roomObj, $updateFlag, $check) = @_;

        # Local variables
        my (
            $painterObj,
            @ivList,
        );

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->paintRoom', @_);
        }

        # Import the painter object (for convenience)
        $painterObj = $self->worldModelObj->painterObj;
        # Import the list of IVs that are to be copied
        @ivList = $self->worldModelObj->painterIVList;

        foreach my $iv (@ivList) {

            my (
                $ivType,
                %hash,
            );

            # Work out what kind of IV this is - scalar, list or hash
            $ivType = ref $roomObj->{$iv};

            if ($ivType eq 'HASH') {

                # Hash IVs are merged
                %hash = $painterObj->$iv;
                foreach my $key (keys %hash) {

                    $roomObj->ivAdd($iv, $key, $hash{$key});
                }

            } else {

                # Scalar/list IVs are copied whole, replacing the existing IV
                $roomObj->{$iv} = $painterObj->{$iv};
            }
        }

        if ($updateFlag) {

            # Redraw the room in every Automapper window using this world model
            $self->worldModelObj->updateMaps('room', $roomObj);
        }

        return 1;
    }

    sub autoCompareLocatorRoom {

        # Called by $self->createNewRoom
        # Compares the Locator's current room against all rooms in the same regionmap. Returns a
        #   list of all the matching rooms
        #
        # Expected arguments
        #   $regionmapObj  - The GA::Obj::Regionmap containing the current room
        #
        # Return values
        #   An empty list on improper arguments or if there are no matching rooms
        #   Otherwise returns a list of blessed references to matching GA::ModelObj::Room objects

        my ($self, $regionmapObj, $check) = @_;

        # Local variables
        my (@emptyList, @roomList, @returnList);

        # Check for improper arguments
        if (! defined $regionmapObj || defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->autoCompareLocatorRoom', @_);
            return @emptyList;
        }

        # Don't do anything if the Locator task isn't running or doesn't know about the current room
        if (! $self->session->locatorTask || ! $self->session->locatorTask->roomObj) {

            return @emptyList;
        }

        # Get a list of rooms in the same regionmap
        @roomList = $regionmapObj->ivValues('gridRoomHash');
        foreach my $roomNum (@roomList) {

            my ($roomObj, $result);

            $roomObj = $self->worldModelObj->ivShow('modelHash', $roomNum);

            # Compare the two rooms. The TRUE arguments forbids empty, dark or unspecified rooms
            #   from being matched
            ($result) = $self->compareRooms($self->session, $roomObj, TRUE);
            if (! $result) {

                # $roomRef matches the Locator's current room
                push (@returnList, $roomObj);
            }
        }

        return @returnList;
    }

    sub findOccupiedMap {

        # Called by $self->regionScreenshotCallback
        # Finds the boundaries of the regionmap occupied by rooms and labels on the current level
        #
        # Expected arguments
        #   (none besides $self)
        #
        # Return values
        #   An empty list on improper arguments or if there are no rooms or labels on the current
        #       level of the current regionmap
        #   Otherwise a list of map coordinates - representing the boundaries (in pixels) of the
        #       smallest square containing all rooms and labels on the current level - in the form
        #       ($left, $right, $top, $bottom)

        my ($self, $check) = @_;

        # Local variables
        my (
            $left, $right, $top, $bottom,
            @emptyList,
        );

        # Check for improper arguments
        if (defined $check) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->findOccupiedMap', @_);
            return @emptyList;
        }

        # Otherwise, go through each room in turn
        foreach my $number ($self->currentRegionmap->ivValues('gridRoomHash')) {

            my $roomObj = $self->worldModelObj->ivShow('modelHash', $number);

            if ($roomObj->zPosBlocks == $self->currentRegionmap->currentLevel) {

                if (! defined $left) {

                    # This is the first object so far
                    $left = $roomObj->xPosBlocks;
                    $right = $roomObj->xPosBlocks;
                    $top = $roomObj->yPosBlocks;
                    $bottom = $roomObj->yPosBlocks;

                } else {

                    if ($roomObj->xPosBlocks < $left) {

                        $left = $roomObj->xPosBlocks;
                    }

                    if ($roomObj->xPosBlocks > $right) {

                        $right = $roomObj->xPosBlocks;
                    }

                    if ($roomObj->yPosBlocks < $top) {

                        $top = $roomObj->yPosBlocks;
                    }

                    if ($roomObj->yPosBlocks > $bottom) {

                        $bottom = $roomObj->yPosBlocks;
                    }
                }
            }
        }

        # Convert these boundaries from blocks to pixels
        if (defined $left) {

            $left *= $self->currentRegionmap->blockWidthPixels;
            $right *= $self->currentRegionmap->blockWidthPixels;
            $top *= $self->currentRegionmap->blockHeightPixels;
            $bottom *= $self->currentRegionmap->blockHeightPixels;
        }

        # Now go through each label in turn
        foreach my $labelObj ($self->currentRegionmap->ivValues('gridLabelHash')) {

            if ($labelObj->level == $self->currentRegionmap->currentLevel) {

                if (! defined $left) {

                    # This is the first object so far
                    $left = $labelObj->xPosPixels;
                    $right = $labelObj->xPosPixels;
                    $top = $labelObj->yPosPixels;
                    $bottom = $labelObj->yPosPixels;

                } else {

                    if ($labelObj->xPosPixels < $left) {

                        $left = $labelObj->xPosPixels;
                    }

                    if ($labelObj->xPosPixels > $right) {

                        $right = $labelObj->xPosPixels;
                    }

                    if ($labelObj->yPosPixels < $top) {

                        $top = $labelObj->yPosPixels;
                    }

                    if ($labelObj->yPosPixels > $bottom) {

                        $bottom = $labelObj->yPosPixels;
                    }
                }
            }
        }

        # Can't do anything if there are no rooms or labels on the current level of the current
        #   regionmap
        if (! defined $left) {

            return @emptyList;

        } else {

            # Expand the boundaries by a couple of gridblocks in each direction, so that there's
            #   much less chance of anything being drawn outside them
            # (Because occupied gridblocks are positioned at their top-left corners, we expand the
            #   right and bottom boundaries by 3 to compensate)
            $left -= ($self->currentRegionmap->blockWidthPixels * 2);
            if ($left < 0) {

                $left = 0;
            }

            $right += ($self->currentRegionmap->blockWidthPixels * 3);
            if (
                $right >= (
                    $self->currentRegionmap->gridWidthBlocks
                    * $self->currentRegionmap->blockWidthPixels
                )
            ) {
                $right = (
                    $self->currentRegionmap->gridWidthBlocks
                    * $self->currentRegionmap->blockWidthPixels
                ) - 1;
            }

            $top -= ($self->currentRegionmap->blockHeightPixels * 2);
            if ($top < 0) {

                $top = 0;
            }

            $bottom += ($self->currentRegionmap->blockHeightPixels * 3);
            if (
                $bottom >= (
                    $self->currentRegionmap->gridHeightBlocks
                    * $self->currentRegionmap->blockHeightPixels
                )
            ) {
                $bottom = (
                    $self->currentRegionmap->gridHeightBlocks
                    * $self->currentRegionmap->blockHeightPixels
                ) - 1;
            }

            # Operation complete
            return ($left, $right, $top, $bottom);
        }
    }

    sub getAvailableDirs {

        # Called by $self->allocateMapDirCallback
        # Given a list of primary directions - either the usual list of ten, or the full list of
        #   eighteen - check a room's exits, to see which of these primary directions are not in
        #   use
        #
        # Expected arguments
        #   $roomObj    - The parent room of $self->selectedExit
        #   @dirList    - A list of primary directions
        #
        # Return values
        #   An empty list on improper arguments
        #   Otherwise returns a list in the form:
        #       (exit's_temporarily_allocated_dir, list_of_other_available_exits)
        #   ...where 'exit's_temporarily_allocated_dir' will be 'undef', if the exit has no
        #       allocated direction, and where list_of_other_available_exits will be an empty list,
        #       if none of the remaining primary directions in @dirList are available

        my ($self, $roomObj, @dirList) = @_;

        # Local variables
        my (
            $firstComboItem,
            @emptyList, @comboList,
        );

        # Check for improper arguments
        if (! defined $roomObj || ! @dirList) {

            $axmud::CLIENT->writeImproper($self->_objClass . '->getAvailableDirs', @_);
            return @emptyList;
        }

        # Prepare a list of primary direction exits which are still available
        OUTER: foreach my $mapDir (@dirList) {

            INNER: foreach my $exitNum ($roomObj->ivValues('exitNumHash')) {

                my $exitObj = $self->worldModelObj->ivShow('exitModelHash', $exitNum);

                if (
                    $exitObj->mapDir
                    && $exitObj->mapDir eq $mapDir
                    && ($exitObj->drawMode eq 'primary' || $exitObj->drawMode eq 'perm_alloc')
                ) {
                    # $mapDir isn't available
                    next OUTER;
                }
            }

            # $mapDir is available. If it's the same as the selected exit's temporarily allocated
            #   map direction, we put that at the front of the list; otherwise, add it to the end
            if (
                $self->selectedExit->mapDir
                && $self->selectedExit->mapDir eq $mapDir
            ) {
                $firstComboItem = $mapDir;
            } else {
                push (@comboList, $mapDir);
            }
        }

        return ($firstComboItem, @comboList);
    }

    ##################
    # Accessors - set

    sub del_drawObj {

        # (Called by GA::Obj::WorldModel->deleteRooms, ->deleteExits and ->deleteLabels)

        my ($self, $iv, $number, $check) = @_;

        # Check for improper arguments
        if (! defined $iv || ! defined $number || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->del_drawObj', @_);
        }

        $self->ivDelete($iv, $number);

        return 1;
    }

    sub reset_freeClickMode {

        my ($self, $check) = @_;

        # Check for improper arguments
        if (defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->reset_freeClickMode', @_);
        }

        $self->ivPoke('freeClickMode', 'default');

        return 1;
    }

    sub set_ignoreMenuUpdateFlag {

        my ($self, $flag, $check) = @_;

        # Check for improper arguments
        if (! defined $flag || defined $check) {

            return $axmud::CLIENT->writeImproper(
                $self->_objClass . '->set_ignoreMenuUpdateFlag',
                @_,
            );
        }

        if ($flag) {
            $self->ivPoke('ignoreMenuUpdateFlag', TRUE);
        } else {
            $self->ivPoke('ignoreMenuUpdateFlag', FALSE);
        }

        return 1;
    }

    sub set_mapObj {

        my ($self, $mapObj, $check) = @_;

        # Check for improper arguments
        if (! defined $mapObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->set_mapObj', @_);
        }

        # Update IVs
        $self->ivPoke('mapObj', $mapObj);

        return 1;
    }

    sub del_markedExit {

        # (Called by GA::Obj::WorldModel->deleteExits)

        my ($self, $number, $check) = @_;

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->del_markedExit', @_);
        }

        $self->ivDelete('markedExitHash', $number);
        $self->ivDelete('markedExitTagHash', $number);

        return 1;
    }

    sub del_markedLabel {

        # (Called by GA::Obj::WorldModel->deleteLabels)

        my ($self, $number, $check) = @_;

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->del_markedLabel', @_);
        }

        $self->ivDelete('markedLabelHash', $number);

        return 1;
    }

    sub del_markedRoom {

        # (Called by GA::Obj::WorldModel->deleteRooms)

        my ($self, $number, $check) = @_;

        # Check for improper arguments
        if (! defined $number || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->del_markedRoom', @_);
        }

        $self->ivDelete('markedRoomHash', $number);
        $self->ivDelete('markedRoomTagHash', $number);
        $self->ivDelete('markedRoomGuildHash', $number);

        return 1;
    }

    sub set_pairedTwinExit {

        my ($self, $exitObj, $check) = @_;

        # Check for improper arguments
        if (! defined $exitObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->set_pairedTwinExit', @_);
        }

        # Update IVs
        $self->ivPoke('pairedTwinExit', $exitObj);

        return 1;
    }

    sub set_pairedTwinRoom {

        my ($self, $roomObj, $check) = @_;

        # Check for improper arguments
        if (! defined $roomObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->set_pairedTwinRoom', @_);
        }

        # Update IVs
        $self->ivPoke('pairedTwinRoom', $roomObj);

        return 1;
    }

    sub set_worldModelObj {

        my ($self, $worldModelObj, $check) = @_;

        # Check for improper arguments
        if (! defined $worldModelObj || defined $check) {

            return $axmud::CLIENT->writeImproper($self->_objClass . '->set_worldModelObj', @_);
        }

        $self->ivPoke('worldModelObj', $worldModelObj);

        # Reset the window, which avoids all kinds of problems
        $self->winReset($self->session->mapObj);

        return 1;
    }

    ##################
    # Accessors - get

    sub mapObj
        { $_[0]->{mapObj} }
    sub worldModelObj
        { $_[0]->{worldModelObj} }

    sub menuBar
        { $_[0]->{menuBar} }
    sub toolbar
        { $_[0]->{toolbar} }
    sub menuToolItemHash
        { my $self = shift; return %{$self->{menuToolItemHash}}; }

    sub toolbarCurrentSet
        { $_[0]->{toolbarCurrentSet} }
    sub toolbarSetCount
        { $_[0]->{toolbarSetCount} }
    sub toolbarSwitchIcon
        { $_[0]->{toolbarSwitchIcon} }
    sub toolbarMainSeparator
        { $_[0]->{toolbarMainSeparator} }
    sub toolbarButtonHash
        { my $self = shift; return %{$self->{toolbarButtonHash}}; }

    sub hPaned
        { $_[0]->{hPaned} }

    sub treeViewModel
        { $_[0]->{treeViewModel} }
    sub treeView
        { $_[0]->{treeView} }
    sub treeViewScroller
        { $_[0]->{treeViewScroller} }
    sub treeViewWidthPixels
        { $_[0]->{treeViewWidthPixels} }
    sub treeViewSelectedLine
        { $_[0]->{treeViewSelectedLine} }
    sub treeViewRegionHash
        { my $self = shift; return %{$self->{treeViewRegionHash}}; }
    sub treeViewPointerHash
        { my $self = shift; return %{$self->{treeViewPointerHash}}; }

    sub canvas
        { $_[0]->{canvas} }
    sub canvasRoot
        { $_[0]->{canvasRoot} }
    sub canvasFrame
        { $_[0]->{canvasFrame} }
    sub canvasScroller
        { $_[0]->{canvasScroller} }
    sub canvasHAdjustment
        { $_[0]->{canvasHAdjustment} }
    sub canvasVAdjustment
        { $_[0]->{canvasVAdjustment} }
    sub canvasBackground
        { $_[0]->{canvasBackground} }

    sub canvasTooltipObj
        { $_[0]->{canvasTooltipObj} }
    sub canvasTooltipObjType
        { $_[0]->{canvasTooltipObjType} }
    sub canvasTooltipFlag
        { $_[0]->{canvasTooltipFlag} }

    sub currentRegionmap
        { $_[0]->{currentRegionmap} }

    sub selectedRoom
        { $_[0]->{selectedRoom} }
    sub selectedRoomTag
        { $_[0]->{selectedRoomTag} }
    sub selectedRoomGuild
        { $_[0]->{selectedRoomGuild} }
    sub selectedExit
        { $_[0]->{selectedExit} }
    sub selectedExitTag
        { $_[0]->{selectedExitTag} }
    sub selectedLabel
        { $_[0]->{selectedLabel} }

    sub selectedRoomHash
        { my $self = shift; return %{$self->{selectedRoomHash}}; }
    sub selectedRoomTagHash
        { my $self = shift; return %{$self->{selectedRoomTagHash}}; }
    sub selectedRoomGuildHash
        { my $self = shift; return %{$self->{selectedRoomGuildHash}}; }
    sub selectedExitHash
        { my $self = shift; return %{$self->{selectedExitHash}}; }
    sub selectedExitTagHash
        { my $self = shift; return %{$self->{selectedExitTagHash}}; }
    sub selectedLabelHash
        { my $self = shift; return %{$self->{selectedLabelHash}}; }

    sub pairedTwinExit
        { $_[0]->{pairedTwinExit} }
    sub pairedTwinRoom
        { $_[0]->{pairedTwinRoom} }

    sub drawnRoomHash
        { my $self = shift; return %{$self->{drawnRoomHash}}; }
    sub dummyRoomHash
        { my $self = shift; return %{$self->{dummyRoomHash}}; }
    sub drawnRoomEchoHash
        { my $self = shift; return %{$self->{drawnRoomEchoHash}}; }
    sub drawnRoomTagHash
        { my $self = shift; return %{$self->{drawnRoomTagHash}}; }
    sub drawnRoomGuildHash
        { my $self = shift; return %{$self->{drawnRoomGuildHash}}; }
    sub drawnRoomTextHash
        { my $self = shift; return %{$self->{drawnRoomTextHash}}; }
    sub drawnExitHash
        { my $self = shift; return %{$self->{drawnExitHash}}; }
    sub drawnExitTagHash
        { my $self = shift; return %{$self->{drawnExitTagHash}}; }
    sub drawnOrnamentHash
        { my $self = shift; return %{$self->{drawnOrnamentHash}}; }
    sub drawnLabelHash
        { my $self = shift; return %{$self->{drawnLabelHash}}; }
    sub delayDrawFlag
        { $_[0]->{delayDrawFlag} }

    sub markedRoomHash
        { my $self = shift; return %{$self->{markedRoomHash}}; }
    sub markedRoomTagHash
        { my $self = shift; return %{$self->{markedRoomTagHash}}; }
    sub markedRoomGuildHash
        { my $self = shift; return %{$self->{markedRoomGuildHash}}; }
    sub markedExitHash
        { my $self = shift; return %{$self->{markedExitHash}}; }
    sub markedExitTagHash
        { my $self = shift; return %{$self->{markedExitTagHash}}; }
    sub markedLabelHash
        { my $self = shift; return %{$self->{markedLabelHash}}; }

    sub drawCycleExitHash
        { my $self = shift; return %{$self->{drawCycleExitHash}}; }
    sub drawRoomTextSize
        { $_[0]->{drawRoomTextSize} }
    sub drawRoomTextWidth
        { $_[0]->{drawRoomTextWidth} }
    sub drawRoomTextHeight
        { $_[0]->{drawRoomTextHeight} }
    sub drawMapTextSize
        { $_[0]->{drawMapTextSize} }

    sub freeClickMode
        { $_[0]->{freeClickMode} }
    sub exitSensitivity
        { $_[0]->{exitSensitivity} }
    sub exitBendSize
        { $_[0]->{exitBendSize} }
    sub exitClickXPosn
        { $_[0]->{exitClickXPosn} }
    sub exitClickYPosn
        { $_[0]->{exitClickYPosn} }

    sub mode
        { $_[0]->{mode} }

    sub showChar
        { $_[0]->{showChar} }
    sub painterFlag
        { $_[0]->{painterFlag} }

    sub constVectorHash
        { my $self = shift; return %{$self->{constVectorHash}}; }
    sub constDoubleVectorHash
        { my $self = shift; return %{$self->{constDoubleVectorHash}}; }
    sub constArrowVectorHash
        { my $self = shift; return %{$self->{constArrowVectorHash}}; }
    sub constPerpVectorHash
        { my $self = shift; return %{$self->{constPerpVectorHash}}; }
    sub constSpecialVectorHash
        { my $self = shift; return %{$self->{constSpecialVectorHash}}; }
    sub constTriangleCornerHash
        { my $self = shift; return %{$self->{constTriangleCornerHash}}; }
    sub constGtkAnchorHash
        { my $self = shift; return %{$self->{constGtkAnchorHash}}; }

    sub blockCornerXPosPixels
        { $_[0]->{blockCornerXPosPixels} }
    sub blockCornerYPosPixels
        { $_[0]->{blockCornerYPosPixels} }
    sub blockCentreXPosPixels
        { $_[0]->{blockCentreXPosPixels} }
    sub blockCentreYPosPixels
        { $_[0]->{blockCentreYPosPixels} }
    sub borderCornerXPosPixels
        { $_[0]->{borderCornerXPosPixels} }
    sub borderCornerYPosPixels
        { $_[0]->{borderCornerYPosPixels} }
    sub preDrawnIncompleteExitHash
        { my $self = shift; return %{$self->{preDrawnIncompleteExitHash}}; }
    sub preDrawnUncertainExitHash
        { my $self = shift; return %{$self->{preDrawnUncertainExitHash}}; }
    sub preDrawnLongExitHash
        { my $self = shift; return %{$self->{preDrawnLongExitHash}}; }
    sub preDrawnSquareExitHash
        { my $self = shift; return %{$self->{preDrawnSquareExitHash}}; }

    sub constMagnifyList
        { my $self = shift; return @{$self->{constMagnifyList}}; }
    sub constShortMagnifyList
        { my $self = shift; return @{$self->{constShortMagnifyList}}; }
    sub ignoreMenuUpdateFlag
        { $_[0]->{ignoreMenuUpdateFlag} }

    sub dragModeFlag
        { $_[0]->{dragModeFlag} }
    sub dragFlag
        { $_[0]->{dragFlag} }
    sub dragCanvasObj
        { $_[0]->{dragCanvasObj} }
    sub dragModelObj
        { $_[0]->{dragModelObj} }
    sub dragModelObjType
        { $_[0]->{dragModelObjType} }
    sub dragInitXPos
        { $_[0]->{dragInitXPos} }
    sub dragInitYPos
        { $_[0]->{dragInitYPos} }
    sub dragCurrentXPos
        { $_[0]->{dragCurrentXPos} }
    sub dragCurrentYPos
        { $_[0]->{dragCurrentYPos} }
    sub dragFakeRoomObj
        { $_[0]->{dragFakeRoomObj} }
    sub dragBendNum
        { $_[0]->{dragBendNum} }
    sub dragBendInitXPos
        { $_[0]->{dragBendInitXPos} }
    sub dragBendInitYPos
        { $_[0]->{dragBendInitYPos} }
    sub dragBendTwinNum
        { $_[0]->{dragBendTwinNum} }
    sub dragBendTwinInitXPos
        { $_[0]->{dragBendTwinInitXPos} }
    sub dragBendTwinInitYPos
        { $_[0]->{dragBendTwinInitYPos} }
    sub dragExitDrawMode
        { $_[0]->{dragExitDrawMode} }
}

# Package must return true
1
