Merge pull request #2371 from myk002/myk_overlay_hotkeys

[hotkeys] implement hotspot menu widget
develop
Myk 2022-11-14 16:50:30 -08:00 committed by GitHub
commit 35cea1b5e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 386 additions and 307 deletions

@ -12,9 +12,8 @@
keybinding add ` gui/launcher
keybinding add Ctrl-Shift-D gui/launcher
# show all current key bindings
keybinding add Ctrl-F1 hotkeys
keybinding add Alt-F1 hotkeys
# show hotkey popup menu
keybinding add Ctrl-Shift-C hotkeys
# on-screen keyboard
keybinding add Ctrl-Shift-K gui/cp437-table

@ -56,6 +56,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `dwarfmonitor`: widgets have been ported to the overlay framework and can be enabled and configured via the overlay command
- `ls`: indent tag listings and wrap them in the right column for better readability
- `ls`: new ``--exclude`` option for hiding matched scripts from the output. this can be especially useful for modders who don't want their mod scripts to be included in ``ls`` output.
- `hotkeys`: hotkey screen has been transformed into an interactive `overlay` widget that you can bring up by moving the mouse cursor over the hotspot (in the upper left corner of the screen by default)
- `digtype`: new ``-z`` option for digtype to restrict designations to the current z-level and down
- UX: List widgets now have mouse-interactive scrollbars
- UX: You can now hold down the mouse button on a scrollbar to make it scroll multiple times.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

@ -2,7 +2,7 @@ hotkeys
=======
.. dfhack-tool::
:summary: Show all dfhack keybindings for the current context.
:summary: Show all DFHack keybindings for the current context.
:tags: dfhack
The command opens an in-game screen showing which DFHack keybindings are active
@ -11,8 +11,22 @@ in the current context. See also `hotkey-notes`.
Usage
-----
::
``hotkeys``
Show the list of keybindings for the current context in an in-game menu.
``hotkeys list``
List the keybindings to the console.
hotkeys
Menu overlay widget
-------------------
.. image:: ../images/hotkeys.png
The in-game hotkeys menu is registered with the `overlay` framework and can be
enabled as a hotspot in the upper-left corner of the screen. You can bring up
the menu by hovering the mouse cursor over the hotspot and can select a command
to run from the list by clicking on it with the mouse or by using the keyboard
to select a command with the arrow keys and hitting :kbd:`Enter`.
A short description of the command will appear in a nearby textbox. If you'd
like to see the full help text for the command or edit the command before
running, you can open it for editing in `gui/launcher` by right clicking on the
command, left clicking on the arrow to the left of the command, or by pressing
the right arrow key while the command is selected.

@ -1,94 +1,116 @@
#include "uicommon.h"
#include "listcolumn.h"
#include <map>
#include <string>
#include <vector>
#include "df/viewscreen_dwarfmodest.h"
#include "df/ui.h"
#include "modules/Maps.h"
#include "modules/World.h"
#include "modules/Gui.h"
#include "modules/Screen.h"
#include "Debug.h"
#include "LuaTools.h"
#include "PluginManager.h"
DFHACK_PLUGIN("hotkeys");
#define PLUGIN_VERSION 0.1
namespace DFHack {
DBG_DECLARE(hotkeys, log, DebugCategory::LINFO);
}
using std::map;
using std::string;
using std::vector;
using namespace DFHack;
static const string INVOKE_MENU_COMMAND = "overlay trigger hotkeys.menu";
static const string INVOKE_HOTKEYS_COMMAND = "hotkeys";
static const std::string MENU_SCREEN_FOCUS_STRING = "dfhack/lua/hotkeys/menu";
static bool valid = false; // whether the following two vars contain valid data
static string current_focus;
static map<string, string> current_bindings;
static vector<string> sorted_keys;
static bool show_usage = false;
static bool can_invoke(string cmdline, df::viewscreen *screen)
{
static bool can_invoke(const string &cmdline, df::viewscreen *screen) {
vector<string> cmd_parts;
split_string(&cmd_parts, cmdline, " ");
if (toLower(cmd_parts[0]) == "hotkeys")
return false;
return Core::getInstance().getPluginManager()->CanInvokeHotkey(cmd_parts[0], screen);
}
static void add_binding_if_valid(string sym, string cmdline, df::viewscreen *screen)
{
static int cleanupHotkeys(lua_State *) {
DEBUG(log).print("cleaning up old stub keybindings for %s\n", current_focus.c_str());
std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) {
string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING;
DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str());
Core::getInstance().ClearKeyBindings(keyspec);
});
valid = false;
current_focus = "";
sorted_keys.clear();
current_bindings.clear();
return 0;
}
static void add_binding_if_valid(const string &sym, const string &cmdline, df::viewscreen *screen, bool filtermenu) {
if (!can_invoke(cmdline, screen))
return;
if (filtermenu && (cmdline == INVOKE_MENU_COMMAND ||
cmdline == INVOKE_HOTKEYS_COMMAND)) {
DEBUG(log).print("filtering out hotkey menu keybinding\n");
return;
}
current_bindings[sym] = cmdline;
sorted_keys.push_back(sym);
string keyspec = sym + "@dfhack/viewscreen_hotkeys";
Core::getInstance().AddKeyBinding(keyspec, "hotkeys invoke " + int_to_string(sorted_keys.size() - 1));
string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING;
string binding = "hotkeys invoke " + int_to_string(sorted_keys.size() - 1);
DEBUG(log).print("adding keybinding: %s -> %s\n", keyspec.c_str(), binding.c_str());
Core::getInstance().AddKeyBinding(keyspec, binding);
}
static void find_active_keybindings(df::viewscreen *screen)
{
current_bindings.clear();
sorted_keys.clear();
static void find_active_keybindings(df::viewscreen *screen, bool filtermenu) {
DEBUG(log).print("scanning for active keybindings\n");
if (valid)
cleanupHotkeys(NULL);
vector<string> valid_keys;
for (char c = 'A'; c <= 'Z'; c++)
{
for (char c = 'A'; c <= 'Z'; c++) {
valid_keys.push_back(string(&c, 1));
}
for (int i = 1; i < 10; i++)
{
for (int i = 1; i < 10; i++) {
valid_keys.push_back("F" + int_to_string(i));
}
valid_keys.push_back("`");
auto current_focus = Gui::getFocusString(screen);
for (int shifted = 0; shifted < 2; shifted++)
{
for (int ctrl = 0; ctrl < 2; ctrl++)
{
for (int alt = 0; alt < 2; alt++)
{
for (auto it = valid_keys.begin(); it != valid_keys.end(); it++)
{
current_focus = Gui::getFocusString(screen);
for (int shifted = 0; shifted < 2; shifted++) {
for (int alt = 0; alt < 2; alt++) {
for (int ctrl = 0; ctrl < 2; ctrl++) {
for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) {
string sym;
if (shifted) sym += "Shift-";
if (ctrl) sym += "Ctrl-";
if (alt) sym += "Alt-";
if (shifted) sym += "Shift-";
sym += *it;
auto list = Core::getInstance().ListKeyBindings(sym);
for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++)
{
if (invoke_cmd->find(":") == string::npos)
{
add_binding_if_valid(sym, *invoke_cmd, screen);
for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) {
string::size_type colon_pos = invoke_cmd->find(":");
// colons at location 0 are for commands like ":lua"
if (colon_pos == string::npos || colon_pos == 0) {
add_binding_if_valid(sym, *invoke_cmd, screen, filtermenu);
}
else
{
else {
vector<string> tokens;
split_string(&tokens, *invoke_cmd, ":");
string focus = tokens[0].substr(1);
if (prefix_matches(focus, current_focus))
{
if (prefix_matches(focus, current_focus)) {
auto cmdline = trim(tokens[1]);
add_binding_if_valid(sym, cmdline, screen);
add_binding_if_valid(sym, cmdline, screen, filtermenu);
}
}
}
@ -96,288 +118,87 @@ static void find_active_keybindings(df::viewscreen *screen)
}
}
}
}
static bool close_hotkeys_screen()
{
auto screen = Core::getTopViewscreen();
if (Gui::getFocusString(screen) != "dfhack/viewscreen_hotkeys")
return false;
Screen::dismiss(Core::getTopViewscreen());
for_each_(sorted_keys, [] (const string &sym)
{ Core::getInstance().ClearKeyBindings(sym + "@dfhack/viewscreen_hotkeys"); });
sorted_keys.clear();
return true;
valid = true;
}
static void invoke_command(const size_t index)
{
if (sorted_keys.size() <= index)
return;
auto cmd = current_bindings[sorted_keys[index]];
if (close_hotkeys_screen())
{
Core::getInstance().setHotkeyCmd(cmd);
}
static int getHotkeys(lua_State *L) {
find_active_keybindings(Gui::getCurViewscreen(true), true);
Lua::PushVector(L, sorted_keys);
Lua::Push(L, current_bindings);
return 2;
}
static std::string get_help(const std::string &command, bool full_help)
{
auto L = Lua::Core::State;
color_ostream_proxy out(Core::getInstance().getConsole());
Lua::StackUnwinder top(L);
if (!lua_checkstack(L, 2) ||
!Lua::PushModulePublic(out, L, "helpdb",
full_help ? "get_entry_long_help" : "get_entry_short_help"))
return "Help text unavailable.";
Lua::Push(L, command);
DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(getHotkeys),
DFHACK_LUA_COMMAND(cleanupHotkeys),
DFHACK_LUA_END
};
if (!Lua::SafeCall(out, L, 1, 1))
return "Help text unavailable.";
static void list(color_ostream &out) {
DEBUG(log).print("listing active hotkeys\n");
bool was_valid = valid;
if (!valid)
find_active_keybindings(Gui::getCurViewscreen(true), false);
const char *s = lua_tostring(L, -1);
if (!s)
return "Help text unavailable.";
out.print("Valid keybindings for the current screen (%s)\n",
current_focus.c_str());
std::for_each(sorted_keys.begin(), sorted_keys.end(), [&](const string &sym) {
out.print("%s: %s\n", sym.c_str(), current_bindings[sym].c_str());
});
return s;
if (!was_valid)
cleanupHotkeys(NULL);
}
class ViewscreenHotkeys : public dfhack_viewscreen
{
public:
ViewscreenHotkeys(df::viewscreen *top_screen) : top_screen(top_screen)
{
hotkeys_column.multiselect = false;
hotkeys_column.auto_select = true;
hotkeys_column.setTitle("Key Binding");
hotkeys_column.bottom_margin = 4;
hotkeys_column.allow_search = false;
focus = Gui::getFocusString(top_screen);
populateColumns();
}
void populateColumns()
{
hotkeys_column.clear();
size_t max_key_length = 0;
for_each_(sorted_keys, [&] (const string &sym)
{ if (sym.length() > max_key_length) { max_key_length = sym.length(); } });
int padding = max_key_length + 2;
for (size_t i = 0; i < sorted_keys.size(); i++)
{
string text = pad_string(sorted_keys[i], padding, false);
text += current_bindings[sorted_keys[i]];
hotkeys_column.add(text, i+1);
}
help_start = hotkeys_column.fixWidth() + 2;
hotkeys_column.filterDisplay();
}
void feed(set<df::interface_key> *input)
{
if (hotkeys_column.feed(input))
return;
if (input->count(interface_key::LEAVESCREEN))
{
close_hotkeys_screen();
}
else if (input->count(interface_key::SELECT))
{
invoke_command(hotkeys_column.highlighted_index);
}
else if (input->count(interface_key::CUSTOM_U))
{
show_usage = !show_usage;
}
}
void render()
{
if (Screen::isDismissed(this))
return;
dfhack_viewscreen::render();
Screen::clear();
Screen::drawBorder(" Hotkeys ");
hotkeys_column.display(true);
int32_t y = gps->dimy - 3;
int32_t x = 2;
OutputHotkeyString(x, y, "Leave", "Esc");
x += 3;
OutputHotkeyString(x, y, "Invoke", "Enter or Hotkey");
x += 3;
OutputToggleString(x, y, "Show Usage", "u", show_usage);
y = gps->dimy - 4;
x = 2;
OutputHotkeyString(x, y, focus.c_str(), "Context", false, help_start, COLOR_WHITE, COLOR_BROWN);
if (sorted_keys.size() == 0)
return;
y = 2;
x = help_start;
auto width = gps->dimx - help_start - 2;
vector <string> parts;
Core::cheap_tokenise(current_bindings[sorted_keys[hotkeys_column.highlighted_index]], parts);
if(parts.size() == 0)
return;
string first = parts[0];
parts.erase(parts.begin());
if (first[0] == '#')
return;
static bool invoke_command(color_ostream &out, const size_t index) {
auto screen = Core::getTopViewscreen();
if (sorted_keys.size() <= index ||
Gui::getFocusString(screen) != MENU_SCREEN_FOCUS_STRING)
return false;
OutputString(COLOR_BROWN, x, y, "Help", true, help_start);
string help_text = get_help(first, show_usage);
vector <string> lines;
split_string(&lines, help_text, "\n");
for (auto it = lines.begin(); it != lines.end() && y < gps->dimy - 4; it++)
{
auto wrapped_lines = wrapString(*it, width);
for (auto wit = wrapped_lines.begin(); wit != wrapped_lines.end() && y < gps->dimy - 4; wit++)
{
OutputString(COLOR_WHITE, x, y, *wit, true, help_start);
}
}
}
auto cmd = current_bindings[sorted_keys[index]];
DEBUG(log).print("invoking command: '%s'\n", cmd.c_str());
virtual std::string getFocusString()
{
return "viewscreen_hotkeys";
Screen::Hide hideGuard(screen, Screen::Hide::RESTORE_AT_TOP);
Core::getInstance().runCommand(out, cmd);
}
private:
ListColumn<int> hotkeys_column;
df::viewscreen *top_screen;
string focus;
int32_t help_start;
Screen::dismiss(screen);
return true;
}
void resize(int32_t x, int32_t y)
{
dfhack_viewscreen::resize(x, y);
hotkeys_column.resize();
static command_result hotkeys_cmd(color_ostream &out, vector <string> & parameters) {
if (!parameters.size()) {
DEBUG(log).print("invoking command: '%s'\n", INVOKE_MENU_COMMAND.c_str());
return Core::getInstance().runCommand(out, INVOKE_MENU_COMMAND );
}
static vector<string> wrapString(string str, int width)
{
vector<string> result;
string excess;
if (int(str.length()) > width)
{
auto cut_space = str.rfind(' ', width-1);
int excess_start;
if (cut_space == string::npos)
{
cut_space = width-1;
excess_start = cut_space;
}
else
{
excess_start = cut_space + 1;
}
string line = str.substr(0, cut_space);
excess = str.substr(excess_start);
result.push_back(line);
auto excess_lines = wrapString(excess, width);
result.insert(result.end(), excess_lines.begin(), excess_lines.end());
}
else
{
result.push_back(str);
}
return result;
if (parameters[0] == "list") {
list(out);
return CR_OK;
}
};
// internal command -- intentionally undocumented
if (parameters.size() != 2 || parameters[0] != "invoke")
return CR_WRONG_USAGE;
static command_result hotkeys_cmd(color_ostream &out, vector <string> & parameters)
{
if (parameters.empty())
{
if (Maps::IsValid())
{
auto top_screen = Core::getTopViewscreen();
if (Gui::getFocusString(top_screen) != "dfhack/viewscreen_hotkeys")
{
find_active_keybindings(top_screen);
Screen::show(dts::make_unique<ViewscreenHotkeys>(top_screen), plugin_self);
}
}
}
else
{
auto cmd = parameters[0][0];
if (cmd == 'v')
{
out << "Hotkeys" << endl << "Version: " << PLUGIN_VERSION << endl;
}
else if (cmd == 'i')
{
int index;
stringstream index_raw(parameters[1]);
index_raw >> index;
invoke_command(index);
}
else
{
return CR_WRONG_USAGE;
}
}
CoreSuspender guard;
return CR_OK;
int index = string_to_int(parameters[1], -1);
if (index < 0)
return CR_WRONG_USAGE;
return invoke_command(out, index) ? CR_OK : CR_WRONG_USAGE;
}
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
if (!gps)
out.printerr("Could not insert hotkeys hooks!\n");
DFhackCExport command_result plugin_init (color_ostream &out, std::vector <PluginCommand> &commands) {
commands.push_back(
PluginCommand(
"hotkeys",
"Show all dfhack keybindings in current context.",
hotkeys_cmd));
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
case SC_MAP_LOADED:
sorted_keys.clear();
break;
default:
break;
}
"Invoke hotkeys from the interactive menu.",
hotkeys_cmd,
Gui::anywhere_hotkey));
return CR_OK;
}

@ -0,0 +1,244 @@
local _ENV = mkmodule('plugins.hotkeys')
local gui = require('gui')
local helpdb = require('helpdb')
local overlay = require('plugins.overlay')
local widgets = require('gui.widgets')
-- ----------------- --
-- HotspotMenuWidget --
-- ----------------- --
HotspotMenuWidget = defclass(HotspotMenuWidget, overlay.OverlayWidget)
HotspotMenuWidget.ATTRS{
default_pos={x=1,y=3},
hotspot=true,
viewscreens={'dwarfmode'},
overlay_onupdate_max_freq_seconds=0,
frame={w=2, h=1}
}
function HotspotMenuWidget:init()
self:addviews{widgets.Label{text='!!'}}
self.mouseover = false
end
function HotspotMenuWidget:overlay_onupdate()
local hasMouse = self:getMousePos()
if hasMouse and not self.mouseover then
self.mouseover = true
return true
end
self.mouseover = hasMouse
end
function HotspotMenuWidget:overlay_trigger()
local hotkeys, bindings = getHotkeys()
return MenuScreen{
hotspot_frame=self.frame,
hotkeys=hotkeys,
bindings=bindings}:show()
end
-- register the menu hotspot with the overlay
OVERLAY_WIDGETS = {menu=HotspotMenuWidget}
-- ---------- --
-- MenuScreen --
-- ---------- --
local ARROW = string.char(26)
local MAX_LIST_WIDTH = 45
local MAX_LIST_HEIGHT = 15
MenuScreen = defclass(MenuScreen, gui.Screen)
MenuScreen.ATTRS{
focus_path='hotkeys/menu',
hotspot_frame=DEFAULT_NIL,
hotkeys=DEFAULT_NIL,
bindings=DEFAULT_NIL,
}
-- get a map from the binding string to a list of hotkey strings that all
-- point to that binding
local function get_bindings_to_hotkeys(hotkeys, bindings)
local bindings_to_hotkeys = {}
for _,hotkey in ipairs(hotkeys) do
local binding = bindings[hotkey]
table.insert(ensure_key(bindings_to_hotkeys, binding), hotkey)
end
return bindings_to_hotkeys
end
-- number of non-text tiles: icon, space, space between cmd and hk, scrollbar
local LIST_BUFFER = 2 + 1 + 1
local function get_choices(hotkeys, bindings, is_inverted)
local choices, max_width, seen = {}, 0, {}
local bindings_to_hotkeys = get_bindings_to_hotkeys(hotkeys, bindings)
-- build list choices
for _,hotkey in ipairs(hotkeys) do
local command = bindings[hotkey]
if seen[command] then goto continue end
seen[command] = true
local hk_width, tokens = 0, {}
for _,hk in ipairs(bindings_to_hotkeys[command]) do
if hk_width ~= 0 then
table.insert(tokens, ', ')
hk_width = hk_width + 2
end
table.insert(tokens, {text=hk, pen=COLOR_LIGHTGREEN})
hk_width = hk_width + #hk
end
local command_str = command
if hk_width + #command + LIST_BUFFER > MAX_LIST_WIDTH then
local max_command_len = MAX_LIST_WIDTH - hk_width - LIST_BUFFER
command_str = command:sub(1, max_command_len - 3) .. '...'
end
table.insert(tokens, 1, {text=command_str})
local choice = {icon=ARROW, command=command, text=tokens,
hk_width=hk_width}
max_width = math.max(max_width, hk_width + #command_str + LIST_BUFFER)
table.insert(choices, is_inverted and 1 or #choices + 1, choice)
::continue::
end
-- adjust width of command fields so the hotkey tokens are right justified
for _,choice in ipairs(choices) do
local command_token = choice.text[1]
command_token.width = max_width - choice.hk_width - 3
end
return choices, max_width
end
function MenuScreen:init()
self.mouseover = false
local choices,list_width = get_choices(self.hotkeys, self.bindings,
self.hotspot_frame.b)
local list_frame = copyall(self.hotspot_frame)
list_frame.w = list_width + 2
list_frame.h = math.min(#choices, MAX_LIST_HEIGHT) + 2
if list_frame.t then
list_frame.t = math.max(0, list_frame.t - 1)
else
list_frame.b = math.max(0, list_frame.b - 1)
end
if list_frame.l then
list_frame.l = math.max(0, list_frame.l - 1)
else
list_frame.r = math.max(0, list_frame.r - 1)
end
local help_frame = {w=list_frame.w, l=list_frame.l, r=list_frame.r}
if list_frame.t then
help_frame.t = list_frame.t + list_frame.h + 1
else
help_frame.b = list_frame.b + list_frame.h + 1
end
self:addviews{
widgets.ResizingPanel{
view_id='list_panel',
autoarrange_subviews=true,
frame=list_frame,
frame_style=gui.GREY_LINE_FRAME,
frame_background=gui.CLEAR_PEN,
subviews={
widgets.List{
view_id='list',
choices=choices,
icon_width=2,
on_select=self:callback('onSelect'),
on_submit=self:callback('onSubmit'),
on_submit2=self:callback('onSubmit2'),
},
},
},
widgets.ResizingPanel{
view_id='help_panel',
autoarrange_subviews=true,
frame=help_frame,
frame_style=gui.GREY_LINE_FRAME,
frame_background=gui.CLEAR_PEN,
subviews={
widgets.WrappedLabel{
view_id='help',
text_to_wrap='',
scroll_keys={},
},
},
},
}
end
function MenuScreen:onDismiss()
cleanupHotkeys()
end
function MenuScreen:onSelect(_, choice)
if not choice or #self.subviews == 0 then return end
local first_word = choice.command:trim():split(' +')[1]
if first_word:startswith(':') then first_word = first_word:sub(2) end
self.subviews.help.text_to_wrap = helpdb.is_entry(first_word) and
helpdb.get_entry_short_help(first_word) or 'Command not found'
self.subviews.help_panel:updateLayout()
end
function MenuScreen:onSubmit(_, choice)
if not choice then return end
dfhack.screen.hideGuard(self, dfhack.run_command, choice.command)
self:dismiss()
end
function MenuScreen:onSubmit2(_, choice)
if not choice then return end
self:dismiss()
dfhack.run_script('gui/launcher', choice.command)
end
function MenuScreen:onInput(keys)
if keys.LEAVESCREEN then
self:dismiss()
return true
elseif keys.STANDARDSCROLL_RIGHT then
self:onSubmit2(self.subviews.list:getSelected())
return true
elseif keys._MOUSE_L then
local list = self.subviews.list
local x = list:getMousePos()
if x == 0 then -- clicked on icon
self:onSubmit2(list:getSelected())
return true
end
end
return self:inputToSubviews(keys)
end
function MenuScreen:onRenderFrame(dc, rect)
self:renderParent()
end
function MenuScreen:onRenderBody(dc)
local panel = self.subviews.list_panel
local list = self.subviews.list
local idx = list:getIdxUnderMouse()
if idx and idx ~= self.last_mouse_idx then
-- focus follows mouse, but if cursor keys were used to change the
-- selection, don't override the selection until the mouse moves to
-- another item
list:setSelected(idx)
self.mouseover = true
self.last_mouse_idx = idx
elseif not panel:getMousePos(gui.ViewRect{rect=panel.frame_rect})
and self.mouseover then
-- once the mouse has entered the list area, leaving the frame should
-- close the menu screen
self:dismiss()
end
end
return _ENV