nana/source/gui/widgets/menubar.cpp
2017-08-31 02:57:34 +08:00

661 lines
16 KiB
C++

/*
* A Menubar implementation
* Nana C++ Library(http://www.nanapro.org)
* Copyright(C) 2009-2017 Jinhao(cnjinhao@hotmail.com)
*
* Distributed under the Boost Software License, Version 1.0.
* (See accompanying file LICENSE_1_0.txt or copy at
* http://www.boost.org/LICENSE_1_0.txt)
*
* @file: nana/gui/widgets/menubar.cpp
*/
#include <nana/gui/widgets/menubar.hpp>
#include <stdexcept>
namespace nana
{
class menu_accessor
{
public:
static void popup(menu& m, window wd, int x, int y)
{
m._m_popup(wd, { x, y }, true);
}
};
namespace drawerbase
{
namespace menubar
{
struct item_type
{
item_type(const native_string_type& text, unsigned long shortkey)
: text(text), shortkey(shortkey)
{}
native_string_type text;
unsigned long shortkey;
::nana::menu menu_obj;
::nana::point pos;
::nana::size size;
};
class trigger::itembase
{
public:
using container = std::vector<item_type*>;
~itembase()
{
for(auto i : cont_)
delete i;
}
void append(const native_string_type& text, unsigned long shortkey)
{
if(shortkey && shortkey < 0x61) shortkey += (0x61 - 0x41);
cont_.emplace_back(new item_type(text, shortkey));
}
std::size_t find(unsigned long shortkey) const
{
if(shortkey)
{
if(shortkey < 0x61) shortkey += (0x61 - 0x41);
std::size_t index = 0;
for(auto i : cont_)
{
if(i->shortkey == shortkey)
return index;
++index;
}
}
return npos;
}
const container& cont() const
{
return cont_;
}
private:
container cont_;
};
struct trigger::essence
{
widget *widget_ptr{ nullptr };
//paint::graphics *graph{ nullptr };
itembase items;
enum class behavior
{
none, focus, menu,
};
struct state_type
{
std::size_t active{ nana::npos };
behavior behave{ behavior::none };
bool menu_active{ false };
bool passive_close{ true };
bool nullify_mouse{ false };
nana::menu *menu{ nullptr };
nana::point mouse_pos;
}state;
//functions
nana::menu& push_back(const std::string& text)
{
wchar_t shortkey;
API::transform_shortkey_text(text, shortkey, nullptr);
if (shortkey)
API::register_shortkey(this->widget_ptr->handle(), shortkey);
this->items.append(to_nstring(text), shortkey);
API::refresh_window(this->widget_ptr->handle());
return this->items.cont()[this->items.cont().size() - 1]->menu_obj;
}
bool cancel()
{
if (nana::npos == state.active)
return false;
this->close_menu();
state.menu_active = false;
state.behave = behavior::none;
auto pos = API::cursor_position();
API::calc_window_point(widget_ptr->handle(), pos);
state.active = find(pos);
return true;
}
bool open_menu()
{
auto pos = state.active;
if (pos >= items.cont().size())
return false;
auto item_ptr = items.cont()[pos];
if (state.menu_active && (state.menu != &(item_ptr->menu_obj)))
{
API::dev::delay_restore(true);
this->close_menu();
API::dev::delay_restore(false);
state.active = pos;
state.menu = &(item_ptr->menu_obj);
state.menu->destroy_answer([this]
{
state.menu = nullptr;
if (state.passive_close)
{
cancel();
API::refresh_window(*widget_ptr);
}
});
if (API::focus_window() != this->widget_ptr->handle())
API::focus_window(widget_ptr->handle());
menu_accessor::popup(*state.menu, *widget_ptr, item_ptr->pos.x, item_ptr->pos.y + static_cast<int>(item_ptr->size.height));
return true;
}
return false;
}
bool close_menu()
{
if (state.menu)
{
state.passive_close = false;
state.menu->close();
state.passive_close = true;
state.menu = nullptr;
return true;
}
return false;
}
std::size_t find(const ::nana::point& pos)
{
if ((2 <= pos.x) && (2 <= pos.y) && (pos.y < 25))
{
int item_x = 2;
std::size_t index = 0;
for (auto i : items.cont())
{
if (item_x <= pos.x && pos.x < item_x + static_cast<int>(i->size.width))
return index;
item_x += i->size.width;
++index;
}
}
return npos;
}
};
//class item_renderer
item_renderer::item_renderer(window wd, graph_reference graph)
:graph_(graph), scheme_ptr_(static_cast<scheme*>(API::dev::get_scheme(wd)))
{}
void item_renderer::background(const nana::point& pos, const nana::size& size, state item_state)
{
auto bground = scheme_ptr_->text_fgcolor;
::nana::color border, body;
switch (item_state)
{
case state::highlighted:
border = scheme_ptr_->border_highlight;
body = scheme_ptr_->body_highlight;
break;
case state::selected:
border = scheme_ptr_->border_selected;
body = scheme_ptr_->body_selected;
break;
default: //Don't process other states.
return;
}
nana::rectangle r(pos, size);
graph_.rectangle(r, false, border);
graph_.palette(false, body.blend(bground, 0.5));
paint::draw{ graph_ }.corner(r, 1);
graph_.rectangle(r.pare_off(1), true, body);
}
void item_renderer::caption(const point& pos, const native_string_type& text)
{
graph_.string(pos, text, scheme_ptr_->text_fgcolor);
}
//end class item_renderer
//class trigger
trigger::trigger()
: ess_(new essence)
{}
trigger::~trigger()
{
delete ess_;
}
auto trigger::ess() const -> essence&
{
return *ess_;
}
void trigger::attached(widget_reference widget, graph_reference graph)
{
ess_->widget_ptr = &widget;
}
void trigger::refresh(graph_reference graph)
{
auto bgcolor = API::bgcolor(*ess_->widget_ptr);
graph.rectangle(true, bgcolor);
item_renderer ird{ *ess_->widget_ptr, graph };
nana::point item_pos(2, 2);
nana::size item_s(0, 23);
unsigned long index = 0;
for (auto pm : ess_->items.cont())
{
//Transform the text if it contains the hotkey character
wchar_t hotkey;
::std::wstring::size_type hotkey_pos;
auto text = API::transform_shortkey_text(to_utf8(pm->text), hotkey, &hotkey_pos);
nana::size text_s = graph.text_extent_size(text);
item_s.width = text_s.width + 16;
pm->pos = item_pos;
pm->size = item_s;
using state = item_renderer::state;
state item_state = (index != ess_->state.active ? state::normal : (ess_->state.menu_active ? state::selected : state::highlighted));
ird.background(item_pos, item_s, item_state);
if (state::selected == item_state)
{
int x = item_pos.x + item_s.width;
int y1 = item_pos.y + 2, y2 = item_pos.y + item_s.height - 1;
graph.line({ x, y1 }, { x, y2 }, bgcolor.blend(colors::gray_border, 0.6));
graph.line({ x + 1, y1 }, { x + 1, y2 }, bgcolor.blend(colors::button_face_shadow_end, 0.5));
}
//Draw text, the text is transformed from orignal for hotkey character
int text_top_off = (item_s.height - text_s.height) / 2;
ird.caption({ item_pos.x + 8, item_pos.y + text_top_off }, to_nstring(text));
API::dev::draw_shortkey_underline(graph, text, hotkey, hotkey_pos, { item_pos.x + 8, item_pos.y + text_top_off }, ird.scheme_ptr()->text_fgcolor);
item_pos.x += pm->size.width;
++index;
}
}
void trigger::mouse_move(graph_reference graph, const arg_mouse& arg)
{
if (arg.pos != ess_->state.mouse_pos)
ess_->state.nullify_mouse = false;
bool popup = false;
if(essence::behavior::focus == ess_->state.behave)
{
auto index = ess_->find(arg.pos);
if(index != npos && ess_->state.active != index)
{
ess_->state.active = index;
popup = true;
}
}
else if (!ess_->state.nullify_mouse)
{
auto which = ess_->find(arg.pos);
if ((which != ess_->state.active) && (which != npos || (false == ess_->state.menu_active)))
{
ess_->state.active = which;
popup = true;
}
}
if(popup)
{
ess_->open_menu();
refresh(graph);
API::dev::lazy_refresh();
}
ess_->state.mouse_pos = arg.pos;
}
void trigger::mouse_leave(graph_reference graph, const arg_mouse& arg)
{
ess_->state.nullify_mouse = false;
mouse_move(graph, arg);
}
void trigger::mouse_down(graph_reference graph, const arg_mouse& arg)
{
ess_->state.nullify_mouse = false;
ess_->state.active = ess_->find(arg.pos);
if (npos != ess_->state.active)
{
if (!ess_->state.menu_active)
ess_->state.menu_active = true;
ess_->open_menu();
}
else
ess_->cancel();
refresh(graph);
API::dev::lazy_refresh();
}
void trigger::mouse_up(graph_reference graph, const arg_mouse&)
{
ess_->state.nullify_mouse = false;
if(ess_->state.behave != essence::behavior::menu)
{
if(ess_->state.menu_active)
ess_->state.behave = essence::behavior::menu;
}
else
{
ess_->state.behave = essence::behavior::none;
ess_->cancel();
refresh(graph);
API::dev::lazy_refresh();
}
}
void trigger::focus(graph_reference graph, const arg_focus& arg)
{
if((arg.getting == false) && (ess_->state.active != npos))
{
ess_->state.behave = essence::behavior::none;
ess_->state.nullify_mouse = true;
ess_->state.menu_active = false;
ess_->close_menu();
ess_->state.active = npos;
refresh(graph);
API::dev::lazy_refresh();
}
}
void trigger::key_press(graph_reference graph, const arg_keyboard& arg)
{
ess_->state.nullify_mouse = true;
auto & menu_ptr = ess_->state.menu;
if(ess_->state.menu)
{
switch(arg.key)
{
case keyboard::os_arrow_down:
case keyboard::backspace:
case keyboard::os_arrow_up:
menu_ptr->goto_next(keyboard::os_arrow_down == arg.key);
break;
case keyboard::os_arrow_right:
if(menu_ptr->goto_submen() == false)
_m_move(graph, false);
break;
case keyboard::os_arrow_left:
if(menu_ptr->exit_submenu() == false)
_m_move(graph, true);
break;
case keyboard::escape:
if(menu_ptr->exit_submenu() == false)
{
ess_->close_menu();
ess_->state.behave = essence::behavior::focus;
ess_->state.menu_active = false;
}
break;
case keyboard::enter:
menu_ptr->pick();
break;
default:
//Katsuhisa Yuasa: menubar key_press improvements
//send_shortkey has 3 states, 0 = UNKNOWN KEY, 1 = ITEM, 2 = GOTO SUBMENU
int sk_state = menu_ptr->send_shortkey(arg.key);
switch(sk_state)
{
case 0: //UNKNOWN KEY
break;
case 1: //ITEM
if (ess_->state.active != npos)
{
ess_->cancel();
if (arg.key == 18) //ALT
ess_->state.behave = essence::behavior::focus;
}
break;
case 2: //GOTO SUBMENU
menu_ptr->goto_submen();
break;
default: break;
}
break;
}
}
else
{
switch(arg.key)
{
case keyboard::os_arrow_right:
case keyboard::backspace:
case keyboard::os_arrow_left:
_m_move(graph, keyboard::os_arrow_right != arg.key);
break;
case keyboard::os_arrow_up:
case keyboard::os_arrow_down:
case keyboard::enter:
ess_->state.menu_active = true;
if(ess_->open_menu())
menu_ptr->goto_next(true);
break;
case keyboard::escape:
if(essence::behavior::focus == ess_->state.behave)
{
ess_->state.active= npos;
ess_->state.behave = essence::behavior::none;
}
break;
default:
auto index = ess_->items.find(arg.key);
if(index != npos)
{
ess_->state.active = index;
ess_->state.menu_active = true;
if(ess_->open_menu())
menu_ptr->goto_next(true);
}
break;
}
}
refresh(graph);
API::dev::lazy_refresh();
}
void trigger::key_release(graph_reference graph, const arg_keyboard& arg)
{
if(arg.key == 18)
{
if(essence::behavior::none == ess_->state.behave)
{
ess_->state.behave = essence::behavior::focus;
ess_->state.active = 0;
}
else
{
ess_->state.behave = essence::behavior::none;
auto pos = API::cursor_position();
API::calc_window_point(ess_->widget_ptr->handle(), pos);
ess_->state.active = ess_->find(pos);
}
ess_->state.menu_active = false;
refresh(graph);
API::dev::lazy_refresh();
}
}
void trigger::shortkey(graph_reference graph, const arg_keyboard& arg)
{
API::focus_window(ess_->widget_ptr->handle());
auto index = ess_->items.find(arg.key);
if(index != npos && (index != ess_->state.active || nullptr == ess_->state.menu))
{
ess_->close_menu();
ess_->state.menu_active = true;
ess_->state.nullify_mouse = true;
ess_->state.active = index;
if(ess_->open_menu())
ess_->state.menu->goto_next(true);
refresh(graph);
API::dev::lazy_refresh();
ess_->state.behave = essence::behavior::menu;
}
}
void trigger::_m_move(graph_reference graph, bool to_left)
{
if(ess_->items.cont().empty()) return;
const std::size_t last_pos = ess_->items.cont().size() - 1;
auto index = ess_->state.active;
if(to_left)
{
--index;
if (index > last_pos)
index = last_pos;
}
else
{
++index;
if(index > last_pos)
index = 0;
}
if(index != ess_->state.active)
{
ess_->state.active = index;
refresh(graph);
API::dev::lazy_refresh();
if(ess_->open_menu())
ess_->state.menu->goto_next(true);
}
}
//end class trigger
}//end namespace menubar
}//end namespace drawerbase
//class menubar
menubar::menubar(window wd)
{
create(wd);
}
menubar::~menubar()
{
API::umake_event(evt_resized_);
}
void menubar::create(window wd)
{
widget_object<category::widget_tag, drawerbase::menubar::trigger, ::nana::general_events, drawerbase::menubar::scheme>
::create(wd, rectangle(nana::size(API::window_size(wd).width, 28)));
API::dev::set_menubar(handle(), true);
evt_resized_ = API::events(wd).resized.connect_unignorable([this](const ::nana::arg_resized& arg)
{
auto sz = this->size();
sz.width = arg.width;
this->size(sz);
});
}
menu& menubar::push_back(const std::string& text)
{
return get_drawer_trigger().ess().push_back(text);
}
menu& menubar::at(std::size_t pos) const
{
return get_drawer_trigger().ess().items.cont().at(pos)->menu_obj;
}
std::size_t menubar::length() const
{
return get_drawer_trigger().ess().items.cont().size();
}
bool menubar::cancel()
{
return get_drawer_trigger().ess().cancel();
}
bool menubar::hovered() const
{
auto const native_handle = API::root(this->handle());
if (native_handle)
{
auto wd = API::find_window(API::cursor_position());
if (wd == this->handle())
return true;
auto & items = get_drawer_trigger().ess().items.cont();
while (wd)
{
auto owner = API::get_owner_window(wd);
if (API::root(owner) == native_handle)
{
for (auto p : items)
{
if (p->menu_obj.handle() == wd)
return true;
}
}
wd = owner;
}
}
return false;
}
//end class menubar
}//end namespace nana