/* * A Categorize Implementation * Nana C++ Library(http://www.nanapro.org) * Copyright(C) 2003-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/categorize.cpp */ #include #include #include #include #include #include namespace nana { namespace drawerbase { namespace categorize { struct event_agent_holder { std::function selected; }; struct item : public float_listbox::item_interface { nana::paint::image item_image; std::string item_text; public: item(const std::string& s) : item_text(s) {} public: //Implement item_interface methods const nana::paint::image& image() const override { return item_image; } const char * text() const override { return item_text.data(); } }; struct item_tag { nana::size scale; unsigned pixels; nana::any value; }; //class renderer renderer::ui_element::ui_element() : what(none), index(0) {} renderer::~renderer(){} //end class renderer //interior_renderer class interior_renderer : public renderer { private: void background(graph_reference graph, window wd, const nana::rectangle& r, const ui_element& ue) { ui_el_ = ue; style_.bgcolor = API::bgcolor(wd); style_.fgcolor = API::fgcolor(wd); if(ue.what == ue.none || (API::window_enabled(wd) == false)) { //the mouse is out of the widget. style_.bgcolor = style_.bgcolor.blend(static_cast(0xa0c9f5), 0.1); } graph.rectangle(r, true, style_.bgcolor); } virtual void root_arrow(graph_reference graph, const nana::rectangle& r, mouse_action state) { ::nana::rectangle arrow_r{r.x + static_cast(r.width - 16) / 2, r.y + static_cast(r.height - 16) / 2, 16, 16}; if(ui_el_.what == ui_el_.item_root) { _m_item_bground(graph, r.x + 1, r.y, r.width - 2, r.height, (state == mouse_action::pressed ? mouse_action::pressed : mouse_action::hovered)); graph.rectangle(r, false, static_cast(0x3C7FB1)); if(state == mouse_action::pressed) { ++arrow_r.x; ++arrow_r.y; } } else graph.rectangle(r, true, style_.bgcolor); facade arrow("double"); arrow.direction(::nana::direction::west); arrow.draw(graph, {}, style_.fgcolor, arrow_r, element_state::normal); } void item(graph_reference graph, const nana::rectangle& r, std::size_t index, const std::string& name_utf8, unsigned txtheight, bool has_child, mouse_action state) { nana::point strpos(r.x + 5, r.y + static_cast(r.height - txtheight) / 2); if((ui_el_.what == ui_el_.item_arrow || ui_el_.what == ui_el_.item_name) && (ui_el_.index == index)) { mouse_action state_arrow, state_name; if(mouse_action::pressed != state) { state_arrow = (ui_el_.what == ui_el_.item_arrow ? mouse_action::hovered : mouse_action::normal); state_name = (ui_el_.what == ui_el_.item_name ? mouse_action::hovered : mouse_action::normal); } else { state_name = state_arrow = mouse_action::pressed; ++strpos.x; ++strpos.y; } int top = r.y + 1; unsigned width = r.width - 2; unsigned height = r.height - 2; ::nana::color clr{static_cast(0x3C7FB1)}; if(has_child) { width -= 16; int left = r.x + r.width - 17; _m_item_bground(graph, left + 1, top, 15, height, state_arrow); graph.line({ left, top }, { left, r.y + static_cast(height) }, clr); } _m_item_bground(graph, r.x + 1, top, width, height, state_name); graph.rectangle(r, false, clr); } graph.string(strpos, name_utf8, style_.fgcolor); if(has_child) { facade arrow("double"); arrow.direction(::nana::direction::east); arrow.draw(graph, {}, style_.fgcolor, { r.right() - 16, r.y + static_cast(r.height - 16) / 2, 16, 16 }, element_state::normal); } } void border(graph_reference graph) { rectangle r{ graph.size() }; graph.rectangle(r, false, static_cast(0xf0f0f0)); color lb(static_cast(0x9dabb9)); color tr(static_cast(0x484e55)); graph.frame_rectangle(r.pare_off(1), lb, tr, tr, lb); } private: void _m_item_bground(graph_reference graph, int x, int y, unsigned width, unsigned height, mouse_action state) { const unsigned half = (height - 2) / 2; int left = x + 1; int top = y + 1; nana::color clr_top(static_cast(0xEAEAEA)), clr_bottom(static_cast(0xDCDCDC)); switch(state) { case mouse_action::hovered: clr_top.from_rgb(0xdf, 0xf2, 0xfc); clr_bottom.from_rgb(0xa9, 0xda, 0xf5); break; case mouse_action::pressed: clr_top.from_rgb(0xa6, 0xd7, 0xf2); clr_bottom.from_rgb(0x92, 0xc4, 0xf6); ++left; ++top; break; default: break; } graph.rectangle(rectangle{ left, top, width - 2, half }, true, clr_top); graph.rectangle(rectangle{ left, top + static_cast(half), width - 2, (height - 2) - half }, true, clr_bottom); if(mouse_action::pressed == state) { int bottom = y + height - 1; int right = x + width - 1; graph.palette(false, static_cast(0x6E8D9F)); graph.line(point{ x, y }, point{ right, y }); graph.line(point{ x, y + 1 }, point{ x, bottom }); ++x; ++y; graph.palette(false, static_cast(0xa6c7d9)); graph.line(point{ x, y }, point{ right, y }); graph.line(point{ x, y + 1 }, point{ x, bottom }); } } private: ui_element ui_el_; struct style_tag { color bgcolor; color fgcolor; }style_; }; class tree_wrapper { public: using container = widgets::detail::tree_cont; using node_handle = container::node_type*; tree_wrapper() : splitstr_("\\") {} bool seq(std::size_t index, std::vector & seqv) const { _m_read_node_path(seqv); if(index < seqv.size()) { if(index) seqv.erase(seqv.begin(), seqv.begin() + index); return true; } return false; } void splitstr(const std::string& ss) { if(ss.size()) splitstr_ = ss; } const std::string& splitstr() const { return splitstr_; } std::string path() const { std::vector v; _m_read_node_path(v); std::string str; bool not_head = false; for(auto i : v) { if(not_head) str += splitstr_; else not_head = true; str += i->value.first; } return str; } void path(const std::string& key) { cur_ = tree_.ref(key); } node_handle at(std::size_t pos) const { std::vector v; _m_read_node_path(v); return (pos < v.size() ? v[pos] : nullptr); } node_handle tail(std::size_t index) { auto node = at(index); if(node) cur_ = node; return node; } node_handle cur() const { return cur_; } void cur(node_handle node) { cur_ = node; } void insert(const std::string& name, const nana::any& value) { item_tag m; m.pixels = 0; m.value = value; cur_ = tree_.insert(cur_, name, m); } bool childset(const std::string& name, const nana::any& value) { if(cur_) { auto cur_before_insert = cur_; insert(name, value); cur_ = cur_before_insert; return true; } return false; } bool childset_erase(const std::string& name) { auto node = find_child(name); if (!node) return false; tree_.remove(node); return true; } node_handle find_child(const std::string& name) const { if(cur_) { for(node_handle i = cur_->child; i; i = i->next) { if(i->value.first == name) return i; } } return nullptr; } bool clear() { if(tree_.get_root()->child) { tree_.clear(tree_.get_root()); return true; } return false; } private: void _m_read_node_path(std::vector& nodes) const { auto root = tree_.get_root(); for(auto i = cur_; i && (i != root); i = i->owner) nodes.insert(nodes.begin(), i); } private: container tree_; std::string splitstr_ ; node_handle cur_{ nullptr }; }; //class scheme class trigger::scheme { public: typedef tree_wrapper container; typedef container::node_handle node_handle; typedef renderer::ui_element ui_element; enum class mode { normal, floatlist }; scheme() { proto_.ui_renderer = pat::cloneable(interior_renderer()); style_.mode = mode::normal; style_.listbox = nullptr; } void attach(window wd) { window_ = wd; API::bgcolor(wd, colors::white); } void detach() { window_ = nullptr; } window window_handle() const { return window_; } container& tree() { return treebase_; } void draw(graph_reference graph) { _m_calc_scale(graph); nana::rectangle r = _m_make_rectangle(); //_m_make_rectangle must be called after _m_calc_scale() _m_calc_pixels(r); proto_.ui_renderer->background(graph, window_, r, ui_el_); if(head_) proto_.ui_renderer->root_arrow(graph, _m_make_root_rectangle(), style_.state); _m_draw_items(graph, r); proto_.ui_renderer->border(graph); } bool locate(int x, int y) const { if(head_) { auto r = _m_make_root_rectangle(); if (r.is_hit(x, y)) { style_.active_item_rectangle = r; if(ui_el_.what == ui_el_.item_root) return false; ui_el_.what = ui_el_.item_root; return true; } } nana::rectangle r = _m_make_rectangle(); std::vector seq; if(r.is_hit(x, y) && treebase_.seq(head_, seq)) { const int xbase = r.x; const int xend = r.right(); //Change the meaning of variable r. Now, r indicates the area of a item r.height = item_height_; std::size_t seq_index = 0; for(auto i : seq) { r.width = i->value.second.pixels; //If the item is over the right border of widget, the item would be painted at //the begining of the next line. if(r.right() > xend) { r.x = xbase; r.y += r.height; } if(r.is_hit(x, y)) { style_.active_item_rectangle = r; std::size_t index = seq_index + head_; ui_element::t what = ((i->child && (r.right() - 16 < x)) ? ui_el_.item_arrow : ui_el_.item_name); if(what == ui_el_.what && index == ui_el_.index) return false; ui_el_.what = what; ui_el_.index = index; return true; } r.x += r.width; ++seq_index; } } if(ui_el_.what == ui_el_.somewhere) return false; ui_el_.what = ui_el_.somewhere; return true; } bool erase_locate() { ui_el_.index = npos; if(ui_el_.what == ui_el_.none) return false; ui_el_.what = ui_el_.none; return true; } const ui_element& locate() const { return ui_el_; } void mouse_pressed() { style_.state = mouse_action::pressed; //Check the click whether to show the list if (ui_element::item_root == ui_el_.what || ui_element::item_arrow == ui_el_.what) { _m_show_list(); style_.mode = mode::floatlist; } } void mouse_release() { if(style_.mode != mode::floatlist) { style_.state = mouse_action::normal; if (ui_element::item_name == ui_el_.what) _m_selected(treebase_.tail(ui_el_.index)); } } bool is_list_shown() const { return (nullptr != style_.listbox); } event_agent_holder& evt_holder() const { return evt_holder_; } private: void _m_selected(node_handle node) { if(node) { API::dev::window_caption(window_handle(), to_nstring(tree().path())); if(evt_holder_.selected) evt_holder_.selected(node->value.second.value); } } void _m_show_list() { if(style_.listbox) style_.listbox->close(); style_.module.items.clear(); nana::rectangle r; style_.list_trigger = ui_el_.what; if(ui_el_.what == ui_el_.item_arrow) { style_.active = ui_el_.index; node_handle i = treebase_.at(ui_el_.index); if(i) { for(node_handle child = i->child; child; child = child->next) style_.module.items.emplace_back(std::make_shared(child->value.first)); } r = style_.active_item_rectangle; } else if(ui_el_.item_root == ui_el_.what) { std::vector v; if(treebase_.seq(0, v)) { auto end = v.cbegin() + head_; for(auto i = v.cbegin(); i != end; ++i) style_.module.items.emplace_back(std::make_shared((*i)->value.first)); } r = style_.active_item_rectangle; } r.y += r.height; r.width = r.height = 100; style_.listbox = &(form_loader()(window_, r, true)); style_.listbox->set_module(style_.module, 16); style_.listbox->events().destroy.connect_unignorable([this](const arg_destroy&) { //Close list when listbox is destoryed style_.mode = mode::normal; style_.state = mouse_action::normal; if ((style_.module.index != npos) && style_.module.have_selected) { node_handle node = nullptr; if (ui_element::item_arrow == style_.list_trigger) { treebase_.tail(style_.active); node = treebase_.find_child(style_.module.items[style_.module.index]->text()); if (!node) { style_.listbox = nullptr; return; } treebase_.cur(node); } else if (ui_element::item_root != style_.list_trigger) { style_.listbox = nullptr; return; } else node = treebase_.tail(style_.module.index); _m_selected(node); } API::refresh_window(window_); API::update_window(window_); style_.listbox = nullptr; }); } private: unsigned _m_item_fix_scale() const { return (API::window_size(this->window_handle()).height - 2); } nana::rectangle _m_make_root_rectangle() const { return{ 1, 1, 16, _m_item_fix_scale() }; } //_m_make_rectangle //@brief: This function calculate the items area. This must be called after _m_calc_scale() nana::rectangle _m_make_rectangle() const { auto dimension = API::window_size(this->window_handle()); nana::rectangle r(1, 1, dimension.width - 2, _m_item_fix_scale()); unsigned px = r.width; std::size_t lines = item_lines_; std::vector v; treebase_.seq(0, v); for(auto node : v) { if(node->value.second.scale.width > px) { if(lines > 1) { --lines; px = r.width; if(px < node->value.second.scale.width) { --lines; continue; } } else { //Too many items, so some of items cann't be displayed r.x += 16; r.width -= 16; return r; } } px -= node->value.second.scale.width; } return r; } void _m_calc_scale(graph_reference graph) { nana::size tsz; unsigned highest = 0; std::vector v; treebase_.seq(0, v); for(auto node : v) { node->value.second.scale = graph.text_extent_size(node->value.first); if(highest < node->value.second.scale.height) highest = node->value.second.scale.height; node->value.second.scale.width += (node->child ? 26 : 10); } highest += 6; //the default height of item. auto fixed_scale_px = _m_item_fix_scale(); item_lines_ = fixed_scale_px / highest; if(item_lines_ == 0) item_lines_ = 1; item_height_ = (1 != item_lines_ ? highest : fixed_scale_px); } void _m_calc_pixels(const nana::rectangle& r) { std::size_t lines = item_lines_; unsigned px = 0; head_ = 0; std::vector v; treebase_.seq(0, v); for(auto vi = v.rbegin(); vi != v.rend(); ++vi) { item_tag & m = (*vi)->value.second; if(r.width >= px + m.scale.width) { px += m.scale.width; m.pixels = m.scale.width; continue; } //In fact, this item must be in the font of a line. m.pixels = (r.width >= m.scale.width ? m.scale.width : _m_minimial_pixels()); if(0 == px) //This line is empty, NOT a newline { px = m.pixels; continue; } //Newline, and check here whether is more lines. if(0 == --lines) { head_ = std::distance(vi, v.rend()); break; } px = m.pixels; } } static unsigned _m_minimial_pixels() { return 46; } void _m_draw_items(graph_reference graph, const nana::rectangle& r) { nana::rectangle item_r = r; item_r.height = item_height_; std::size_t index = head_; const int xend = static_cast(r.width) + r.x; std::vector v; treebase_.seq(0, v); for(auto vi = v.begin() + head_; vi != v.end(); ++vi) { node_handle i = (*vi); if(static_cast(i->value.second.pixels) + item_r.x > xend) { item_r.x = r.x; item_r.y += item_height_; } item_r.width = i->value.second.pixels; proto_.ui_renderer->item(graph, item_r, index++, i->value.first, i->value.second.scale.height, i->child != 0, style_.state); item_r.x += item_r.width; } } private: window window_{nullptr}; std::size_t head_; unsigned item_height_; std::size_t item_lines_; container treebase_; mutable ui_element ui_el_; struct style_tag { ui_element::t list_trigger; std::size_t active; //It indicates the item corresponding listbox. mutable ::nana::rectangle active_item_rectangle; ::nana::float_listbox::module_type module; ::nana::float_listbox * listbox; scheme::mode mode; mouse_action state; //The state of mouse }style_; struct proto_tag { pat::cloneable ui_renderer; }proto_; mutable event_agent_holder evt_holder_; }; //class trigger trigger::trigger() : scheme_(new scheme) {} trigger::~trigger() { delete scheme_; } void trigger::insert(const std::string& str, nana::any value) { throw_not_utf8(str); scheme_->tree().insert(str, value); API::dev::window_caption(scheme_->window_handle(), to_nstring(scheme_->tree().path())); API::refresh_window(this->scheme_->window_handle()); } bool trigger::childset(const std::string& str, nana::any value) { if(scheme_->tree().childset(str, value)) { API::refresh_window(this->scheme_->window_handle()); return true; } return false; } bool trigger::childset_erase(const std::string& str) { if(scheme_->tree().childset_erase(str)) { API::refresh_window(this->scheme_->window_handle()); return true; } return false; } bool trigger::clear() { if(scheme_->tree().clear()) { API::refresh_window(this->scheme_->window_handle()); return true; } return false; } void trigger::splitstr(const std::string& sstr) { scheme_->tree().splitstr(sstr); } const std::string& trigger::splitstr() const { return scheme_->tree().splitstr(); } void trigger::path(const std::string& str) { scheme_->tree().path(str); } std::string trigger::path() const { return scheme_->tree().path(); } nana::any& trigger::value() const { auto node = scheme_->tree().cur(); if(node) return node->value.second.value; throw std::runtime_error("nana::categorize::value, current category is empty"); } void trigger::_m_event_agent_ready() const { auto evt_agent = event_agent_.get(); scheme_->evt_holder().selected = [evt_agent](::nana::any& val){ evt_agent->selected(val); }; } void trigger::attached(widget_reference widget, graph_reference) { scheme_->attach(widget); } void trigger::detached() { scheme_->detach(); } void trigger::refresh(graph_reference graph) { scheme_->draw(graph); } void trigger::mouse_down(graph_reference graph, const arg_mouse&) { if(scheme_->locate().what > ui_element::somewhere) { if(API::window_enabled(scheme_->window_handle())) { scheme_->mouse_pressed(); scheme_->draw(graph); API::dev::lazy_refresh(); } } } void trigger::mouse_up(graph_reference graph, const arg_mouse&) { if(scheme_->locate().what > ui_element::somewhere) { if(API::window_enabled(scheme_->window_handle())) { scheme_->mouse_release(); scheme_->draw(graph); API::dev::lazy_refresh(); } } } void trigger::mouse_move(graph_reference graph, const arg_mouse& arg) { if(scheme_->locate(arg.pos.x, arg.pos.y) && API::window_enabled(scheme_->window_handle())) { scheme_->draw(graph); API::dev::lazy_refresh(); } } void trigger::mouse_leave(graph_reference graph, const arg_mouse&) { if(API::window_enabled(scheme_->window_handle()) && (scheme_->is_list_shown() == false) && scheme_->erase_locate()) { scheme_->draw(graph); API::dev::lazy_refresh(); } } //end class trigger }//end namespace categorize }//end namespace draerbase }//end namespace nana