6102 lines
164 KiB
C++
6102 lines
164 KiB
C++
/*
|
|
* A List Box Implementation
|
|
* Nana C++ Library(http://www.nanapro.org)
|
|
* Copyright(C) 2003-2018 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/listbox.cpp
|
|
* @contributors:
|
|
* Hiroshi Seki
|
|
* Ariel Vina-Rodriguez
|
|
* leobackes(pr#86,pr#97)
|
|
* Benjamin Navarro(pr#81)
|
|
* besh81(pr#130)
|
|
* dankan1890(pr#158)
|
|
*
|
|
*/
|
|
|
|
#include <nana/gui/widgets/listbox.hpp>
|
|
#include <nana/gui/widgets/panel.hpp> //for inline widget
|
|
|
|
#include <nana/gui/layout_utility.hpp>
|
|
#include <nana/gui/element.hpp>
|
|
#include <nana/paint/text_renderer.hpp>
|
|
#include <nana/system/dataexch.hpp>
|
|
#include <nana/system/platform.hpp>
|
|
#include "skeletons/content_view.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <list>
|
|
#include <deque>
|
|
#include <stdexcept>
|
|
#include <map>
|
|
|
|
namespace nana
|
|
{
|
|
static void check_range(std::size_t pos, std::size_t size)
|
|
{
|
|
if (!(pos < size))
|
|
throw std::out_of_range("listbox: invalid element position");
|
|
}
|
|
|
|
namespace drawerbase
|
|
{
|
|
namespace listbox
|
|
{
|
|
class model_lock_guard
|
|
{
|
|
model_lock_guard(const model_lock_guard&) = delete;
|
|
model_lock_guard& operator=(const model_lock_guard&) = delete;
|
|
public:
|
|
model_lock_guard(model_interface* model)
|
|
: model_ptr_(model)
|
|
{
|
|
if (model)
|
|
model->lock();
|
|
}
|
|
|
|
~model_lock_guard() noexcept
|
|
{
|
|
if (model_ptr_)
|
|
model_ptr_->unlock();
|
|
}
|
|
private:
|
|
model_interface* const model_ptr_;
|
|
};
|
|
|
|
|
|
//struct cell
|
|
cell::format::format(const ::nana::color& bgcolor, const ::nana::color& fgcolor) noexcept
|
|
: bgcolor{ bgcolor }, fgcolor{ fgcolor }
|
|
{}
|
|
|
|
cell::cell(const cell& rhs)
|
|
: text(rhs.text),
|
|
custom_format{ rhs.custom_format ? new format(*rhs.custom_format) : nullptr }
|
|
{}
|
|
|
|
//A workaround that VS2013 does not support to define an explicit default move constructor
|
|
cell::cell(cell&& other) noexcept
|
|
: text(std::move(other.text)),
|
|
custom_format{ std::move(other.custom_format) }
|
|
{
|
|
}
|
|
|
|
cell::cell(std::string text) noexcept
|
|
: text(std::move(text))
|
|
{}
|
|
|
|
cell::cell(std::string text, const format& fmt)
|
|
: text(std::move(text)),
|
|
custom_format(std::make_unique<format>( fmt ))
|
|
{}
|
|
|
|
cell& cell::operator=(const cell& rhs)
|
|
{
|
|
if (this != &rhs)
|
|
{
|
|
text = rhs.text;
|
|
custom_format.reset(rhs.custom_format ? new format{*rhs.custom_format} : nullptr);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
cell& cell::operator=(cell&& other) noexcept
|
|
{
|
|
if (this != &other)
|
|
{
|
|
text = std::move(other.text);
|
|
custom_format = std::move(other.custom_format);
|
|
}
|
|
return *this;
|
|
}
|
|
//end struct cell
|
|
|
|
// Essence of the columns Header
|
|
class es_header
|
|
{
|
|
public:
|
|
struct attributes
|
|
{
|
|
bool movable{true};
|
|
bool resizable{true};
|
|
bool sortable{true};
|
|
bool visible{true};
|
|
};
|
|
|
|
struct column
|
|
: public column_interface
|
|
{
|
|
native_string_type caption;
|
|
unsigned width_px;
|
|
std::pair<unsigned, unsigned> range_width_px;
|
|
bool visible_state{ true };
|
|
|
|
/// Absolute position of column when it was creating
|
|
size_type index;
|
|
|
|
nana::align alignment{ nana::align::left };
|
|
|
|
std::function<bool(const std::string&, nana::any*, const std::string&, nana::any*, bool reverse)> weak_ordering;
|
|
|
|
std::shared_ptr<paint::font> font; ///< The exclusive column font
|
|
|
|
column() = default;
|
|
|
|
column(const column&) = default;
|
|
|
|
column& operator=(const column& other)
|
|
{
|
|
if (this != &other)
|
|
{
|
|
caption = other.caption;
|
|
width_px = other.width_px;
|
|
range_width_px = other.range_width_px;
|
|
visible_state = other.visible_state;
|
|
index = other.index;
|
|
alignment = other.alignment;
|
|
weak_ordering = other.weak_ordering;
|
|
font = other.font;
|
|
}
|
|
return *this;
|
|
|
|
}
|
|
|
|
column(column&& other):
|
|
caption(std::move(other.caption)),
|
|
width_px(other.width_px),
|
|
range_width_px(other.range_width_px),
|
|
visible_state(other.visible_state),
|
|
index(other.index),
|
|
alignment(other.alignment),
|
|
weak_ordering(std::move(other.weak_ordering)),
|
|
font(std::move(other.font)),
|
|
ess_(other.ess_)
|
|
{
|
|
}
|
|
|
|
column(essence* ess, native_string_type&& text, unsigned px, size_type pos) noexcept :
|
|
caption(std::move(text)),
|
|
width_px(px),
|
|
index(pos),
|
|
ess_(ess)
|
|
{
|
|
}
|
|
private:
|
|
//The definition is provided after essence
|
|
void _m_refresh() noexcept;
|
|
private:
|
|
essence* const ess_;
|
|
public:
|
|
//Implementation of column_interface
|
|
unsigned width() const noexcept override
|
|
{
|
|
return width_px;
|
|
}
|
|
|
|
// Sets the width and overrides the ranged width
|
|
void width(unsigned pixels) noexcept override
|
|
{
|
|
width_px = pixels;
|
|
range_width_px.first = range_width_px.second = 0;
|
|
|
|
_m_refresh();
|
|
}
|
|
|
|
void width(unsigned minimum, unsigned maximum) override
|
|
{
|
|
//maximum must be larger than minimum, but maximum == 0 is allowed if minimum is 0
|
|
if ((minimum >= maximum) && (minimum != 0))
|
|
throw std::invalid_argument("listbox.column.width() minimum must be less than maximum");
|
|
|
|
range_width_px.first = minimum;
|
|
range_width_px.second = maximum;
|
|
|
|
unsigned px = std::clamp(static_cast<int>(width_px), static_cast<int>(minimum), static_cast<int>(maximum));
|
|
if (width_px != px)
|
|
{
|
|
width_px = px;
|
|
_m_refresh();
|
|
}
|
|
}
|
|
|
|
size_type position(bool disp_order) const noexcept override; //The definition is provided after essence
|
|
|
|
std::string text() const noexcept override
|
|
{
|
|
return to_utf8(caption);
|
|
}
|
|
|
|
void text(std::string text_utf8) override
|
|
{
|
|
caption = to_nstring(std::move(text_utf8));
|
|
_m_refresh();
|
|
}
|
|
|
|
void text_align(::nana::align align) noexcept override
|
|
{
|
|
if (alignment != align)
|
|
{
|
|
alignment = align;
|
|
_m_refresh();
|
|
}
|
|
}
|
|
|
|
//Definition is provided after essence
|
|
void fit_content(unsigned maximize = 100000) noexcept override;
|
|
|
|
/// Sets an exclusive font for the column
|
|
void typeface(const paint::font& column_font) override;
|
|
|
|
/// Returns a font
|
|
paint::font typeface() const noexcept override;
|
|
|
|
bool visible() const noexcept override
|
|
{
|
|
return visible_state;
|
|
}
|
|
|
|
void visible(bool is_visible) noexcept override
|
|
{
|
|
visible_state = is_visible;
|
|
_m_refresh();
|
|
}
|
|
};
|
|
public:
|
|
using container = std::vector<column>;
|
|
|
|
export_options::columns_indexs get_headers(bool only_visibles) const
|
|
{
|
|
export_options::columns_indexs idx;
|
|
for(const auto &col : cont_)
|
|
{
|
|
if(col.visible_state || !only_visibles)
|
|
idx.push_back(col.index);
|
|
}
|
|
return idx;
|
|
}
|
|
|
|
std::string to_string(const export_options& exp_opt) const
|
|
{
|
|
std::string head_str;
|
|
bool first{true};
|
|
for( size_type idx{}; idx<exp_opt.columns_order.size(); ++idx)
|
|
{
|
|
if(first)
|
|
first=false;
|
|
else
|
|
head_str += exp_opt.sep;
|
|
head_str += this->at(exp_opt.columns_order[idx]).text();
|
|
}
|
|
return head_str;
|
|
}
|
|
|
|
const attributes& attrib() const noexcept
|
|
{
|
|
return attrib_;
|
|
}
|
|
|
|
attributes& attrib() noexcept
|
|
{
|
|
return attrib_;
|
|
}
|
|
|
|
size_type create(essence* ess, native_string_type&& text, unsigned pixels)
|
|
{
|
|
#ifdef _nana_std_has_emplace_return_type
|
|
return cont_.emplace_back(ess, std::move(text), pixels, static_cast<size_type>(cont_.size())).index;
|
|
#else
|
|
cont_.emplace_back(ess, std::move(text), pixels, static_cast<size_type>(cont_.size()));
|
|
return cont_.back().index;
|
|
#endif
|
|
}
|
|
|
|
void clear()
|
|
{
|
|
cont_.clear();
|
|
}
|
|
|
|
unsigned width_px() const noexcept ///< the visible width of the whole header
|
|
{
|
|
unsigned pixels = 0;
|
|
for(auto & col : cont_)
|
|
{
|
|
if (col.visible_state)
|
|
pixels += col.width_px;
|
|
}
|
|
return pixels;
|
|
}
|
|
|
|
/// Calculates the ranged columns to make the whole header fit a specified width
|
|
/**
|
|
* @param width The width to be fittd
|
|
* @return true if the ranged columns is adjusted for the width, false otherwise.
|
|
*/
|
|
bool calc_ranged_columns(unsigned width) noexcept
|
|
{
|
|
unsigned fixed_px = 0;
|
|
unsigned minimal_px = 0;
|
|
unsigned maximal_px = 0;
|
|
|
|
unsigned ranged_px = 0;
|
|
unsigned ranged_count = 0;
|
|
|
|
auto const & const_cont = cont_;
|
|
for (auto & col : const_cont)
|
|
{
|
|
if (col.visible_state)
|
|
{
|
|
if (col.range_width_px.first == col.range_width_px.second)
|
|
{
|
|
fixed_px += col.width_px;
|
|
continue;
|
|
}
|
|
|
|
minimal_px += col.range_width_px.first;
|
|
maximal_px += col.range_width_px.second;
|
|
|
|
ranged_px += col.width_px;
|
|
++ranged_count;
|
|
}
|
|
}
|
|
|
|
// Don't calculate because the header fits the width
|
|
if (ranged_px + fixed_px == width)
|
|
return true;
|
|
|
|
//Don't calculate the ranged columns if
|
|
//there isn't a ranged column while maximal_px == 0, or
|
|
//the minimal ranged size is larger than width
|
|
if ((0 == maximal_px) || (fixed_px + minimal_px > width))
|
|
return false;
|
|
|
|
const bool beyond = (ranged_px + fixed_px > width);
|
|
unsigned delta_px = (beyond ? ranged_px + fixed_px - width : width - (ranged_px + fixed_px));
|
|
|
|
while (delta_px)
|
|
{
|
|
for (auto & col : cont_)
|
|
{
|
|
if (0 == delta_px)
|
|
break;
|
|
|
|
if (col.visible_state && (col.range_width_px.first < col.range_width_px.second))
|
|
{
|
|
if (beyond)
|
|
{
|
|
if (col.range_width_px.first < col.width_px)
|
|
{
|
|
--col.width_px;
|
|
--delta_px;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (col.width_px < col.range_width_px.second)
|
|
{
|
|
++col.width_px;
|
|
--delta_px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
const container& cont() const noexcept
|
|
{
|
|
return cont_;
|
|
}
|
|
|
|
size_type cast(size_type pos, bool disp_order) const
|
|
{
|
|
check_range(pos, cont_.size());
|
|
|
|
size_type order = 0; //order for display position
|
|
for (auto & m : cont_)
|
|
{
|
|
if (!m.visible_state)
|
|
continue;
|
|
|
|
if (disp_order)
|
|
{
|
|
if (0 == pos)
|
|
return m.index;
|
|
--pos;
|
|
}
|
|
else
|
|
{
|
|
if (m.index == pos)
|
|
return order;
|
|
++order;
|
|
}
|
|
}
|
|
|
|
throw std::invalid_argument("listbox: invalid header index");
|
|
}
|
|
|
|
/// find and return a ref to the column that originaly was at position "pos" previous to any list reorganization.
|
|
column& at(size_type pos, bool disp_order = false)
|
|
{
|
|
check_range(pos, cont_.size());
|
|
|
|
//The order of cont_'s elements is the display order.
|
|
if (!disp_order)
|
|
{
|
|
/// It always match the item with pos, otherwise a bug occurs.
|
|
for (auto & m : cont_)
|
|
{
|
|
if (m.index == pos)
|
|
return m;
|
|
}
|
|
}
|
|
|
|
return cont_[pos];
|
|
}
|
|
|
|
const column& at(size_type pos, bool disp_order = false) const
|
|
{
|
|
check_range(pos, cont_.size());
|
|
|
|
if (!disp_order)
|
|
pos = this->cast(pos, false);
|
|
|
|
return cont_[pos];
|
|
}
|
|
|
|
/// Returns the position(original index when it is creating) of the current column at point x
|
|
size_type column_from_point(int x) const noexcept
|
|
{
|
|
for (const auto & col : cont_)
|
|
{
|
|
if (col.visible_state)
|
|
{
|
|
if (x < static_cast<int>(col.width_px))
|
|
return col.index;
|
|
|
|
x -= static_cast<int>(col.width_px);
|
|
continue;
|
|
}
|
|
}
|
|
return npos;
|
|
}
|
|
|
|
unsigned margin() const
|
|
{
|
|
return margin_;
|
|
}
|
|
|
|
std::pair<int, unsigned> range(size_type pos) const
|
|
{
|
|
int left = static_cast<int>(margin_);
|
|
|
|
for (auto & m : cont_)
|
|
{
|
|
if (m.index == pos)
|
|
return{left, m.width_px};
|
|
|
|
if (m.visible_state)
|
|
left += static_cast<int>(m.width_px);
|
|
}
|
|
|
|
return{ left, 0 };
|
|
}
|
|
|
|
/// return the original index of the visible col currently before(in front of) or after the col originaly at index "index"
|
|
size_type next(size_type index) const noexcept
|
|
{
|
|
bool found_me = false;
|
|
for(auto i = cont_.cbegin(); i != cont_.cend(); ++i) // in current order
|
|
{
|
|
if (!found_me)
|
|
{
|
|
if (i->index == index)
|
|
found_me = true;
|
|
}
|
|
else if(i->visible_state)
|
|
return i->index;
|
|
}
|
|
return npos;
|
|
}
|
|
|
|
/// Returns the absolute position of the first/last visible column.
|
|
size_type boundary(bool get_front) const noexcept
|
|
{
|
|
size_type pos = npos;
|
|
for (const auto & m : cont_)
|
|
{
|
|
if (m.visible_state)
|
|
{
|
|
if (get_front)
|
|
return m.index;
|
|
else
|
|
pos = m.index;
|
|
}
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
/// move the col originaly at "from" to the position currently in front (or after) the col originaly at index "to" invalidating some current index
|
|
void move(size_type from, size_type to, bool front) noexcept
|
|
{
|
|
if ((from == to) || (from >= cont_.size()) || (to >= cont_.size()))
|
|
return;
|
|
|
|
#ifdef _MSC_VER
|
|
for (auto i = cont_.cbegin(); i != cont_.cend(); ++i)
|
|
#else
|
|
for (auto i = cont_.begin(); i != cont_.end(); ++i)
|
|
#endif
|
|
{
|
|
if (from == i->index)
|
|
{
|
|
auto col_from = *i;
|
|
cont_.erase(i);
|
|
|
|
//A workaround for old libstdc++, that some operations of vector
|
|
//don't accept const iterator.
|
|
#ifdef _MSC_VER
|
|
for (auto u = cont_.cbegin(); u != cont_.cend(); ++u)
|
|
#else
|
|
for (auto u = cont_.begin(); u != cont_.end(); ++u)
|
|
#endif
|
|
{
|
|
if (to == u->index)
|
|
{
|
|
cont_.insert(front ? u : ++u, col_from);
|
|
return;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
private:
|
|
attributes attrib_;
|
|
unsigned margin_{ 5 };
|
|
container cont_;
|
|
};
|
|
|
|
|
|
struct item_data
|
|
{
|
|
using container = std::vector<cell>;
|
|
|
|
std::unique_ptr<container> cells;
|
|
nana::color bgcolor;
|
|
nana::color fgcolor;
|
|
paint::image img;
|
|
nana::size img_show_size;
|
|
|
|
struct inner_flags
|
|
{
|
|
bool selected :1;
|
|
bool checked :1;
|
|
}flags;
|
|
|
|
mutable std::unique_ptr<nana::any> anyobj;
|
|
|
|
item_data() noexcept
|
|
{
|
|
flags.selected = flags.checked = false;
|
|
}
|
|
|
|
item_data(const item_data& r)
|
|
: cells(r.cells ? std::make_unique<container>(*r.cells) : nullptr),
|
|
bgcolor(r.bgcolor),
|
|
fgcolor(r.fgcolor),
|
|
img(r.img),
|
|
flags(r.flags),
|
|
anyobj(r.anyobj ? new nana::any(*r.anyobj) : nullptr)
|
|
{}
|
|
|
|
item_data(container&& cont)
|
|
: cells(std::make_unique<container>(std::move(cont)))
|
|
{
|
|
flags.selected = flags.checked = false;
|
|
}
|
|
|
|
item_data(std::string&& s)
|
|
: cells(std::make_unique<container>())
|
|
{
|
|
flags.selected = flags.checked = false;
|
|
cells->emplace_back(std::move(s));
|
|
}
|
|
|
|
item_data& operator=(const item_data& r)
|
|
{
|
|
if (this != &r)
|
|
{
|
|
if (r.cells)
|
|
cells = std::make_unique<container>(*r.cells);
|
|
|
|
flags = r.flags;
|
|
anyobj.reset(r.anyobj ? new nana::any(*r.anyobj) : nullptr);
|
|
bgcolor = r.bgcolor;
|
|
fgcolor = r.fgcolor;
|
|
img = r.img;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
std::string to_string(const export_options& exp_opt, const std::vector<cell>* model_cells) const
|
|
{
|
|
std::string item_str;
|
|
|
|
bool ignore_first = true;
|
|
|
|
for (auto col : exp_opt.columns_order)
|
|
{
|
|
if (ignore_first)
|
|
ignore_first = false;
|
|
else
|
|
item_str += exp_opt.sep;
|
|
|
|
//Use the model cells instead if model cells is avaiable
|
|
item_str += (model_cells ? model_cells : cells.get())->operator[](col).text;
|
|
}
|
|
|
|
return item_str;
|
|
}
|
|
};
|
|
|
|
class inline_indicator;
|
|
|
|
struct category_t
|
|
{
|
|
using container = std::deque<item_data>;
|
|
|
|
native_string_type text;
|
|
std::vector<std::size_t> sorted;
|
|
container items;
|
|
|
|
std::unique_ptr<model_interface> model_ptr;
|
|
|
|
bool expand{ true };
|
|
bool display_number{ true };
|
|
|
|
//A cat may have a key object to identify the category
|
|
std::shared_ptr<nana::detail::key_interface> key_ptr;
|
|
|
|
std::deque<pat::cloneable<pat::abstract_factory<inline_notifier_interface>>> factories;
|
|
std::deque<std::unique_ptr<inline_indicator>> indicators;
|
|
|
|
category_t(native_string_type str = {}) noexcept
|
|
:text(std::move(str))
|
|
{}
|
|
|
|
bool selected() const noexcept
|
|
{
|
|
for (auto & m : items)
|
|
{
|
|
if (false == m.flags.selected)
|
|
return false;
|
|
}
|
|
return !items.empty();
|
|
}
|
|
|
|
void make_sort_order()
|
|
{
|
|
sorted.clear();
|
|
for (std::size_t i = 0; i < items.size(); ++i)
|
|
sorted.push_back(i);
|
|
}
|
|
|
|
std::vector<cell> cells(size_type pos) const
|
|
{
|
|
if (model_ptr)
|
|
return model_ptr->container()->to_cells(pos);
|
|
|
|
return *(items.at(pos).cells);
|
|
}
|
|
};
|
|
|
|
struct inline_pane
|
|
{
|
|
::nana::panel<false> pane_bottom; //pane for pane_widget
|
|
::nana::panel<false> pane_widget; //pane for placing user-define widget
|
|
std::unique_ptr<inline_notifier_interface> inline_ptr;
|
|
inline_indicator * indicator;
|
|
index_pair item_pos; //The item index of the inline widget
|
|
std::size_t column_pos;
|
|
};
|
|
|
|
enum class view_action
|
|
{
|
|
auto_view,
|
|
top_view,
|
|
bottom_view,
|
|
};
|
|
|
|
class es_lister
|
|
{
|
|
public:
|
|
using container = std::list<category_t>;
|
|
using item_type = item_data;
|
|
|
|
std::function<std::function<bool(const ::std::string&, ::nana::any*,
|
|
const ::std::string&, ::nana::any*, bool reverse)>(std::size_t) > fetch_ordering_comparer;
|
|
|
|
struct sort_attributes
|
|
{
|
|
std::size_t column; ///< The position of the column to be sorted
|
|
bool resort;
|
|
bool reverse;
|
|
};
|
|
|
|
es_lister()
|
|
{
|
|
//#0 is a default category
|
|
categories_.emplace_back();
|
|
|
|
sort_attrs_.column = npos;
|
|
sort_attrs_.resort = true;
|
|
sort_attrs_.reverse = false;
|
|
}
|
|
|
|
void bind(essence* ess, widget& wd) noexcept
|
|
{
|
|
ess_ = ess;
|
|
widget_ = dynamic_cast<nana::listbox*>(&wd);
|
|
}
|
|
|
|
nana::listbox* wd_ptr() const noexcept
|
|
{
|
|
return widget_;
|
|
}
|
|
|
|
nana::any * anyobj(const index_pair& id, bool allocate_if_empty) const
|
|
{
|
|
auto& catobj = *get(id.cat);
|
|
if(id.item < catobj.items.size())
|
|
{
|
|
auto& item = catobj.items[id.item];
|
|
|
|
if(item.anyobj)
|
|
return item.anyobj.get();
|
|
|
|
if (allocate_if_empty)
|
|
{
|
|
item.anyobj.reset(new ::nana::any);
|
|
return item.anyobj.get();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::string to_string(const export_options& exp_opt) const;
|
|
|
|
void emit_cs(const index_pair& pos, bool for_selection)
|
|
{
|
|
item_proxy item(ess_, pos);
|
|
arg_listbox arg{ item };
|
|
|
|
auto & events = wd_ptr()->events();
|
|
|
|
if (for_selection)
|
|
events.selected.emit(arg, wd_ptr()->handle());
|
|
else
|
|
events.checked.emit(arg, wd_ptr()->handle());
|
|
|
|
//notify the inline pane. An item may have multiple panes, each pane is for a column.
|
|
for (auto p : active_panes_)
|
|
{
|
|
if (p && (p->item_pos == pos))
|
|
{
|
|
if (for_selection)
|
|
p->inline_ptr->notify_status(inline_widget_status::selecting, item.selected());
|
|
else
|
|
p->inline_ptr->notify_status(inline_widget_status::checking, item.checked());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Definition is provided after struct essence
|
|
unsigned column_content_pixels(size_type pos) const;
|
|
|
|
const sort_attributes& sort_attrs() const noexcept
|
|
{
|
|
return sort_attrs_;
|
|
}
|
|
|
|
/// each sort() ivalidate any existing reference from display position to absolute item, that is after sort() display offset point to different items
|
|
void sort()
|
|
{
|
|
if((npos == sort_attrs_.column) || (!sort_attrs_.resort))
|
|
return;
|
|
|
|
auto weak_ordering_comp = fetch_ordering_comparer(sort_attrs_.column);
|
|
if(weak_ordering_comp)
|
|
{
|
|
for (auto & cat : categories_)
|
|
{
|
|
const bool use_model = (cat.model_ptr != nullptr);
|
|
|
|
std::stable_sort(cat.sorted.begin(), cat.sorted.end(), [&cat, &weak_ordering_comp, use_model, this](std::size_t x, std::size_t y){
|
|
//The predicate must be a strict weak ordering.
|
|
//!comp(x, y) != comp(x, y)
|
|
if (use_model)
|
|
{
|
|
auto & mx = cat.items[x];
|
|
auto & my = cat.items[y];
|
|
|
|
auto mx_cells = cat.model_ptr->container()->to_cells(x);
|
|
auto my_cells = cat.model_ptr->container()->to_cells(y);
|
|
|
|
if (mx_cells.size() <= sort_attrs_.column || my_cells.size() <= sort_attrs_.column)
|
|
{
|
|
std::string a;
|
|
if (mx_cells.size() > sort_attrs_.column)
|
|
a = mx_cells[sort_attrs_.column].text;
|
|
|
|
std::string b;
|
|
if (my_cells.size() > sort_attrs_.column)
|
|
b = my_cells[sort_attrs_.column].text;
|
|
|
|
return weak_ordering_comp(a, mx.anyobj.get(), b, my.anyobj.get(), sort_attrs_.reverse);
|
|
}
|
|
|
|
return weak_ordering_comp(mx_cells[sort_attrs_.column].text, mx.anyobj.get(), my_cells[sort_attrs_.column].text, my.anyobj.get(), sort_attrs_.reverse);
|
|
}
|
|
|
|
auto & mx = cat.items[x];
|
|
auto & my = cat.items[y];
|
|
|
|
if (mx.cells->size() <= sort_attrs_.column || my.cells->size() <= sort_attrs_.column)
|
|
{
|
|
std::string a;
|
|
if (mx.cells->size() > sort_attrs_.column)
|
|
a = (*mx.cells)[sort_attrs_.column].text;
|
|
|
|
std::string b;
|
|
if (my.cells->size() > sort_attrs_.column)
|
|
b = (*my.cells)[sort_attrs_.column].text;
|
|
|
|
return weak_ordering_comp(a, mx.anyobj.get(), b, my.anyobj.get(), sort_attrs_.reverse);
|
|
}
|
|
|
|
return weak_ordering_comp((*mx.cells)[sort_attrs_.column].text, mx.anyobj.get(), (*my.cells)[sort_attrs_.column].text, my.anyobj.get(), sort_attrs_.reverse);
|
|
});
|
|
}
|
|
}
|
|
else
|
|
{ //No user-defined comparer is provided, and default comparer is applying.
|
|
for (auto & cat : categories_)
|
|
{
|
|
const bool use_model = (cat.model_ptr != nullptr);
|
|
|
|
std::stable_sort(cat.sorted.begin(), cat.sorted.end(), [this, &cat, use_model](std::size_t x, std::size_t y){
|
|
//The predicate must be a strict weak ordering.
|
|
//!comp(x, y) != comp(x, y)
|
|
if (use_model)
|
|
{
|
|
auto mx_cells = cat.model_ptr->container()->to_cells(x);
|
|
auto my_cells = cat.model_ptr->container()->to_cells(y);
|
|
|
|
if (mx_cells.size() <= sort_attrs_.column || my_cells.size() <= sort_attrs_.column)
|
|
{
|
|
std::string a;
|
|
if (mx_cells.size() > sort_attrs_.column)
|
|
a = mx_cells[sort_attrs_.column].text;
|
|
|
|
std::string b;
|
|
if (my_cells.size() > sort_attrs_.column)
|
|
b = my_cells[sort_attrs_.column].text;
|
|
|
|
return (sort_attrs_.reverse ? a > b : a < b);
|
|
}
|
|
|
|
auto & a = mx_cells[sort_attrs_.column].text;
|
|
auto & b = my_cells[sort_attrs_.column].text;
|
|
return (sort_attrs_.reverse ? a > b : a < b);
|
|
}
|
|
|
|
auto & mx = cat.items[x];
|
|
auto & my = cat.items[y];
|
|
|
|
if (mx.cells->size() <= sort_attrs_.column || my.cells->size() <= sort_attrs_.column)
|
|
{
|
|
std::string a;
|
|
if (mx.cells->size() > sort_attrs_.column)
|
|
a = (*mx.cells)[sort_attrs_.column].text;
|
|
|
|
std::string b;
|
|
if (my.cells->size() > sort_attrs_.column)
|
|
b = (*my.cells)[sort_attrs_.column].text;
|
|
|
|
return (sort_attrs_.reverse ? a > b : a < b);
|
|
}
|
|
|
|
auto & a = (*mx.cells)[sort_attrs_.column].text;
|
|
auto & b = (*my.cells)[sort_attrs_.column].text;
|
|
return (sort_attrs_.reverse ? a > b : a < b);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sorts the specified column
|
|
/**
|
|
* It sorts the specified column and invalidates all existing item reference from display position to absolute position.
|
|
* The side effect of this method is that all display positions point to different absolute positions.
|
|
* @param pos The position of the specified column.
|
|
* @param reverse A pointer to a boolean which indicates whether to reverse sort. If this parameter is nullptr, the sort is negated to the current reverse state.
|
|
* @return true if the column is sorted, false otherwise.
|
|
*/
|
|
bool sort_column(std::size_t pos, const bool * reverse)
|
|
{
|
|
if (nana::npos == pos)
|
|
{
|
|
sort_attrs_.column = npos;
|
|
return false;
|
|
}
|
|
|
|
if (reverse)
|
|
{
|
|
if (pos != sort_attrs_.column || *reverse != sort_attrs_.reverse)
|
|
{
|
|
sort_attrs_.column = pos;
|
|
sort_attrs_.reverse = *reverse;
|
|
|
|
sort();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (pos != sort_attrs_.column)
|
|
{
|
|
sort_attrs_.column = pos;
|
|
sort_attrs_.reverse = false;
|
|
}
|
|
else
|
|
sort_attrs_.reverse = !sort_attrs_.reverse;
|
|
|
|
sort();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool active_sort(bool resort) noexcept
|
|
{
|
|
bool prstatus = sort_attrs_.resort;
|
|
sort_attrs_.resort = resort;
|
|
return prstatus;
|
|
}
|
|
|
|
/// Scroll the selected item into the view
|
|
void scroll_into_view(const index_pair& abs_pos, view_action vw_act);
|
|
|
|
|
|
/// will use the key to insert new cat before the first cat with compare less than the key, or at the end of the list of cat and return a ref to that new cat. ?
|
|
category_t* create_category(std::shared_ptr<nana::detail::key_interface>& ptr)
|
|
{
|
|
//A workaround for old version of libstdc++
|
|
//Some operations of vector provided by libstdc++ don't accept const iterator.
|
|
#ifdef _MSC_VER
|
|
for (auto i = categories_.cbegin(); i != categories_.cend(); ++i)
|
|
#else
|
|
for (auto i = categories_.begin(); i != categories_.end(); ++i)
|
|
#endif
|
|
{
|
|
if (i->key_ptr)
|
|
{
|
|
if (!i->key_ptr->same_type(ptr.get()))
|
|
{
|
|
this->ordered_categories_ = false;
|
|
break;
|
|
}
|
|
else if (ptr->compare(i->key_ptr.get()))
|
|
{
|
|
auto & catobj = *categories_.emplace(i);
|
|
catobj.key_ptr = ptr;
|
|
return &catobj;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef _nana_std_has_emplace_return_type
|
|
auto & last_cat = categories_.emplace_back();
|
|
last_cat.key_ptr = ptr;
|
|
return &last_cat;
|
|
#else
|
|
categories_.emplace_back();
|
|
categories_.back().key_ptr = ptr;
|
|
return &(categories_.back());
|
|
#endif
|
|
}
|
|
|
|
/// Inserts a new category at position specified by pos
|
|
category_t* create_category(native_string_type&& text, std::size_t pos = nana::npos)
|
|
{
|
|
if (::nana::npos == pos)
|
|
{
|
|
#ifdef _nana_std_has_emplace_return_type
|
|
return &categories_.emplace_back(std::move(text));
|
|
#else
|
|
categories_.emplace_back(std::move(text));
|
|
return &categories_.back();
|
|
#endif
|
|
}
|
|
|
|
return &(*categories_.emplace(this->get(pos), std::move(text)));
|
|
}
|
|
|
|
/// Insert before item in absolute "pos" a new item with "text" in column 0, and place it in last display position of this cat
|
|
void insert(const index_pair& pos, std::string&& text, const std::size_t columns)
|
|
{
|
|
auto & catobj = *get(pos.cat);
|
|
|
|
const auto item_count = catobj.items.size();
|
|
|
|
check_range(pos.item, item_count);
|
|
|
|
catobj.sorted.push_back(item_count);
|
|
|
|
if (catobj.model_ptr)
|
|
{
|
|
throw_if_immutable_model(catobj.model_ptr.get());
|
|
auto container = catobj.model_ptr->container();
|
|
std::size_t item_index;
|
|
//
|
|
if (pos.item < item_count)
|
|
{
|
|
catobj.items.emplace(catobj.items.begin() + pos.item);
|
|
container->emplace(pos.item);
|
|
item_index = pos.item;
|
|
}
|
|
else
|
|
{
|
|
item_index = container->size();
|
|
catobj.items.emplace_back();
|
|
container->emplace_back();
|
|
}
|
|
|
|
std::vector<cell> cells;
|
|
cells.emplace_back(std::move(text));
|
|
cells.resize(columns);
|
|
container->assign(item_index, cells);
|
|
|
|
return;
|
|
}
|
|
|
|
catobj.items.emplace(catobj.items.begin() + (pos.item < item_count ? pos.item : item_count), std::move(text));
|
|
}
|
|
|
|
/// Converts an index between display position and absolute real position.
|
|
/**
|
|
* @param from An index to be converted
|
|
* @param from_display_order If this parameter is true, the method convert a display position to the absolute position. If the parameter
|
|
* is false, the method converts an absolute position to the display position.
|
|
* @return A display position or absolute position that are depending on from_display_order.
|
|
*/
|
|
index_pair index_cast(const index_pair& from, bool from_display_order) const
|
|
{
|
|
auto target = index_cast_noexcept(from, from_display_order);
|
|
if (target.empty())
|
|
throw std::out_of_range("listbox: invalid element position");
|
|
return target;
|
|
}
|
|
|
|
index_pair index_cast_noexcept(const index_pair& from, bool from_display_order, const index_pair& default_value = index_pair{npos, npos}) const noexcept
|
|
{
|
|
if (from.cat < categories_.size())
|
|
{
|
|
auto i = categories_.cbegin();
|
|
std::advance(i, from.cat);
|
|
|
|
auto & cat = *i;
|
|
if (from.item < cat.sorted.size())
|
|
{
|
|
if (from_display_order)
|
|
return index_pair{ from.cat, static_cast<size_type>(cat.sorted[from.item]) };
|
|
|
|
for (size_type i = 0; i < cat.sorted.size(); ++i)
|
|
{
|
|
if (from.item == cat.sorted[i])
|
|
return index_pair{ from.cat, i };
|
|
}
|
|
}
|
|
}
|
|
return default_value;
|
|
}
|
|
|
|
static void throw_if_immutable_model(model_interface* model)
|
|
{
|
|
if (model && model->container()->immutable())
|
|
{
|
|
//Precondition check for the insert/erase operation, it throws if the model is immutable
|
|
throw std::runtime_error("nana::listbox disallow to insert/remove items because of immutable model");
|
|
}
|
|
}
|
|
|
|
void throw_if_immutable_model(const index_pair& pos) const
|
|
{
|
|
if (pos.cat < categories_.size())
|
|
{
|
|
auto i = categories_.cbegin();
|
|
std::advance(i, pos.cat);
|
|
|
|
throw_if_immutable_model(i->model_ptr.get());
|
|
}
|
|
}
|
|
|
|
void assign_model(const index_pair& pos, const std::vector<cell>& cells)
|
|
{
|
|
if (pos.cat < categories_.size())
|
|
{
|
|
auto i = categories_.cbegin();
|
|
std::advance(i, pos.cat);
|
|
if (i->model_ptr)
|
|
{
|
|
throw_if_immutable_model(i->model_ptr.get());
|
|
i->model_ptr->container()->assign(pos.item, cells);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool has_model(const index_pair& pos) const
|
|
{
|
|
return (get(pos.cat)->model_ptr != nullptr);
|
|
}
|
|
|
|
category_t::container::value_type& at_abs(const index_pair& pos)
|
|
{
|
|
return get(pos.cat)->items.at(pos.item);
|
|
}
|
|
|
|
std::vector<cell> at_model_abs(const index_pair& pos) const
|
|
{
|
|
auto model_ptr = get(pos.cat)->model_ptr.get();
|
|
|
|
model_lock_guard lock(model_ptr);
|
|
if (model_ptr)
|
|
return model_ptr->container()->to_cells(pos.item);
|
|
|
|
return{};
|
|
}
|
|
|
|
/// return a ref to the real item object at display!!! position pos using current sorting only if it is active, and at absolute position if no sorting is currently active.
|
|
category_t::container::value_type& at(const index_pair& pos)
|
|
{
|
|
auto acc_pos = pos.item;
|
|
if (npos != sort_attrs_.column)
|
|
acc_pos = index_cast(pos, true).item; //convert display position to absolute position
|
|
|
|
return get(pos.cat)->items.at(acc_pos);
|
|
}
|
|
|
|
const category_t::container::value_type& at(const index_pair& pos) const
|
|
{
|
|
auto acc_pos = pos.item;
|
|
if (npos != sort_attrs_.column)
|
|
acc_pos = index_cast(pos, true).item; //convert display position to absolute position
|
|
|
|
return get(pos.cat)->items.at(acc_pos);
|
|
}
|
|
|
|
std::vector<cell> at_model(const index_pair& pos) const
|
|
{
|
|
auto model_ptr = get(pos.cat)->model_ptr.get();
|
|
if (!model_ptr)
|
|
return{};
|
|
|
|
model_lock_guard lock(model_ptr);
|
|
|
|
auto acc_pos = pos.item;
|
|
if (npos != sort_attrs_.column)
|
|
acc_pos = index_cast(pos, true).item; //convert display position to absolute position
|
|
|
|
return model_ptr->container()->to_cells(acc_pos);
|
|
}
|
|
|
|
void append_active_panes(inline_pane* p)
|
|
{
|
|
if (nullptr == p)
|
|
active_panes_.clear();
|
|
else
|
|
active_panes_.push_back(p);
|
|
}
|
|
|
|
// Removes all items of a specified category
|
|
// It throws when the category is out of range or has an immutable model.
|
|
void clear(size_type cat)
|
|
{
|
|
auto& catobj = *get(cat);
|
|
|
|
model_lock_guard lock(catobj.model_ptr.get());
|
|
if (catobj.model_ptr)
|
|
{
|
|
//The immutable modal can't be cleared.
|
|
throw_if_immutable_model(catobj.model_ptr.get());
|
|
|
|
catobj.model_ptr->container()->clear();
|
|
}
|
|
|
|
catobj.items.clear();
|
|
catobj.sorted.clear();
|
|
}
|
|
|
|
// Clears all items in all cat, but not the container of cat self.
|
|
void clear()
|
|
{
|
|
// Check whether there is a immutable model before performing clear.
|
|
for (auto & cat : categories_)
|
|
throw_if_immutable_model(cat.model_ptr.get());
|
|
|
|
for (size_type i = 0; i < categories_.size(); ++i)
|
|
clear(i);
|
|
}
|
|
|
|
index_pair advance(const index_pair& pos, int n) const
|
|
{
|
|
const auto cat_size = categories_.size();
|
|
index_pair dpos{ npos, npos };
|
|
|
|
if (pos.cat >= cat_size || (pos.item != npos && pos.item >= size_item(pos.cat)))
|
|
return dpos;
|
|
|
|
if ((0 == pos.cat && npos == pos.item) || (!expand(pos.cat) && (npos != pos.item)))
|
|
return dpos;
|
|
|
|
if (0 == n)
|
|
return pos;
|
|
|
|
dpos = pos;
|
|
if (0 < n)
|
|
{
|
|
//Forward
|
|
std::size_t index = (npos == pos.item ? 0 : pos.item + 1);
|
|
|
|
while (n)
|
|
{
|
|
std::size_t end = 1;
|
|
if (expand(dpos.cat))
|
|
end += size_item(dpos.cat);
|
|
|
|
if (n < static_cast<int>(end - index))
|
|
return index_pair{ dpos.cat, index + n - 1 };
|
|
|
|
++dpos.cat;
|
|
if (cat_size == dpos.cat)
|
|
return index_pair{ npos, npos };
|
|
|
|
n -= static_cast<int>(end - index);
|
|
index = 0;
|
|
}
|
|
return index_pair{ dpos.cat, npos };
|
|
}
|
|
|
|
//Backward
|
|
n = -n;
|
|
dpos = pos;
|
|
if (good(dpos.cat))
|
|
{
|
|
auto count = static_cast<int>(dpos.is_category() ? 1 : pos.item + 2);
|
|
auto i = get(pos.cat);
|
|
while (true)
|
|
{
|
|
if (count > n)
|
|
{
|
|
count -= n;
|
|
dpos.item = (count == 1 ? npos : count - 2);
|
|
return dpos;
|
|
}
|
|
|
|
n -= count;
|
|
|
|
if (i == categories_.cbegin())
|
|
break;
|
|
|
|
--i;
|
|
--dpos.cat;
|
|
count = static_cast<int>(i->expand ? i->items.size() : 0) + 1;
|
|
}
|
|
}
|
|
return index_pair{npos, npos};
|
|
}
|
|
|
|
/// change to index arg
|
|
size_type distance(index_pair from, index_pair to) const
|
|
{
|
|
if(from == to ) return 0;
|
|
|
|
if(to.cat == from.cat)
|
|
{
|
|
if(from.item > to.item && from.item != npos)
|
|
std::swap(from, to);
|
|
|
|
return (from.item == npos ? to.item + 1 : to.item - from.item);
|
|
}
|
|
else if(to.cat < from.cat)
|
|
std::swap(from, to);
|
|
|
|
std::size_t count = 1;
|
|
for (auto i = get(from.cat); i != get(to.cat); ++i)
|
|
{
|
|
if (i->expand)
|
|
count += i->items.size() + 1;
|
|
else
|
|
++count;
|
|
}
|
|
|
|
if (npos != to.item)
|
|
count += (1 + to.item);
|
|
|
|
if (npos != from.item)
|
|
count -= (1 + from.item);
|
|
else if (0 == from.cat)
|
|
--count;
|
|
|
|
return count - 1;
|
|
}
|
|
|
|
void text(category_t* cat, size_type pos, size_type abs_col, cell&& cl, size_type columns)
|
|
{
|
|
if ((abs_col < columns) && (pos < cat->items.size()))
|
|
{
|
|
std::vector<cell> model_cells;
|
|
|
|
model_lock_guard lock(cat->model_ptr.get());
|
|
if (cat->model_ptr)
|
|
{
|
|
throw_if_immutable_model(cat->model_ptr.get());
|
|
model_cells = cat->model_ptr->container()->to_cells(pos);
|
|
}
|
|
|
|
auto & cells = (cat->model_ptr ? model_cells : *(cat->items[pos].cells));
|
|
|
|
if (abs_col < cells.size())
|
|
{
|
|
cells[abs_col] = std::move(cl);
|
|
if (sort_attrs_.column == abs_col)
|
|
sort();
|
|
}
|
|
else
|
|
{ //If the index of specified sub item is over the number of sub items that item contained,
|
|
//it fills the non-exist items.
|
|
cells.resize(abs_col);
|
|
cells.emplace_back(std::move(cl));
|
|
}
|
|
|
|
if (cat->model_ptr)
|
|
cat->model_ptr->container()->assign(pos, model_cells);
|
|
}
|
|
}
|
|
|
|
void text(category_t* cat, size_type pos, size_type abs_col, std::string&& str, size_type columns)
|
|
{
|
|
if ((abs_col < columns) && (pos < cat->items.size()))
|
|
{
|
|
std::vector<cell> model_cells;
|
|
|
|
model_lock_guard lock(cat->model_ptr.get());
|
|
if (cat->model_ptr)
|
|
{
|
|
throw_if_immutable_model(cat->model_ptr.get());
|
|
model_cells = cat->model_ptr->container()->to_cells(pos);
|
|
}
|
|
|
|
auto & cells = (cat->model_ptr ? model_cells : *(cat->items[pos].cells));
|
|
|
|
if (abs_col < cells.size())
|
|
{
|
|
cells[abs_col].text = std::move(str);
|
|
if (sort_attrs_.column == abs_col)
|
|
sort();
|
|
}
|
|
else
|
|
{ //If the index of specified sub item is over the number of sub items that item contained,
|
|
//it fills the non-exist items.
|
|
cells.resize(abs_col);
|
|
cells.emplace_back(std::move(str));
|
|
}
|
|
|
|
if (cat->model_ptr)
|
|
cat->model_ptr->container()->assign(pos, model_cells);
|
|
}
|
|
}
|
|
|
|
void erase(const index_pair& pos);
|
|
|
|
void erase(size_type cat)
|
|
{
|
|
auto i = get(cat);
|
|
|
|
//If the category is the first one, it just clears the items instead of removing whole category.
|
|
if(0 == cat)
|
|
{
|
|
if (i->model_ptr)
|
|
{
|
|
throw_if_immutable_model(i->model_ptr.get());
|
|
i->model_ptr->container()->clear();
|
|
}
|
|
|
|
i->items.clear();
|
|
i->sorted.clear();
|
|
}
|
|
else
|
|
categories_.erase(i);
|
|
}
|
|
|
|
void erase()
|
|
{
|
|
//Do not remove the first category.
|
|
|
|
this->erase(0);
|
|
|
|
if (categories_.size() > 1)
|
|
{
|
|
//A workaround for old version of libstdc++
|
|
//Some operations of vector provided by libstdc++ don't accept const iterator.
|
|
#ifdef _MSC_VER
|
|
categories_.erase(++categories_.cbegin(), categories_.cend());
|
|
#else
|
|
categories_.erase(++categories_.begin(), categories_.end());
|
|
#endif
|
|
}
|
|
}
|
|
|
|
bool expand(size_type cat, bool exp) noexcept
|
|
{
|
|
//It is allowed to expand the 1st category.
|
|
if(good(cat) && (cat || exp))
|
|
{
|
|
auto & expanded = get(cat)->expand;
|
|
if(expanded != exp)
|
|
{
|
|
expanded = exp;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool expand(size_type cat) const noexcept
|
|
{
|
|
return (good(cat) ? get(cat)->expand : false);
|
|
}
|
|
|
|
container& cat_container() noexcept
|
|
{
|
|
return categories_;
|
|
}
|
|
|
|
const container& cat_container() const noexcept
|
|
{
|
|
return categories_;
|
|
}
|
|
|
|
//Enable/Disable the ordered categories
|
|
bool enable_ordered(bool enb) noexcept
|
|
{
|
|
if (ordered_categories_ != enb)
|
|
{
|
|
if (enb)
|
|
{
|
|
::nana::detail::key_interface * refkey = nullptr;
|
|
|
|
for (auto & cat : categories_)
|
|
{
|
|
if (!cat.key_ptr)
|
|
continue;
|
|
|
|
if (refkey)
|
|
{
|
|
if (!cat.key_ptr->same_type(refkey))
|
|
return false;
|
|
}
|
|
else
|
|
refkey = cat.key_ptr.get();
|
|
}
|
|
}
|
|
|
|
ordered_categories_ = enb;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool enable_ordered() const noexcept
|
|
{
|
|
return ordered_categories_;
|
|
}
|
|
|
|
size_type the_number_of_expanded() const noexcept
|
|
{
|
|
size_type n = categories_.size() - 1;
|
|
for (auto & i : categories_)
|
|
{
|
|
if(i.expand)
|
|
n += i.items.size();
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/// Finds a good item or category if an item specified by pos is invaild
|
|
index_pair find_next_good(index_pair pos, bool ignore_category) const noexcept
|
|
{
|
|
//Return the pos if it is good
|
|
if (this->good(pos))
|
|
return pos;
|
|
|
|
while(pos.cat < this->categories_.size())
|
|
{
|
|
if ((pos.npos == pos.item) && !ignore_category)
|
|
return pos;
|
|
|
|
auto cat_item_size = this->get(pos.cat)->items.size();
|
|
|
|
if (pos.item < cat_item_size)
|
|
return pos;
|
|
|
|
if ((pos.npos == pos.item) && (cat_item_size > 0) && ignore_category)
|
|
return index_pair{ pos.cat, 0 };
|
|
|
|
++pos.cat;
|
|
pos.item = pos.npos;
|
|
}
|
|
return index_pair{};
|
|
}
|
|
|
|
template<typename Pred>
|
|
std::vector<std::pair<index_pair, bool>> select_display_range_if(index_pair fr_abs, index_pair to_dpl, bool deselect_others, Pred pred)
|
|
{
|
|
fr_abs = find_next_good(fr_abs, true);
|
|
|
|
if (to_dpl.empty())
|
|
{
|
|
if (fr_abs.empty())
|
|
return{};
|
|
|
|
to_dpl = this->last();
|
|
}
|
|
|
|
|
|
auto fr_dpl = (fr_abs.is_category() ? fr_abs : this->index_cast(fr_abs, false)); //Converts an absolute position to display position
|
|
if (fr_dpl > to_dpl)
|
|
std::swap(fr_dpl, to_dpl);
|
|
|
|
if (to_dpl.is_category() && this->size_item(to_dpl.cat) > 0)
|
|
to_dpl.item = this->size_item(to_dpl.cat) - 1;
|
|
|
|
const index_pairs already_selected = (deselect_others ? this->pick_items(true) : index_pairs{});
|
|
|
|
const auto begin = fr_dpl;
|
|
const auto last = to_dpl;
|
|
|
|
//pair first: index in the range of [begin, last]
|
|
//pair second: indicates whether the index is selected before selection.
|
|
std::vector<std::pair<index_pair, bool>> pairs;
|
|
|
|
for (; !(fr_dpl > to_dpl); fr_dpl = advance(fr_dpl, 1)) //fr_dpl <= to_dpl
|
|
{
|
|
if (fr_dpl.is_category())
|
|
{
|
|
if (!expand(fr_dpl.cat))
|
|
{
|
|
auto size = size_item(fr_dpl.cat);
|
|
for (std::size_t i = 0; i < size; ++i)
|
|
{
|
|
index_pair abs_pos{ fr_dpl.cat, i };
|
|
item_proxy m{ ess_, abs_pos };
|
|
pairs.emplace_back(abs_pos, m.selected());
|
|
|
|
if (pred(abs_pos))
|
|
m.select(true);
|
|
}
|
|
|
|
if (fr_dpl.cat == to_dpl.cat)
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
auto abs_pos = index_cast(fr_dpl, true); //convert display position to absolute position
|
|
item_proxy m{ ess_, abs_pos };
|
|
pairs.emplace_back(abs_pos, m.selected());
|
|
|
|
if (pred(abs_pos))
|
|
m.select(true);
|
|
}
|
|
}
|
|
|
|
if (deselect_others)
|
|
{
|
|
//Deselects the already selected which is out of range [begin, last]
|
|
for (auto index : already_selected)
|
|
{
|
|
auto disp_order = this->index_cast(index, false); //converts an absolute position to a display position
|
|
if (begin > disp_order || disp_order > last)
|
|
item_proxy{ ess_, index }.select(false);
|
|
}
|
|
}
|
|
|
|
return pairs;
|
|
}
|
|
|
|
bool select_for_all(bool sel, const index_pair& except_abs = index_pair{npos, npos})
|
|
{
|
|
bool changed = false;
|
|
index_pair pos;
|
|
for (auto & cat : categories_)
|
|
{
|
|
pos.item = 0;
|
|
for(auto & m : cat.items)
|
|
{
|
|
if (except_abs != pos)
|
|
{
|
|
if (m.flags.selected != sel)
|
|
{
|
|
changed = true;
|
|
m.flags.selected = sel;
|
|
|
|
this->emit_cs(pos, true);
|
|
|
|
if (m.flags.selected)
|
|
latest_selected_abs = pos;
|
|
else if (latest_selected_abs == pos)
|
|
latest_selected_abs.set_both(npos); //make empty
|
|
}
|
|
}
|
|
++pos.item;
|
|
}
|
|
++pos.cat;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
/// return absolute positions, no relative to display
|
|
/**
|
|
* @param for_selection Indicates whether the selected items or checked items to be returned.
|
|
* @param find_first Indicates whether or not to return the first item which
|
|
* @param items_status a pointer refers to a bool object to receive the status whethe the picked items are all selected or all checked, in contrast to for_selection
|
|
*/
|
|
index_pairs pick_items(bool for_selection, bool find_first = false, bool * items_status = nullptr) const
|
|
{
|
|
index_pairs results;
|
|
index_pair id;
|
|
|
|
if (items_status)
|
|
*items_status = true;
|
|
|
|
for (auto & cat : categories_)
|
|
{
|
|
id.item = 0;
|
|
for (auto & m : cat.items)
|
|
{
|
|
if (for_selection ? m.flags.selected : m.flags.checked)
|
|
{
|
|
if (items_status && *items_status)
|
|
*items_status = (for_selection ? m.flags.checked : m.flags.selected);
|
|
|
|
results.push_back(id); // absolute positions, no relative to display
|
|
if (find_first)
|
|
return results;
|
|
}
|
|
++id.item;
|
|
}
|
|
++id.cat;
|
|
}
|
|
return results;
|
|
}
|
|
|
|
///<Selects an item besides the current selected item in the display.
|
|
/// we are moving in display, but the selection ocurre in abs position
|
|
void move_select(bool upwards=true, bool unselect_previous=true, bool into_view=false) noexcept;
|
|
|
|
struct pred_cancel
|
|
{
|
|
const bool for_selection;
|
|
|
|
pred_cancel(bool for_sel) noexcept : for_selection(for_sel) {}
|
|
|
|
bool operator()(category_t::container::value_type & m) const noexcept
|
|
{
|
|
return (for_selection ? m.flags.selected : m.flags.checked);
|
|
}
|
|
};
|
|
|
|
struct emit_cancel
|
|
{
|
|
es_lister* const self;
|
|
const bool for_selection;
|
|
|
|
emit_cancel(es_lister* self, bool for_sel) noexcept : self(self), for_selection(for_sel) {}
|
|
|
|
void operator()(category_t::container::value_type& m, const index_pair& item_pos) const
|
|
{
|
|
if (for_selection)
|
|
m.flags.selected = false;
|
|
else
|
|
m.flags.checked = false;
|
|
|
|
self->emit_cs(item_pos, for_selection);
|
|
}
|
|
};
|
|
|
|
void cancel_others_if_single_enabled(bool for_selection, const index_pair& except)
|
|
{
|
|
if (!(for_selection ? single_selection_ : single_check_))
|
|
return;
|
|
|
|
pred_cancel pred{ for_selection };
|
|
emit_cancel do_cancel{ this, for_selection };
|
|
|
|
if (for_selection ? single_selection_category_limited_ : single_check_category_limited_)
|
|
{
|
|
auto i = this->get(except.cat);
|
|
|
|
std::size_t item_pos = 0;
|
|
for (auto & m : i->items)
|
|
{
|
|
if ((item_pos != except.item) && pred(m))
|
|
do_cancel(m, index_pair{ except.cat, item_pos });
|
|
|
|
++item_pos;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
index_pair cancel_pos;
|
|
for (auto & cat : categories_)
|
|
{
|
|
for (auto & m : cat.items)
|
|
{
|
|
if ((cancel_pos != except) && pred(m))
|
|
do_cancel(m, cancel_pos);
|
|
|
|
++cancel_pos.item;
|
|
}
|
|
|
|
++cancel_pos.cat;
|
|
cancel_pos.item = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool single_status(bool for_selection) const noexcept
|
|
{
|
|
return (for_selection ? single_selection_ : single_check_);
|
|
}
|
|
|
|
void enable_single(bool for_selection, bool category_limited)
|
|
{
|
|
bool & single = (for_selection ? single_selection_ : single_check_);
|
|
bool & limited = (for_selection ? single_selection_category_limited_ : single_check_category_limited_);
|
|
|
|
if (single && (limited == category_limited))
|
|
return;
|
|
|
|
single = true;
|
|
limited = category_limited;
|
|
|
|
pred_cancel pred{ for_selection };
|
|
emit_cancel cancel{ this, for_selection };
|
|
|
|
std::size_t cat_pos = 0;
|
|
|
|
bool selected = false;
|
|
|
|
for (auto & cat : categories_)
|
|
{
|
|
if ((category_limited) || (!selected))
|
|
{
|
|
bool ignore = true; //Ignore the first matched item
|
|
std::size_t pos = 0;
|
|
for (auto & m : cat.items)
|
|
{
|
|
if (pred(m))
|
|
{
|
|
selected = true;
|
|
|
|
if (ignore)
|
|
ignore = false;
|
|
else
|
|
cancel(m, index_pair{ cat_pos, pos });
|
|
}
|
|
++pos;
|
|
}
|
|
++cat_pos;
|
|
}
|
|
else
|
|
{
|
|
std::size_t skip_cat = 0;
|
|
for (auto & cat : categories_)
|
|
{
|
|
if (skip_cat++ < cat_pos)
|
|
continue;
|
|
|
|
std::size_t pos = 0;
|
|
for (auto & m : cat.items)
|
|
{
|
|
if (pred(m))
|
|
cancel(m, index_pair{ cat_pos, pos });
|
|
++pos;
|
|
}
|
|
++cat_pos;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void disable_single(bool for_selection) noexcept
|
|
{
|
|
(for_selection ? single_selection_ : single_check_) = false;
|
|
}
|
|
|
|
size_type size_item(size_type cat) const
|
|
{
|
|
return get(cat)->items.size();
|
|
}
|
|
|
|
bool cat_status(size_type pos, bool for_selection) const
|
|
{
|
|
auto & items = get(pos)->items;
|
|
for (auto & m : items)
|
|
{
|
|
if ((for_selection ? m.flags.selected : m.flags.checked) == false)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool cat_status(size_type pos, bool for_selection, bool value);
|
|
|
|
bool cat_status_reverse(size_type pos, bool for_selection) noexcept
|
|
{
|
|
if (pos < categories_.size())
|
|
{
|
|
return cat_status(pos, !cat_status(pos, for_selection));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// can be used as the absolute position of the last absolute item, or as the display pos of the last displayed item
|
|
index_pair last() const noexcept
|
|
{
|
|
index_pair i{ categories_.size() - 1, categories_.back().items.size() };
|
|
|
|
if (i.cat)
|
|
{
|
|
if (i.item && categories_.back().expand)
|
|
--i.item;
|
|
else
|
|
i.item = npos;
|
|
}
|
|
else
|
|
{
|
|
if (i.item)
|
|
--i.item;
|
|
else
|
|
return index_pair{ npos, npos };
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
/// can be used as the absolute position of the first absolute item, or as the display pos of the first displayed item
|
|
index_pair first() const noexcept
|
|
{
|
|
auto i = categories_.cbegin();
|
|
if (i->items.size())
|
|
return index_pair{ 0, 0 };
|
|
|
|
if (categories_.size() > 1)
|
|
return index_pair{ 1, npos };
|
|
|
|
return index_pair{ npos, npos };
|
|
}
|
|
|
|
bool good(size_type cat) const noexcept
|
|
{
|
|
return (cat < categories_.size());
|
|
}
|
|
|
|
bool good(const index_pair& pos) const noexcept
|
|
{
|
|
return ((pos.cat < categories_.size()) && (pos.item < size_item(pos.cat)));
|
|
}
|
|
|
|
/// categories iterator
|
|
container::iterator get(size_type pos)
|
|
{
|
|
check_range(pos, categories_.size());
|
|
|
|
auto i = categories_.begin();
|
|
std::advance(i, pos);
|
|
return i;
|
|
}
|
|
|
|
container::const_iterator get(size_type pos) const
|
|
{
|
|
check_range(pos, categories_.size());
|
|
|
|
auto i = categories_.cbegin();
|
|
std::advance(i, pos);
|
|
return i;
|
|
}
|
|
public:
|
|
index_pair latest_selected_abs; //Stands for the latest selected item that selected by last operation. Invalid if it is empty.
|
|
private:
|
|
essence * ess_{nullptr};
|
|
nana::listbox * widget_{nullptr};
|
|
|
|
sort_attributes sort_attrs_; //Attributes of sort
|
|
container categories_;
|
|
|
|
bool ordered_categories_{false}; ///< A switch indicates whether the categories are ordered.
|
|
/// The ordered categories always creates a new category at a proper position(before the first one which is larger than it).
|
|
|
|
bool single_selection_{ false };
|
|
bool single_selection_category_limited_{ false };
|
|
bool single_check_{ false };
|
|
bool single_check_category_limited_{ false };
|
|
|
|
std::vector<inline_pane*> active_panes_;
|
|
};//end class es_lister
|
|
|
|
enum class operation_states
|
|
{
|
|
none,
|
|
msup_deselect
|
|
};
|
|
|
|
/// created and live by the trigger, holds data for listbox: the state of the struct does not effect on member funcions, therefore all data members are public.
|
|
struct essence
|
|
{
|
|
enum class item_state{normal, highlighted, pressed, grabbed, floated};
|
|
enum class parts{unknown = -1, header, list, list_blank, checker};
|
|
|
|
::nana::listbox* listbox_ptr{nullptr};
|
|
::nana::listbox::scheme_type* scheme_ptr{nullptr};
|
|
::nana::paint::graphics *graph{nullptr};
|
|
bool auto_draw{true};
|
|
bool checkable{false};
|
|
bool if_image{false};
|
|
unsigned text_height;
|
|
|
|
::nana::listbox::export_options def_exp_options;
|
|
|
|
es_header header;
|
|
es_lister lister; // we have at least one emty cat. the #0
|
|
|
|
item_state ptr_state{ item_state::normal };
|
|
std::pair<parts, std::size_t> pointer_where; //The 'first' stands for which object, such as header and lister, 'second' stands for item
|
|
//if where == header, 'second' indicates the item
|
|
//if where == lister || where == checker, 'second' indicates the offset to the scroll offset_y which stands for the first item displayed in lister.
|
|
//if where == unknown, 'second' ignored.
|
|
|
|
std::unique_ptr<widgets::skeletons::content_view> content_view;
|
|
|
|
std::function<void(paint::graphics&, const rectangle&, bool)> ctg_icon_renderer; ///< Renderer for the category icon
|
|
|
|
struct operation_rep
|
|
{
|
|
operation_states state{operation_states::none};
|
|
index_pair item;
|
|
}operation;
|
|
|
|
struct mouse_selection_part
|
|
{
|
|
bool started{ false };
|
|
bool reverse_selection{ false };
|
|
|
|
point screen_pos;
|
|
point begin_position; ///< Logical position to the
|
|
point end_position;
|
|
index_pairs already_selected;
|
|
index_pairs selections;
|
|
|
|
bool scroll_direction;
|
|
unsigned scroll_step{ 1 };
|
|
unsigned mouse_move_timestamp{ 0 };
|
|
}mouse_selection;
|
|
|
|
|
|
std::map<pat::detail::abstract_factory_base*, std::deque<std::unique_ptr<inline_pane>>> inline_table, inline_buffered_table;
|
|
|
|
essence()
|
|
{
|
|
pointer_where.first = parts::unknown;
|
|
|
|
lister.fetch_ordering_comparer = [this](std::size_t pos) -> std::function<bool(const std::string&, nana::any*, const std::string&, nana::any*, bool reverse)>
|
|
{
|
|
return header.at(pos).weak_ordering;
|
|
};
|
|
}
|
|
|
|
bool cs_status(index_pair abs_pos, bool for_selection) const
|
|
{
|
|
if (abs_pos.is_category())
|
|
return lister.cat_status(abs_pos.cat, for_selection);
|
|
|
|
auto & flags = lister.get(abs_pos.cat)->items.at(abs_pos.item).flags;
|
|
return (for_selection ? flags.selected : flags.checked);
|
|
}
|
|
|
|
void resize_disp_area()
|
|
{
|
|
auto head_px = this->header_visible_px();
|
|
|
|
auto r = this->content_area();
|
|
r.y += head_px;
|
|
|
|
if (r.height <= head_px)
|
|
r.height = 0;
|
|
else
|
|
r.height -= head_px;
|
|
|
|
this->content_view->disp_area(r, { -1, 0 }, { 1, -static_cast<int>(head_px) }, { 2, head_px });
|
|
}
|
|
|
|
size_type column_from_pos(int screen_x) const
|
|
{
|
|
return header.column_from_point(screen_x - content_area().x + content_view->origin().x);
|
|
}
|
|
|
|
std::string to_string(const export_options& exp_opt) const
|
|
{
|
|
return header.to_string(exp_opt) + exp_opt.endl + lister.to_string(exp_opt) ;
|
|
}
|
|
|
|
int content_position(const index_pair& pos) const
|
|
{
|
|
return static_cast<int>(lister.distance(lister.first(), pos) * this->item_height());
|
|
}
|
|
|
|
index_pair first_display() const noexcept
|
|
{
|
|
return lister.advance(lister.first(), static_cast<int>(content_view->origin().y / item_height()));
|
|
}
|
|
|
|
unsigned item_height() const noexcept
|
|
{
|
|
auto px = (std::max)(scheme_ptr->item_height_ex + text_height, unsigned(1));
|
|
content_view->step(px, false);
|
|
return px;
|
|
}
|
|
|
|
point coordinate_cast(const point& from, bool from_screen) noexcept
|
|
{
|
|
rectangle orignal;
|
|
if (!rect_lister(orignal))
|
|
return{};
|
|
|
|
const auto origin = this->content_view->origin();
|
|
|
|
if (from_screen)
|
|
return from - orignal.position() + origin;
|
|
|
|
return from + orignal.position() - origin;
|
|
}
|
|
|
|
std::pair<int, int> columns_range() const
|
|
{
|
|
rectangle r;
|
|
if (!rect_header(r))
|
|
return{};
|
|
|
|
auto origin = content_view->origin();
|
|
return{ r.x - origin.x, r.x - origin.x + static_cast<int>(header.width_px()) };
|
|
}
|
|
|
|
void start_mouse_selection(const arg_mouse& arg)
|
|
{
|
|
auto logic_pos = coordinate_cast(arg.pos, true);
|
|
|
|
mouse_selection.started = true;
|
|
mouse_selection.begin_position = logic_pos;
|
|
mouse_selection.end_position = logic_pos;
|
|
|
|
if (arg.ctrl || arg.shift)
|
|
{
|
|
mouse_selection.already_selected = lister.pick_items(true);
|
|
mouse_selection.reverse_selection = arg.ctrl;
|
|
}
|
|
API::set_capture(*listbox_ptr, true);
|
|
}
|
|
|
|
void update_mouse_selection(const point& screen_pos)
|
|
{
|
|
//Don't update if it is not started
|
|
if (!mouse_selection.started)
|
|
return;
|
|
|
|
mouse_selection.screen_pos = screen_pos;
|
|
|
|
auto logic_pos = coordinate_cast(screen_pos, true);
|
|
auto imd_area = content_view->view_area();
|
|
|
|
if (logic_pos.y > mouse_selection.begin_position.y)
|
|
{
|
|
//The top of logic_pos shouldn't be less than the top of imd_area.
|
|
//This is a feature that listbox always shows the first displayed item on the screen as a selected item when
|
|
//move the cursor upwards.
|
|
logic_pos.y = (std::max)(logic_pos.y, coordinate_cast(imd_area.position(), true).y);
|
|
}
|
|
|
|
mouse_selection.end_position = logic_pos;
|
|
|
|
bool cancel_selections = true;
|
|
|
|
auto content_x = coordinate_cast({ columns_range().first, 0 }, true).x;
|
|
if ((std::max)(mouse_selection.end_position.x, mouse_selection.begin_position.x) >= content_x &&
|
|
(std::min)(mouse_selection.end_position.x, mouse_selection.begin_position.x) < content_x + static_cast<int>(header.width_px()))
|
|
{
|
|
auto const begin_off = (std::max)((std::min)(mouse_selection.begin_position.y, mouse_selection.end_position.y), 0) / item_height();
|
|
|
|
auto begin = lister.advance(lister.first(), begin_off);
|
|
if (!begin.empty())
|
|
{
|
|
std::vector<std::pair<index_pair, bool>> selections;
|
|
|
|
if ((mouse_selection.end_position.y < 0) || (lister.distance(lister.first(), begin) == begin_off))
|
|
{
|
|
//The range [begin_off, last_off] is a range of box selection
|
|
auto last_off = (std::max)(mouse_selection.begin_position.y, mouse_selection.end_position.y) / item_height();
|
|
auto last = lister.advance(lister.first(), last_off);
|
|
|
|
selections = lister.select_display_range_if(begin, last, false, [this](const index_pair& abs_pos) {
|
|
if (this->mouse_selection.reverse_selection)
|
|
{
|
|
if (mouse_selection.already_selected.cend() != std::find(mouse_selection.already_selected.cbegin(), mouse_selection.already_selected.cend(), abs_pos))
|
|
{
|
|
item_proxy{ this, abs_pos }.select(false);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
|
|
for (auto & pair : selections)
|
|
{
|
|
if (pair.second)
|
|
continue;
|
|
|
|
if (mouse_selection.selections.cend() ==
|
|
std::find(mouse_selection.selections.cbegin(), mouse_selection.selections.cend(), pair.first))
|
|
{
|
|
mouse_selection.selections.push_back(pair.first);
|
|
}
|
|
}
|
|
|
|
#ifdef _MSC_VER
|
|
for (auto i = mouse_selection.selections.cbegin(); i != mouse_selection.selections.cend();)
|
|
#else
|
|
for(auto i = mouse_selection.selections.begin(); i != mouse_selection.selections.end();)
|
|
|
|
#endif
|
|
{
|
|
auto & selpos = *i;
|
|
if (selections.cend() == std::find_if(selections.cbegin(), selections.cend(), /*pred_mouse_selection{ *i }*/[&selpos](const std::pair<index_pair, bool>& m){
|
|
return (selpos == m.first);
|
|
}))
|
|
{
|
|
item_proxy{ this, selpos }.select(false);
|
|
i = mouse_selection.selections.erase(i);
|
|
}
|
|
else
|
|
++i;
|
|
}
|
|
|
|
cancel_selections = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cancel_selections)
|
|
{
|
|
if (!mouse_selection.already_selected.empty())
|
|
{
|
|
for (auto & pos : mouse_selection.selections)
|
|
item_proxy(this, pos).select(false);
|
|
|
|
//Don't restore the already selections if it is reverse selection(pressing shift). Behaves like Windows Explorer.
|
|
if (!mouse_selection.reverse_selection)
|
|
{
|
|
for (auto & abs_pos : mouse_selection.already_selected)
|
|
item_proxy(this, abs_pos).select(true);
|
|
}
|
|
}
|
|
else
|
|
lister.select_for_all(false);
|
|
|
|
mouse_selection.selections.clear();
|
|
}
|
|
}
|
|
|
|
void stop_mouse_selection() noexcept
|
|
{
|
|
mouse_selection.started = false;
|
|
API::release_capture(*listbox_ptr);
|
|
mouse_selection.begin_position = mouse_selection.end_position;
|
|
mouse_selection.already_selected.clear();
|
|
mouse_selection.selections.clear();
|
|
}
|
|
|
|
/// Returns the number of items that are contained on on screen
|
|
/**
|
|
* @pram with_rest Indicates whether the extra one item which is not completely contained in rest pixels to be included.
|
|
* @return The number of items
|
|
*/
|
|
size_type count_of_exposed(bool with_rest) const
|
|
{
|
|
auto lister_s = this->content_view->view_area().height;
|
|
return (lister_s / item_height()) + (with_rest && (lister_s % item_height()) ? 1 : 0);
|
|
}
|
|
|
|
void update(bool ignore_auto_draw = false) noexcept
|
|
{
|
|
if((auto_draw || ignore_auto_draw) && lister.wd_ptr())
|
|
{
|
|
calc_content_size(false);
|
|
API::refresh_window(lister.wd_ptr()->handle());
|
|
}
|
|
}
|
|
|
|
::nana::size calc_content_size(bool try_update = true)
|
|
{
|
|
size ctt_size(
|
|
this->header.width_px() + this->header.margin(),
|
|
static_cast<size::value_type>(this->lister.the_number_of_expanded()) * this->item_height()
|
|
);
|
|
|
|
this->content_view->content_size(ctt_size, try_update);
|
|
|
|
return ctt_size;
|
|
}
|
|
|
|
nana::rectangle checkarea(int x, int y) const noexcept /// move to scheme ?? 16 ?
|
|
{
|
|
return nana::rectangle(x + 4, y + (static_cast<int>(item_height()) - 16) / 2, 16, 16);
|
|
}
|
|
|
|
int item_xpos(const nana::rectangle& r) const
|
|
{
|
|
auto seq = ordered_columns(r.width);
|
|
|
|
if (seq.empty())
|
|
return 0;
|
|
|
|
return header.range(seq.front()).first + r.x - this->content_view->origin().x;
|
|
}
|
|
|
|
//Returns the top of the specified item in listbox window.
|
|
int item_window_top(const index_pair& pos) const
|
|
{
|
|
int top = static_cast<int>(this->lister.distance(index_pair{}, pos) * item_height()) - content_view->origin().y;
|
|
|
|
rectangle r;
|
|
if (rect_lister(r))
|
|
top += r.y;
|
|
|
|
return top;
|
|
}
|
|
|
|
std::pair<parts, size_t> where(const nana::point& pos) const noexcept
|
|
{
|
|
std::pair<parts, size_t> new_where{ parts::unknown, npos };
|
|
|
|
const auto area = this->content_area();
|
|
|
|
if(area.is_hit(pos))
|
|
{ /// we are inside
|
|
auto const origin = content_view->origin();
|
|
|
|
if (header.attrib().visible && (pos.y < static_cast<int>(header_visible_px()) + area.y))
|
|
{ /// we are in the header
|
|
new_where.first = parts::header;
|
|
new_where.second = this->column_from_pos(pos.x);
|
|
}
|
|
else if (area.x <= pos.x + origin.x && pos.x + origin.x < area.x + static_cast<int>(header.width_px()))
|
|
{
|
|
// detect if cursor is in the area of header margin
|
|
if (pos.x < area.x - origin.x + static_cast<int>(header.margin()))
|
|
return{ parts::list_blank, npos };
|
|
|
|
new_where.first = parts::list;
|
|
|
|
auto const item_h = item_height();
|
|
//don't combine the following formula into the (pos.y - area.y - header_visible_px()) / item_h
|
|
new_where.second = ((pos.y - area.y - header_visible_px() + origin.y) / item_h) - (origin.y / item_h);
|
|
|
|
if (this->lister.the_number_of_expanded() < new_where.second + 1)
|
|
return{ parts::list_blank, npos };
|
|
|
|
if (checkable)
|
|
{
|
|
nana::rectangle r;
|
|
if (rect_lister(r))
|
|
{
|
|
auto top = new_where.second * item_h + header_visible_px();
|
|
if (checkarea(item_xpos(r), static_cast<int>(top)).is_hit(pos))
|
|
new_where.first = parts::checker;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
new_where.first = parts::list_blank;
|
|
}
|
|
|
|
return new_where;
|
|
}
|
|
|
|
bool calc_where(const point& pos) noexcept
|
|
{
|
|
auto new_where = where(pos);
|
|
if (new_where == pointer_where)
|
|
return false;
|
|
|
|
pointer_where = new_where;
|
|
return true;
|
|
}
|
|
|
|
void widget_to_header(nana::point& pos) noexcept
|
|
{
|
|
--pos.y;
|
|
pos.x += this->content_view->origin().x - 2;
|
|
}
|
|
|
|
void draw_peripheral()
|
|
{
|
|
auto ctt_area = this->content_area();
|
|
|
|
if (!API::widget_borderless(*lister.wd_ptr()))
|
|
{
|
|
//Draw Border
|
|
graph->rectangle(false, static_cast<color_rgb>(0x9cb6c5));
|
|
|
|
graph->line({ ctt_area.x - 1, ctt_area.y }, { ctt_area.x - 1, ctt_area.bottom() - 1 }, colors::white);
|
|
graph->line({ ctt_area.right(), ctt_area.y }, { ctt_area.right(), ctt_area.bottom() - 1 });
|
|
}
|
|
|
|
this->content_view->draw_corner(*graph);
|
|
}
|
|
|
|
rectangle content_area() const
|
|
{
|
|
rectangle r{ graph->size() };
|
|
|
|
if (!this->listbox_ptr->borderless())
|
|
{
|
|
r.x = 2;
|
|
r.width -= (r.width > 4 ? 4 : r.width);
|
|
|
|
r.y = 1;
|
|
r.height -= (r.height > 2 ? 2 : r.height);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
double header_font_px() const
|
|
{
|
|
unsigned max_font_px = 0;
|
|
|
|
paint::graphics graph{ size{ 1, 1 } };
|
|
for (auto & col : this->header.cont())
|
|
{
|
|
if (!col.visible())
|
|
continue;
|
|
|
|
graph.typeface(col.typeface());
|
|
|
|
unsigned as, ds, ileading;
|
|
graph.text_metrics(as, ds, ileading);
|
|
|
|
max_font_px = (std::max)(as + ds, max_font_px);
|
|
}
|
|
|
|
if (0 == max_font_px)
|
|
{
|
|
graph.typeface(listbox_ptr->typeface());
|
|
unsigned as, ds, ileading;
|
|
graph.text_metrics(as, ds, ileading);
|
|
|
|
max_font_px = (std::max)(as + ds, max_font_px);
|
|
}
|
|
|
|
return max_font_px;
|
|
}
|
|
|
|
unsigned header_visible_px() const
|
|
{
|
|
if(header.attrib().visible)
|
|
return scheme_ptr->header_padding_top + scheme_ptr->header_padding_bottom + static_cast<unsigned>(header_font_px());
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool rect_header(nana::rectangle& r) const
|
|
{
|
|
if(header.attrib().visible)
|
|
{
|
|
r = this->content_area();
|
|
|
|
r.height = header_visible_px();
|
|
|
|
if (lister.wd_ptr()->borderless())
|
|
return !r.empty();
|
|
|
|
auto exs = this->content_view->extra_space(false);
|
|
const unsigned ex_width = (exs ? exs - 1 : 0);
|
|
|
|
if(r.width > ex_width)
|
|
{
|
|
r.width -= ex_width;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool rect_lister(nana::rectangle& r) const
|
|
{
|
|
auto head_pixels = header_visible_px();
|
|
|
|
auto exs_vert = this->content_view->extra_space(false);
|
|
auto exs_horz = this->content_view->extra_space(true);
|
|
|
|
unsigned extr_w = (exs_vert ? exs_vert - 1 : 0);
|
|
unsigned extr_h = exs_horz + head_pixels;
|
|
|
|
r = this->content_area();
|
|
|
|
if (r.width <= extr_w || r.height <= extr_h)
|
|
return false;
|
|
|
|
r.y += head_pixels;
|
|
|
|
r.width -= extr_w;
|
|
r.height -= extr_h;
|
|
|
|
return true;
|
|
}
|
|
|
|
std::vector<size_type> ordered_columns(unsigned lister_w) const
|
|
{
|
|
std::vector<size_type> seqs;
|
|
int x = -content_view->origin().x;
|
|
|
|
for (const auto& col : header.cont())
|
|
{
|
|
if (!col.visible_state)
|
|
continue;
|
|
|
|
x += col.width_px;
|
|
if (x > 0)
|
|
{
|
|
seqs.push_back(col.index);
|
|
|
|
if (x >= static_cast<int>(lister_w))
|
|
break;
|
|
}
|
|
}
|
|
return seqs;
|
|
}
|
|
|
|
inline_pane * open_inline(pat::abstract_factory<inline_notifier_interface>* factory, inline_indicator* indicator)
|
|
{
|
|
std::unique_ptr<inline_pane> pane_ptr;
|
|
auto i = inline_buffered_table.find(factory);
|
|
if (i != inline_buffered_table.end())
|
|
{
|
|
auto & panes = i->second;
|
|
if (!panes.empty())
|
|
{
|
|
pane_ptr = std::move(panes.front());
|
|
panes.pop_front();
|
|
}
|
|
}
|
|
|
|
if (!pane_ptr)
|
|
{
|
|
pane_ptr.reset(new inline_pane);
|
|
pane_ptr->indicator = indicator;
|
|
pane_ptr->pane_bottom.create(this->lister.wd_ptr()->handle());
|
|
pane_ptr->pane_widget.create(pane_ptr->pane_bottom);
|
|
pane_ptr->inline_ptr = factory->create();
|
|
pane_ptr->inline_ptr->create(pane_ptr->pane_widget);
|
|
}
|
|
|
|
auto ptr = pane_ptr.get();
|
|
inline_table[factory].emplace_back(std::move(pane_ptr));
|
|
return ptr;
|
|
}
|
|
};
|
|
|
|
//definition of iresolver/oresolver
|
|
oresolver::oresolver(essence* ess) noexcept
|
|
: ess_(ess)
|
|
{}
|
|
|
|
oresolver& oresolver::operator<<(bool n)
|
|
{
|
|
cells_.emplace_back(std::string(n ? "true" : "false"));
|
|
return *this;
|
|
}
|
|
oresolver& oresolver::operator<<(short n)
|
|
{
|
|
cells_.emplace_back(std::to_string(n));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(unsigned short n)
|
|
{
|
|
cells_.emplace_back(std::to_string(n));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(int n)
|
|
{
|
|
cells_.emplace_back(std::to_string(n));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(unsigned int n)
|
|
{
|
|
cells_.emplace_back(std::to_string(n));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(long n)
|
|
{
|
|
cells_.emplace_back(std::to_string(n));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(unsigned long n)
|
|
{
|
|
cells_.emplace_back(std::to_string(n));
|
|
return *this;
|
|
}
|
|
oresolver& oresolver::operator<<(long long n)
|
|
{
|
|
cells_.emplace_back(std::to_string(n));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(unsigned long long n)
|
|
{
|
|
cells_.emplace_back(std::to_string(n));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(float f)
|
|
{
|
|
cells_.emplace_back(std::to_string(f));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(double f)
|
|
{
|
|
cells_.emplace_back(std::to_string(f));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(long double f)
|
|
{
|
|
cells_.emplace_back(std::to_string(f));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(const char* text)
|
|
{
|
|
cells_.emplace_back(std::string(text));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(const wchar_t* text)
|
|
{
|
|
cells_.emplace_back(to_utf8(text));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(const std::string& text)
|
|
{
|
|
cells_.emplace_back(text);
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(const std::wstring& text)
|
|
{
|
|
cells_.emplace_back(to_utf8(text));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(std::wstring&& text)
|
|
{
|
|
cells_.emplace_back(to_utf8(text));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(cell cl)
|
|
{
|
|
cells_.emplace_back(std::move(cl));
|
|
return *this;
|
|
}
|
|
|
|
oresolver& oresolver::operator<<(std::nullptr_t)
|
|
{
|
|
#ifdef _nana_std_has_emplace_return_type
|
|
cells_.emplace_back().text.assign(1, wchar_t{});
|
|
#else
|
|
cells_.emplace_back();
|
|
cells_.back().text.assign(1, wchar_t(0)); //means invalid cell
|
|
#endif
|
|
return *this;
|
|
}
|
|
|
|
std::vector<cell>&& oresolver::move_cells() noexcept
|
|
{
|
|
return std::move(cells_);
|
|
}
|
|
|
|
::nana::listbox& oresolver::listbox() noexcept
|
|
{
|
|
return *ess_->listbox_ptr;
|
|
}
|
|
|
|
iresolver& iresolver::operator>>(bool& n)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
n = (std::stoi(cells_[pos_++].text) == 0);
|
|
return *this;
|
|
}
|
|
|
|
iresolver& iresolver::operator>>(short& n)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
n = std::stoi(cells_[pos_++].text);
|
|
return *this;
|
|
}
|
|
|
|
iresolver& iresolver::operator>>(unsigned short& n)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
n = static_cast<unsigned short>(std::stoul(cells_[pos_++].text));
|
|
return *this;
|
|
}
|
|
|
|
iresolver& iresolver::operator>>(int& n)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
n = std::stoi(cells_[pos_++].text);
|
|
return *this;
|
|
}
|
|
|
|
iresolver& iresolver::operator>>(unsigned int& n)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
n = std::stoul(cells_[pos_++].text);
|
|
return *this;
|
|
}
|
|
|
|
iresolver& iresolver::operator>>(long& n)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
n = std::stol(cells_[pos_++].text);
|
|
return *this;
|
|
}
|
|
|
|
iresolver& iresolver::operator>>(unsigned long& n)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
n = std::stoul(cells_[pos_++].text);
|
|
return *this;
|
|
}
|
|
|
|
iresolver& iresolver::operator>>(long long& n)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
n = std::stoll(cells_[pos_++].text);
|
|
return *this;
|
|
}
|
|
iresolver& iresolver::operator>>(unsigned long long& n)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
n = std::stoull(cells_[pos_++].text);
|
|
return *this;
|
|
}
|
|
|
|
iresolver& iresolver::operator>>(float& f)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
f = std::stof(cells_[pos_++].text);
|
|
return *this;
|
|
}
|
|
|
|
|
|
iresolver& iresolver::operator>>(double& f)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
f = std::stod(cells_[pos_++].text);
|
|
return *this;
|
|
}
|
|
|
|
iresolver& iresolver::operator>>(long double& f)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
f = std::stold(cells_[pos_++].text);
|
|
return *this;
|
|
}
|
|
|
|
iresolver& iresolver::operator>>(std::string& text)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
text = cells_[pos_++].text;
|
|
return *this;
|
|
}
|
|
|
|
iresolver& iresolver::operator>>(std::wstring& text)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
text = to_wstring(cells_[pos_++].text);
|
|
|
|
return *this;
|
|
}
|
|
|
|
iresolver::iresolver(std::vector<cell> cl) noexcept
|
|
: cells_(std::move(cl))
|
|
{}
|
|
|
|
iresolver& iresolver::operator>>(cell& cl)
|
|
{
|
|
if (pos_ < cells_.size())
|
|
cl = cells_[pos_++];
|
|
return *this;
|
|
}
|
|
|
|
iresolver& iresolver::operator>>(std::nullptr_t) noexcept
|
|
{
|
|
++pos_;
|
|
return *this;
|
|
}
|
|
//end class iresolver/oresolver
|
|
|
|
unsigned es_lister::column_content_pixels(size_type pos) const
|
|
{
|
|
unsigned max_px = 0;
|
|
|
|
std::unique_ptr<paint::graphics> graph_helper;
|
|
auto graph = ess_->graph;
|
|
if (graph->empty())
|
|
{
|
|
//Creates a helper if widget graph is empty(when its size is 0).
|
|
graph_helper.reset(new paint::graphics{ nana::size{ 5, 5 } });
|
|
graph_helper->typeface(ess_->graph->typeface());
|
|
graph = graph_helper.get();
|
|
}
|
|
|
|
for (auto & cat : categories_)
|
|
{
|
|
for (std::size_t i = 0; i < cat.items.size(); ++i)
|
|
{
|
|
unsigned content_px = 0;
|
|
if (cat.model_ptr)
|
|
{
|
|
auto model_cells = cat.model_ptr->container()->to_cells(i);
|
|
if (pos >= model_cells.size())
|
|
continue;
|
|
|
|
content_px = graph->text_extent_size(model_cells[pos].text).width;
|
|
}
|
|
else
|
|
{
|
|
if (pos >= cat.items[i].cells->size())
|
|
continue;
|
|
|
|
content_px = graph->text_extent_size((*cat.items[i].cells)[pos].text).width;
|
|
}
|
|
|
|
if (content_px > max_px)
|
|
max_px = content_px;
|
|
}
|
|
}
|
|
return max_px;
|
|
}
|
|
|
|
//es_header::column member functions
|
|
void es_header::column::_m_refresh() noexcept
|
|
{
|
|
ess_->update(true);
|
|
}
|
|
|
|
size_type es_header::column::position(bool disp_order) const noexcept
|
|
{
|
|
return (disp_order ? ess_->header.cast(index, false) : index);
|
|
}
|
|
|
|
void es_header::column::fit_content(unsigned maximize) noexcept
|
|
{
|
|
auto content_px = ess_->lister.column_content_pixels(index);
|
|
|
|
if (0 == content_px)
|
|
return;
|
|
|
|
content_px += (ess_->scheme_ptr->text_margin * 2); //margin at left/right end.
|
|
|
|
if (index == 0 && ess_->checkable) // only before the first column (display_order=0 ?)
|
|
content_px += 18; // add to geom. scheme (width of the checker) ??
|
|
|
|
if (range_width_px.first != range_width_px.second)
|
|
{
|
|
if (range_width_px.first > content_px)
|
|
content_px = range_width_px.first;
|
|
|
|
//Use range_width_px defined max if maximize is unspecified
|
|
if (0 == maximize)
|
|
maximize = range_width_px.second;
|
|
}
|
|
|
|
if (0 == maximize)
|
|
maximize = ess_->scheme_ptr->max_fit_content;
|
|
|
|
//maximize is only available when it > 0
|
|
if (maximize && (content_px > maximize))
|
|
content_px = maximize;
|
|
|
|
width_px = content_px;
|
|
|
|
_m_refresh();
|
|
}
|
|
|
|
/// Sets an exclusive font for the column
|
|
void es_header::column::typeface(const paint::font& column_font)
|
|
{
|
|
this->font.reset(new paint::font{ column_font });
|
|
|
|
API::refresh_window(*ess_->listbox_ptr);
|
|
}
|
|
|
|
/// Returns a font
|
|
paint::font es_header::column::typeface() const noexcept
|
|
{
|
|
//Returns the exclusive font if it is not empty
|
|
if (this->font && !this->font->empty())
|
|
return *this->font;
|
|
|
|
//Returns the column font if it is not empty
|
|
if (ess_->scheme_ptr->column_font && !ess_->scheme_ptr->column_font)
|
|
return *(ess_->scheme_ptr->column_font);
|
|
|
|
//If all above fonts are invalid, returns the widget font.
|
|
return ess_->listbox_ptr->typeface();
|
|
}
|
|
//end es_header::column functions
|
|
|
|
class inline_indicator
|
|
: public ::nana::detail::inline_widget_indicator<index_pair, std::string>
|
|
{
|
|
public:
|
|
using parts = essence::parts;
|
|
|
|
inline_indicator(essence* ess, std::size_t column_pos)
|
|
: ess_{ ess }, column_pos_{column_pos}
|
|
{
|
|
}
|
|
|
|
void attach(index_type pos, inline_pane* pane)
|
|
{
|
|
for (auto & pn : panes_)
|
|
{
|
|
if (pn.first == pos)
|
|
{
|
|
pn.second = pane;
|
|
return;
|
|
}
|
|
}
|
|
panes_.emplace_back(std::make_pair(pos, pane));
|
|
}
|
|
|
|
void detach() noexcept
|
|
{
|
|
panes_.clear();
|
|
}
|
|
public:
|
|
//Implement inline_widget_indicator
|
|
::nana::widget& host() const override
|
|
{
|
|
return *ess_->lister.wd_ptr();
|
|
}
|
|
|
|
std::size_t column() const override
|
|
{
|
|
return column_pos_;
|
|
}
|
|
|
|
void modify(index_type pos, const value_type& value) const override
|
|
{
|
|
ess_->lister.throw_if_immutable_model(pos);
|
|
|
|
auto model_cells = ess_->lister.at_model_abs(pos);
|
|
auto & cells = ess_->lister.has_model(pos) ? model_cells : (*ess_->lister.at_abs(pos).cells);
|
|
|
|
if (cells.size() <= column_pos_)
|
|
cells.resize(column_pos_ + 1);
|
|
|
|
if (cells[column_pos_].text != value)
|
|
{
|
|
cells[column_pos_].text = value;
|
|
|
|
if (model_cells.size())
|
|
ess_->lister.assign_model(pos, model_cells);
|
|
|
|
ess_->update();
|
|
}
|
|
}
|
|
|
|
void selected(index_type pos) override
|
|
{
|
|
if (ess_->lister.at(pos).flags.selected)
|
|
return;
|
|
ess_->lister.select_for_all(false);
|
|
cat_proxy(ess_, pos.cat).at(pos.item).select(true);
|
|
}
|
|
|
|
void hovered(index_type pos) override
|
|
{
|
|
auto offset = ess_->lister.distance(ess_->first_display(), ess_->lister.index_cast(pos, false));
|
|
|
|
if (ess_->pointer_where.first != parts::list || ess_->pointer_where.second != offset)
|
|
{
|
|
ess_->pointer_where.first = parts::list;
|
|
ess_->pointer_where.second = offset;
|
|
ess_->update();
|
|
}
|
|
}
|
|
private:
|
|
essence * const ess_;
|
|
const std::size_t column_pos_;
|
|
std::vector<std::pair<index_type, inline_pane*>> panes_;
|
|
};
|
|
|
|
void es_lister::scroll_into_view(const index_pair& abs_pos, view_action vw_act)
|
|
{
|
|
auto& cat = *get(abs_pos.cat);
|
|
|
|
if ((abs_pos.item != nana::npos) && (abs_pos.item >= cat.items.size()))
|
|
throw std::invalid_argument("listbox: invalid pos to scroll");
|
|
|
|
if (!cat.expand)
|
|
{
|
|
this->expand(abs_pos.cat, true);
|
|
ess_->calc_content_size();
|
|
}
|
|
else if (!ess_->auto_draw)
|
|
{
|
|
//force a update of content size and scrollbar status when auto_draw is false to make
|
|
//sure that the scroll function works fine.
|
|
ess_->calc_content_size();
|
|
}
|
|
|
|
auto origin = ess_->content_view->origin();
|
|
|
|
auto off = this->distance(this->first(), this->index_cast(abs_pos, false)) * ess_->item_height();
|
|
|
|
auto screen_px = ess_->content_view->view_area().height;
|
|
|
|
if (view_action::auto_view == vw_act)
|
|
{
|
|
if (static_cast<int>(off) < origin.y)
|
|
vw_act = view_action::top_view;
|
|
else if (static_cast<int>(off) >= static_cast<int>(origin.y + ess_->content_view->view_area().height))
|
|
vw_act = view_action::bottom_view;
|
|
else
|
|
return;
|
|
}
|
|
|
|
origin.y = 0;
|
|
|
|
if (view_action::bottom_view == vw_act)
|
|
{
|
|
off += ess_->item_height();
|
|
if (off >= screen_px)
|
|
origin.y = static_cast<int>(off - screen_px);
|
|
}
|
|
else
|
|
{
|
|
auto const content_px = ess_->content_view->content_size().height;
|
|
if (content_px - off >= screen_px)
|
|
origin.y = static_cast<int>(off);
|
|
else if (content_px >= screen_px)
|
|
origin.y = static_cast<int>(content_px - screen_px);
|
|
}
|
|
|
|
if (ess_->content_view->move_origin(origin - ess_->content_view->origin()))
|
|
ess_->content_view->sync(false);
|
|
}
|
|
|
|
void es_lister::erase(const index_pair& pos)
|
|
{
|
|
auto & cat = *get(pos.cat);
|
|
if (pos.item < cat.items.size())
|
|
{
|
|
if (cat.model_ptr)
|
|
{
|
|
throw_if_immutable_model(cat.model_ptr.get());
|
|
cat.model_ptr->container()->erase(pos.item);
|
|
}
|
|
|
|
cat.items.erase(cat.items.begin() + pos.item);
|
|
cat.sorted.erase(std::find(cat.sorted.begin(), cat.sorted.end(), cat.items.size()));
|
|
|
|
sort();
|
|
}
|
|
}
|
|
|
|
void es_lister::move_select(bool upwards, bool unselect_previous, bool into_view) noexcept
|
|
{
|
|
auto next_selected_dpl = index_cast_noexcept(latest_selected_abs, false); //convert absolute position to display position
|
|
|
|
if (next_selected_dpl.empty()) // has no cat ? (cat == npos) => beging from first cat
|
|
{
|
|
bool good = false;
|
|
for (size_type i = 0, size = categories_.size(); i < size; ++i) // run all cat
|
|
{
|
|
if(size_item(i))
|
|
{
|
|
//The first category which contains at least one item.
|
|
next_selected_dpl.cat = i;
|
|
next_selected_dpl.item = 0;
|
|
good = true;
|
|
break;
|
|
}
|
|
}
|
|
if(! good ) return; // items in listbox : nothing to select (and an empty but visible cat?)
|
|
}
|
|
|
|
//start moving
|
|
while(true)
|
|
{
|
|
if(upwards == false)
|
|
{
|
|
if(good(next_selected_dpl.cat))
|
|
{
|
|
if (size_item(next_selected_dpl.cat) > next_selected_dpl.item + 1)
|
|
{
|
|
++next_selected_dpl.item;
|
|
}
|
|
else
|
|
{
|
|
next_selected_dpl.item = 0;
|
|
if (categories_.size() > next_selected_dpl.cat + 1)
|
|
++next_selected_dpl.cat;
|
|
else
|
|
next_selected_dpl.cat = 0;
|
|
}
|
|
}
|
|
else
|
|
next_selected_dpl.set_both(0);
|
|
}
|
|
else
|
|
{
|
|
if (0 == next_selected_dpl.item)
|
|
{
|
|
//there is an item at least definitely, because the start pos is an available item.
|
|
do
|
|
{
|
|
if (0 == next_selected_dpl.cat)
|
|
next_selected_dpl.cat = categories_.size() - 1;
|
|
else
|
|
--next_selected_dpl.cat;
|
|
|
|
}while (0 == size_item(next_selected_dpl.cat));
|
|
|
|
next_selected_dpl.item = size_item(next_selected_dpl.cat) - 1;
|
|
}
|
|
else
|
|
--next_selected_dpl.item;
|
|
}
|
|
|
|
if (good(next_selected_dpl.cat))
|
|
{
|
|
expand(next_selected_dpl.cat, true); // revise expand
|
|
|
|
if (good(next_selected_dpl))
|
|
{
|
|
if (unselect_previous && !single_selection_)
|
|
select_for_all(false);
|
|
|
|
/// is ignored if no change (maybe set last_selected anyway??), but if change emit event, deselect others if need ans set/unset last_selected
|
|
item_proxy::from_display(ess_, next_selected_dpl).select(true);
|
|
}
|
|
break;
|
|
}
|
|
else break;
|
|
}
|
|
|
|
if (into_view && !latest_selected_abs.empty())
|
|
this->scroll_into_view(latest_selected_abs, view_action::auto_view);
|
|
}
|
|
|
|
std::string es_lister::to_string(const export_options& exp_opt) const
|
|
{
|
|
std::string list_str;
|
|
bool first{true};
|
|
for(auto & cat: cat_container())
|
|
{
|
|
if(first)
|
|
first=false;
|
|
else
|
|
list_str += (to_utf8(cat.text) + exp_opt.endl);
|
|
|
|
std::vector<cell> model_cells;
|
|
|
|
auto const pcell = (cat.model_ptr ? &model_cells : nullptr);
|
|
|
|
for (auto i : cat.sorted)
|
|
{
|
|
auto& item = cat.items[i];
|
|
if (item.flags.selected || !exp_opt.only_selected_items)
|
|
{
|
|
//Test if the category have a model set.
|
|
if (pcell)
|
|
cat.model_ptr->container()->to_cells(i).swap(model_cells);
|
|
|
|
list_str += (item.to_string(exp_opt, pcell) + exp_opt.endl);
|
|
}
|
|
}
|
|
}
|
|
return list_str ;
|
|
}
|
|
|
|
bool es_lister::cat_status(size_type pos, bool for_selection, bool value)
|
|
{
|
|
bool changed = false;
|
|
|
|
if (for_selection)
|
|
{
|
|
cat_proxy cpx{ ess_, pos };
|
|
for (item_proxy &it : cpx)
|
|
it.select(value);
|
|
|
|
latest_selected_abs.cat = pos;
|
|
latest_selected_abs.item = npos;
|
|
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
auto & items = get(pos)->items;
|
|
size_type index = 0;
|
|
for (auto & m : items)
|
|
{
|
|
if (m.flags.checked != value)
|
|
{
|
|
m.flags.checked = value;
|
|
this->emit_cs(index_pair{ pos, index }, false);
|
|
changed = true;
|
|
}
|
|
++index;
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
class drawer_header_impl
|
|
{
|
|
public:
|
|
using graph_reference = nana::paint::graphics&;
|
|
using item_state = essence::item_state;
|
|
using parts = essence::parts;
|
|
|
|
struct column_rendering_parameter
|
|
{
|
|
unsigned margin;
|
|
unsigned height;
|
|
double max_font_px;
|
|
paint::font wdg_font;
|
|
};
|
|
|
|
drawer_header_impl(essence* es) noexcept: essence_(es){}
|
|
|
|
size_type splitter() const noexcept
|
|
{
|
|
return grabs_.splitter;
|
|
}
|
|
|
|
void cancel_splitter() noexcept
|
|
{
|
|
grabs_.splitter = npos;
|
|
}
|
|
|
|
// Detects a header spliter, return true if x is in the splitter area after that header item (column)
|
|
bool detect_splitter(int x) noexcept
|
|
{
|
|
nana::rectangle r;
|
|
if (!essence_->rect_header(r))
|
|
return false;
|
|
|
|
if(essence_->ptr_state == item_state::highlighted)
|
|
{
|
|
x -= r.x - essence_->content_view->origin().x + static_cast<int>(essence_->header.margin());
|
|
|
|
for(auto & col : essence_->header.cont()) // in current order
|
|
{
|
|
if(col.visible_state)
|
|
{
|
|
if ((static_cast<int>(col.width_px) < x + static_cast<int>(essence_->scheme_ptr->header_splitter_area_before))
|
|
&& (x < static_cast<int>(col.width_px) + static_cast<int>(essence_->scheme_ptr->header_splitter_area_after)))
|
|
{
|
|
grabs_.splitter = col.index; // original index
|
|
return true;
|
|
}
|
|
x -= static_cast<int>(col.width_px);
|
|
}
|
|
}
|
|
}
|
|
else if(essence_->ptr_state == item_state::normal)
|
|
grabs_.splitter = npos;
|
|
return false;
|
|
}
|
|
|
|
void grab(const nana::point& pos, bool is_grab)
|
|
{
|
|
if(is_grab)
|
|
{
|
|
grabs_.start_pos = pos.x;
|
|
if(grabs_.splitter != npos) // resize header item, not move it
|
|
grabs_.item_width = essence_->header.at(grabs_.splitter).width_px;
|
|
}
|
|
else if((grab_terminal_.index != npos) && (grab_terminal_.index != essence_->pointer_where.second))
|
|
essence_->header.move(essence_->pointer_where.second, grab_terminal_.index, grab_terminal_.place_front);
|
|
}
|
|
|
|
//grab_move
|
|
/// @brief draw when an item is grabbing.
|
|
/// @return true if refresh is needed, false otherwise
|
|
bool grab_move(const nana::point& pos)
|
|
{
|
|
if(npos == grabs_.splitter)
|
|
{ // move column, not resize it
|
|
options_.grab_column = true;
|
|
options_.grab_column_position = pos;
|
|
return true;
|
|
}
|
|
else
|
|
{ // resize column, not move it
|
|
auto& col = essence_->header.at(grabs_.splitter);
|
|
|
|
auto delta_px = (grabs_.start_pos - pos.x);
|
|
|
|
//Resize the item specified by item_spliter_.
|
|
auto new_w = static_cast<int>(grabs_.item_width) > delta_px ? grabs_.item_width - delta_px : 0;
|
|
|
|
//Check the minimized and maximized value
|
|
if (col.range_width_px.first != col.range_width_px.second)
|
|
{
|
|
//Column ranged width
|
|
new_w = std::clamp(static_cast<int>(new_w), static_cast<int>(col.range_width_px.first), static_cast<int>(col.range_width_px.second));
|
|
}
|
|
else
|
|
{
|
|
//Default scheme
|
|
new_w = (std::max)(new_w, essence_->scheme_ptr->suspension_width + essence_->scheme_ptr->min_column_width);
|
|
}
|
|
|
|
if(col.width_px != new_w)
|
|
{
|
|
col.width_px = new_w;
|
|
essence_->calc_content_size();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void draw(graph_reference graph, const nana::rectangle& r)
|
|
{
|
|
const auto border_color = essence_->scheme_ptr->header_bgcolor.get_color().blend(colors::black, 0.2);
|
|
|
|
auto text_color = essence_->scheme_ptr->header_fgcolor.get_color();
|
|
|
|
auto state = item_state::normal;
|
|
//check whether grabing an item, if item_spliter_ != npos, that indicates the grab item is a spliter.
|
|
if ((parts::header == essence_->pointer_where.first) && (npos == grabs_.splitter))
|
|
state = essence_->ptr_state;
|
|
|
|
rectangle column_r{
|
|
r.x - essence_->content_view->origin().x, r.y,
|
|
0, r.height - 1
|
|
};
|
|
|
|
column_rendering_parameter crp;
|
|
|
|
//The first item includes the margin
|
|
crp.margin = essence_->header.margin();
|
|
|
|
crp.height = essence_->header_visible_px();
|
|
crp.max_font_px = essence_->header_font_px();
|
|
crp.wdg_font = graph.typeface();
|
|
|
|
for (auto & col : essence_->header.cont())
|
|
{
|
|
if (col.visible_state)
|
|
{
|
|
column_r.width = col.width_px + crp.margin;
|
|
|
|
const auto right_pos = column_r.right();
|
|
|
|
//Make sure the column is in the display area.
|
|
if (right_pos > r.x)
|
|
{
|
|
_m_draw_header_item(graph, crp, column_r, text_color, col, (col.index == essence_->pointer_where.second ? state : item_state::normal));
|
|
graph.line({ right_pos - 1, r.y }, { right_pos - 1, r.bottom() - 2 }, border_color);
|
|
}
|
|
|
|
crp.margin = 0;
|
|
|
|
column_r.x = right_pos;
|
|
if (right_pos > r.right())
|
|
break;
|
|
}
|
|
}
|
|
|
|
//If the last rendered column's right point doesn't reach at r.right, fill the spare space.
|
|
if (column_r.x < r.right())
|
|
{
|
|
column_r.width = (r.right() - column_r.x);
|
|
if(API::dev::copy_transparent_background(essence_->listbox_ptr->handle(), column_r, graph, column_r.position()))
|
|
graph.blend(column_r, essence_->scheme_ptr->header_bgcolor, 0.5);
|
|
else
|
|
graph.rectangle(column_r, true, essence_->scheme_ptr->header_bgcolor);
|
|
}
|
|
|
|
const int y = r.bottom() - 1;
|
|
graph.line({ r.x, y }, { r.right(), y }, border_color);
|
|
|
|
if (options_.grab_column)
|
|
{
|
|
_m_make_float(r, options_.grab_column_position); // now draw one floating header item
|
|
|
|
//Draw the target strip
|
|
grab_terminal_.index = _m_target_strip(options_.grab_column_position.x, r, essence_->pointer_where.second, grab_terminal_.place_front);
|
|
|
|
options_.grab_column = false;
|
|
}
|
|
}
|
|
private:
|
|
size_type _m_target_strip(int x, const nana::rectangle& rect, size_type grab, bool& place_front)
|
|
{
|
|
//convert x to header logic coordinate.
|
|
auto const x_offset = essence_->content_view->origin().x;
|
|
|
|
x = std::clamp(x, x_offset, x_offset + static_cast<int>(rect.width));
|
|
|
|
auto i = essence_->header.column_from_point(x);
|
|
|
|
if (i == npos)
|
|
i = essence_->header.boundary(essence_->header.range(grab).first >= x);
|
|
|
|
if(grab != i)
|
|
{
|
|
auto item_rg = essence_->header.range(i);
|
|
|
|
//Get the item pos
|
|
//if mouse pos is at left of an item middle, the pos of itself otherwise the pos of the next.
|
|
place_front = (x <= (item_rg.first + static_cast<int>(item_rg.second / 2)));
|
|
x = (place_front ? item_rg.first : essence_->header.range(essence_->header.next(i)).first);
|
|
|
|
if (npos != i)
|
|
{
|
|
if (place_front && (0 == essence_->header.cast(i, false)))
|
|
x -= static_cast<int>(essence_->header.margin());
|
|
essence_->graph->rectangle({ x - x_offset + rect.x, rect.y, 2, rect.height }, true, colors::red);
|
|
}
|
|
|
|
return i;
|
|
}
|
|
return npos;
|
|
}
|
|
|
|
void _m_draw_header_item(graph_reference graph, const column_rendering_parameter& crp, const rectangle& column_r, const ::nana::color& fgcolor, const es_header::column& column, item_state state)
|
|
{
|
|
::nana::color bgcolor;
|
|
|
|
switch(state)
|
|
{
|
|
case item_state::normal: bgcolor = essence_->scheme_ptr->header_bgcolor.get_color(); break;
|
|
case item_state::highlighted: bgcolor = essence_->scheme_ptr->header_bgcolor.get_color().blend(colors::white, 0.5); break;
|
|
case item_state::pressed:
|
|
case item_state::grabbed: bgcolor = essence_->scheme_ptr->header_grabbed.get_color(); break;
|
|
case item_state::floated: bgcolor = essence_->scheme_ptr->header_floated.get_color(); break;
|
|
}
|
|
|
|
if(API::dev::copy_transparent_background(essence_->listbox_ptr->handle(), column_r, graph, column_r.position()))
|
|
{
|
|
paint::graphics grad_graph{column_r.dimension()};
|
|
grad_graph.gradual_rectangle(rectangle{column_r.dimension()}, bgcolor.blend(colors::white, 0.1), bgcolor.blend(colors::black, 0.1), true);
|
|
|
|
graph.blend(column_r, grad_graph, {}, 0.8);
|
|
}
|
|
else
|
|
graph.gradual_rectangle(column_r, bgcolor.blend(colors::white, 0.1), bgcolor.blend(colors::black, 0.1), true);
|
|
|
|
paint::aligner text_aligner{ graph, column.alignment, column.alignment };
|
|
|
|
auto text_margin = essence_->scheme_ptr->text_margin;
|
|
|
|
if (text_margin < column_r.width)
|
|
{
|
|
graph.palette(true, fgcolor);
|
|
|
|
//Set column font
|
|
graph.typeface(column.typeface());
|
|
|
|
unsigned ascent, descent, ileading;
|
|
graph.text_metrics(ascent, descent, ileading);
|
|
|
|
point text_pos{
|
|
column_r.x + static_cast<int>(crp.margin),
|
|
column_r.bottom() - static_cast<int>(crp.height + ascent + descent) / 2
|
|
};
|
|
|
|
if (align::left == column.alignment)
|
|
text_pos.x += text_margin;
|
|
else if (align::center == column.alignment)
|
|
text_margin = 0;
|
|
|
|
text_aligner.draw(column.caption, text_pos, column_r.width - text_margin);
|
|
|
|
//Restores widget font
|
|
graph.typeface(crp.wdg_font);
|
|
}
|
|
|
|
auto & sort = essence_->lister.sort_attrs();
|
|
|
|
if (column.index == sort.column)
|
|
{
|
|
facade<element::arrow> arrow("hollow_triangle");
|
|
arrow.direction(sort.reverse ? ::nana::direction::south : ::nana::direction::north);
|
|
arrow.draw(graph, {}, colors::black, { column_r.x + (static_cast<int>(column_r.width) - 16) / 2, -4, 16, 16 }, element_state::normal); // geometric scheme?
|
|
}
|
|
}
|
|
|
|
void _m_make_float(const nana::rectangle& rect, const nana::point& pos)
|
|
{
|
|
const auto & col = essence_->header.at(essence_->pointer_where.second);
|
|
|
|
column_rendering_parameter crp;
|
|
crp.margin = 0;
|
|
crp.height = essence_->header_visible_px();
|
|
crp.max_font_px = essence_->header_font_px();
|
|
crp.wdg_font = essence_->listbox_ptr->typeface();
|
|
|
|
if (&essence_->header.at(0, true) == &col)
|
|
crp.margin = essence_->header.margin();
|
|
|
|
paint::graphics fl_graph({ col.width_px + crp.margin, crp.height });
|
|
|
|
fl_graph.typeface(essence_->graph->typeface());
|
|
|
|
_m_draw_header_item(fl_graph, crp, rectangle{ fl_graph.size()}, colors::white, col, item_state::floated);
|
|
|
|
auto xpos = essence_->header.range(col.index).first + pos.x - grabs_.start_pos;
|
|
essence_->graph->blend(rectangle{ point{ xpos - essence_->content_view->origin().x + rect.x, rect.y } , fl_graph.size() }, fl_graph, {}, 0.5);
|
|
}
|
|
|
|
private:
|
|
essence * const essence_;
|
|
|
|
struct grab_variables
|
|
{
|
|
int start_pos;
|
|
unsigned item_width;
|
|
|
|
size_type splitter{ npos };
|
|
}grabs_;
|
|
|
|
struct grab_terminal
|
|
{
|
|
size_type index;
|
|
bool place_front;
|
|
}grab_terminal_;
|
|
|
|
struct options
|
|
{
|
|
bool grab_column{ false };
|
|
point grab_column_position;
|
|
}options_;
|
|
};
|
|
|
|
class drawer_lister_impl
|
|
{
|
|
public:
|
|
using item_state = essence::item_state;
|
|
using parts = essence::parts;
|
|
using status_type = inline_notifier_interface::status_type;
|
|
|
|
drawer_lister_impl(essence * es) noexcept
|
|
:essence_(es)
|
|
{}
|
|
|
|
void draw(const nana::rectangle& visual_r)
|
|
{
|
|
internal_scope_guard lock;
|
|
|
|
//clear active panes
|
|
essence_->lister.append_active_panes(nullptr);
|
|
|
|
//The count of items to be drawn
|
|
auto item_count = essence_->count_of_exposed(true);
|
|
if (0 == item_count)
|
|
return;
|
|
|
|
auto const self = essence_->lister.wd_ptr();
|
|
auto const bgcolor = self->bgcolor();
|
|
auto const fgcolor = self->fgcolor();
|
|
|
|
essence_->graph->palette(false, bgcolor);
|
|
|
|
auto const header_w = essence_->header.width_px();
|
|
auto const item_height_px = essence_->item_height();
|
|
|
|
auto const origin = essence_->content_view->origin();
|
|
|
|
auto const header_margin = essence_->header.margin();
|
|
if (header_w + header_margin < origin.x + visual_r.width)
|
|
{
|
|
rectangle r{ point{ visual_r.x + static_cast<int>(header_w + header_margin) - origin.x, visual_r.y },
|
|
size{ visual_r.width + origin.x - header_w, visual_r.height } };
|
|
|
|
if (!API::dev::copy_transparent_background(essence_->listbox_ptr->handle(), r, *essence_->graph, r.position()))
|
|
essence_->graph->rectangle(r, true);
|
|
}
|
|
|
|
if (header_margin > 0)
|
|
{
|
|
rectangle r = visual_r;
|
|
r.width = header_margin;
|
|
|
|
if (!API::dev::copy_transparent_background(essence_->listbox_ptr->handle(), r, *essence_->graph, r.position()))
|
|
essence_->graph->rectangle(r, true);
|
|
}
|
|
|
|
es_lister & lister = essence_->lister;
|
|
|
|
auto & ptr_where = essence_->pointer_where;
|
|
|
|
auto first_disp = essence_->first_display();
|
|
|
|
point item_coord{
|
|
essence_->item_xpos(visual_r),
|
|
visual_r.y - static_cast<int>(origin.y % item_height_px)
|
|
};
|
|
|
|
essence_->inline_buffered_table.swap(essence_->inline_table);
|
|
|
|
// The first display is empty when the listbox is empty.
|
|
if (!first_disp.empty())
|
|
{
|
|
index_pair hoverred_pos(npos, npos); //the hoverred item.
|
|
|
|
//if where == lister || where == checker, 'second' indicates the offset to the relative display-order pos of the scroll offset_y which stands for the first item to be displayed in lister.
|
|
if ((ptr_where.first == parts::list || ptr_where.first == parts::checker) && ptr_where.second != npos)
|
|
{
|
|
hoverred_pos = lister.advance(first_disp, static_cast<int>(ptr_where.second));
|
|
}
|
|
|
|
auto const columns = essence_->ordered_columns(visual_r.width);
|
|
|
|
if (columns.empty())
|
|
return;
|
|
|
|
auto const txtoff = static_cast<int>(essence_->scheme_ptr->item_height_ex) / 2;
|
|
|
|
for (auto & cat : lister.cat_container())
|
|
for (auto & ind : cat.indicators)
|
|
{
|
|
if (ind)
|
|
ind->detach();
|
|
}
|
|
|
|
auto idx = first_disp;
|
|
for (auto i_categ = lister.get(first_disp.cat); i_categ != lister.cat_container().end(); ++i_categ)
|
|
{
|
|
if (item_coord.y > visual_r.bottom())
|
|
break;
|
|
|
|
if (idx.cat > 0 && idx.is_category())
|
|
{
|
|
_m_draw_categ(*i_categ, visual_r.x - origin.x, item_coord.y, txtoff, header_w, bgcolor,
|
|
(hoverred_pos.is_category() && (idx.cat == hoverred_pos.cat) ? item_state::highlighted : item_state::normal)
|
|
);
|
|
item_coord.y += static_cast<int>(item_height_px);
|
|
idx.item = 0;
|
|
}
|
|
|
|
if (i_categ->expand)
|
|
{
|
|
auto size = i_categ->items.size();
|
|
for (; idx.item < size; ++idx.item)
|
|
{
|
|
if (item_coord.y > visual_r.bottom())
|
|
break;
|
|
|
|
auto item_pos = lister.index_cast(index_pair{ idx.cat, idx.item }, true); //convert display position to absolute position
|
|
|
|
_m_draw_item(*i_categ, item_pos, item_coord, txtoff, header_w, visual_r, columns, bgcolor, fgcolor,
|
|
(idx == hoverred_pos ? item_state::highlighted : item_state::normal)
|
|
);
|
|
|
|
item_coord.y += static_cast<int>(item_height_px);
|
|
}
|
|
}
|
|
|
|
++idx.cat;
|
|
idx.item = nana::npos;
|
|
}
|
|
}
|
|
|
|
essence_->inline_buffered_table.clear();
|
|
|
|
if (item_coord.y < visual_r.bottom())
|
|
{
|
|
rectangle bground_r{ visual_r.x, item_coord.y, visual_r.width, static_cast<unsigned>(visual_r.bottom() - item_coord.y) };
|
|
if (!API::dev::copy_transparent_background(essence_->listbox_ptr->handle(), bground_r, *essence_->graph, bground_r.position()))
|
|
essence_->graph->rectangle(bground_r, true, bgcolor);
|
|
}
|
|
|
|
//Draw mouse selection
|
|
//Check if the mouse selection box presents.
|
|
if (essence_->mouse_selection.begin_position != essence_->mouse_selection.end_position)
|
|
{
|
|
point box_position{
|
|
std::min(essence_->mouse_selection.begin_position.x, essence_->mouse_selection.end_position.x),
|
|
std::min(essence_->mouse_selection.begin_position.y, essence_->mouse_selection.end_position.y)
|
|
};
|
|
size box_size{
|
|
static_cast<size::value_type>(std::abs(essence_->mouse_selection.begin_position.x - essence_->mouse_selection.end_position.x)),
|
|
static_cast<size::value_type>(std::abs(essence_->mouse_selection.begin_position.y - essence_->mouse_selection.end_position.y))
|
|
};
|
|
|
|
paint::graphics box_graph{ box_size };
|
|
box_graph.rectangle(true, essence_->scheme_ptr->selection_box.get_color().blend(colors::white, 0.6));
|
|
box_graph.rectangle(false, essence_->scheme_ptr->selection_box.get_color());
|
|
|
|
essence_->graph->blend(rectangle{ essence_->coordinate_cast(box_position, false), box_size }, box_graph, {}, 0.5);
|
|
}
|
|
}
|
|
private:
|
|
void _m_draw_categ(const category_t& categ, int x, int y, int txtoff, unsigned width, nana::color bgcolor, item_state state)
|
|
{
|
|
const auto item_height = essence_->item_height();
|
|
|
|
rectangle bground_r{ x + static_cast<int>(essence_->header.margin()), y, width, item_height };
|
|
auto graph = essence_->graph;
|
|
|
|
item_data item;
|
|
item.flags.selected = categ.selected();
|
|
|
|
this->_m_draw_item_bground(bground_r, bgcolor, {}, state, item);
|
|
|
|
color txt_color{ static_cast<color_rgb>(0x3399) };
|
|
|
|
//Area of category icon
|
|
rectangle rt_ctg_icon{ x + 5, y + static_cast<int>(item_height - 16) / 2, 16, 16 };
|
|
|
|
if (essence_->ctg_icon_renderer)
|
|
{
|
|
essence_->ctg_icon_renderer(*graph, rt_ctg_icon, categ.expand);
|
|
}
|
|
else
|
|
{
|
|
facade<element::arrow> arrow("double");
|
|
arrow.direction(categ.expand ? ::nana::direction::south : ::nana::direction::east);
|
|
arrow.draw(*graph, {}, txt_color, rt_ctg_icon, element_state::normal);
|
|
}
|
|
|
|
graph->string({ x + 20, y + txtoff }, categ.text, txt_color);
|
|
|
|
auto text_px = graph->text_extent_size(categ.text).width;
|
|
if (categ.display_number)
|
|
{
|
|
//Display the number of items in the category
|
|
native_string_type str = to_nstring('(' + std::to_string(categ.items.size()) + ')');
|
|
graph->string({ x + 25 + static_cast<int>(text_px), y + txtoff }, str);
|
|
text_px += graph->text_extent_size(str).width;
|
|
}
|
|
|
|
if (35 + text_px < width)
|
|
{
|
|
::nana::point pos{ x + 30 + static_cast<int>(text_px), y + static_cast<int>(item_height) / 2 };
|
|
|
|
graph->line(pos, { x + static_cast<int>(width) - 5, pos.y }, txt_color);
|
|
}
|
|
|
|
//Draw selecting inner rectangle
|
|
if (item.flags.selected && (categ.expand == false))
|
|
_m_draw_item_border(y);
|
|
}
|
|
|
|
color _m_draw_item_bground(const rectangle& bground_r, color bgcolor, color cell_color, item_state state, const item_data& item)
|
|
{
|
|
auto graph = essence_->graph;
|
|
|
|
auto const is_transparent = API::dev::copy_transparent_background(essence_->listbox_ptr->handle(), bground_r, *graph, bground_r.position());
|
|
|
|
if (is_transparent)
|
|
bgcolor = color{};
|
|
|
|
if (item.flags.selected)
|
|
{
|
|
bgcolor = essence_->scheme_ptr->item_selected;
|
|
|
|
if (!cell_color.invisible())
|
|
bgcolor = bgcolor.blend(cell_color, 0.5);
|
|
}
|
|
else if (!cell_color.invisible())
|
|
bgcolor = cell_color;
|
|
else if (!item.bgcolor.invisible())
|
|
bgcolor = item.bgcolor;
|
|
|
|
if (item_state::highlighted == state)
|
|
{
|
|
if (item.flags.selected)
|
|
bgcolor = bgcolor.blend(essence_->scheme_ptr->item_highlighted, 0.5);
|
|
else
|
|
bgcolor = bgcolor.blend(essence_->scheme_ptr->item_highlighted, 0.7);
|
|
}
|
|
|
|
if (is_transparent)
|
|
{
|
|
if(!bgcolor.invisible())
|
|
graph->blend(bground_r, bgcolor, 0.2);
|
|
}
|
|
else
|
|
{
|
|
graph->rectangle(bground_r, true, bgcolor);
|
|
|
|
}
|
|
|
|
return bgcolor;
|
|
}
|
|
|
|
/// Draws an item
|
|
void _m_draw_item(const category_t& cat,
|
|
const index_pair& item_pos,
|
|
const point& coord,
|
|
const int txtoff, ///< below y to print the text
|
|
unsigned header_width, ///< width of all visible columns, in pixel
|
|
const nana::rectangle& content_r, ///< the rectangle where the full list content have to be drawn
|
|
const std::vector<size_type>& seqs, ///< columns to print
|
|
nana::color bgcolor,
|
|
nana::color fgcolor,
|
|
item_state state
|
|
)
|
|
{
|
|
auto & item = cat.items[item_pos.item];
|
|
|
|
std::vector<cell> model_cells;
|
|
if (cat.model_ptr)
|
|
{
|
|
model_cells = cat.model_ptr->container()->to_cells(item_pos.item);
|
|
}
|
|
|
|
auto & cells = (cat.model_ptr ? model_cells : *item.cells);
|
|
|
|
if(!item.fgcolor.invisible())
|
|
fgcolor = item.fgcolor;
|
|
|
|
const unsigned columns_shown_width = (std::min)(content_r.width, header_width - essence_->content_view->origin().x);
|
|
|
|
auto graph = essence_->graph;
|
|
|
|
//draw the background for the whole item
|
|
rectangle bground_r{
|
|
content_r.x + static_cast<int>(essence_->header.margin()) - essence_->content_view->origin().x,
|
|
coord.y,
|
|
columns_shown_width + essence_->content_view->origin().x,
|
|
essence_->item_height() };
|
|
auto const state_bgcolor = this->_m_draw_item_bground(bground_r, bgcolor, {}, state, item);
|
|
|
|
//The position of column in x-axis.
|
|
int column_x = coord.x;
|
|
|
|
for (size_type display_order{ 0 }; display_order < seqs.size(); ++display_order) // get the cell (column) index in the order headers are displayed
|
|
{
|
|
const auto column_pos = seqs[display_order];
|
|
const auto & col = essence_->header.at(column_pos); // deduce the corresponding header which is in a kind of dislay order
|
|
|
|
if (col.width_px > essence_->scheme_ptr->text_margin)
|
|
{
|
|
//The column text position, it is a offset to column_x.
|
|
int content_pos = 0;
|
|
|
|
element_state estate = element_state::normal;
|
|
nana::rectangle img_r;
|
|
|
|
//Draw the image in the 1st column in display order
|
|
if (0 == display_order)
|
|
{
|
|
if (essence_->checkable)
|
|
{
|
|
content_pos += 18; // checker width, geom scheme?
|
|
|
|
if (essence_->pointer_where.first == parts::checker)
|
|
{
|
|
switch (state)
|
|
{
|
|
case item_state::highlighted:
|
|
estate = element_state::hovered; break;
|
|
case item_state::grabbed:
|
|
estate = element_state::pressed; break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
using state = facade<element::crook>::state;
|
|
crook_renderer_.check(item.flags.checked ? state::checked : state::unchecked);
|
|
}
|
|
|
|
if (essence_->if_image)
|
|
{
|
|
//Draw the image in the 1st column in display order
|
|
if (item.img)
|
|
{
|
|
nana::rectangle imgt(item.img_show_size);
|
|
img_r = imgt;
|
|
img_r.x = content_pos + coord.x + (16 - static_cast<int>(item.img_show_size.width)) / 2; // center in 16 - geom scheme?
|
|
img_r.y = coord.y + (static_cast<int>(essence_->item_height()) - static_cast<int>(item.img_show_size.height)) / 2; // center
|
|
}
|
|
content_pos += 18; // image width, geom scheme?
|
|
}
|
|
}
|
|
|
|
bool draw_column = true;
|
|
|
|
if ( content_pos + essence_->scheme_ptr->text_margin < col.width_px) // we have room
|
|
{
|
|
auto inline_wdg = _m_get_inline_pane(cat, column_pos);
|
|
if (inline_wdg)
|
|
{
|
|
//Make sure the user-define inline widgets is in the right visible rectangle.
|
|
rectangle pane_r;
|
|
|
|
const auto wdg_x = column_x + content_pos;
|
|
const auto wdg_w = col.width_px - static_cast<unsigned>(content_pos);
|
|
|
|
bool visible_state = true;
|
|
if (::nana::overlap(content_r, { wdg_x, coord.y, wdg_w, essence_->item_height() }, pane_r))
|
|
{
|
|
::nana::point pane_pos;
|
|
if (wdg_x < content_r.x)
|
|
pane_pos.x = wdg_x - content_r.x;
|
|
|
|
if (coord.y < content_r.y)
|
|
pane_pos.y = coord.y - content_r.y;
|
|
|
|
inline_wdg->pane_widget.move(pane_pos);
|
|
inline_wdg->pane_bottom.move(pane_r);
|
|
}
|
|
else
|
|
visible_state = false;
|
|
|
|
draw_column = inline_wdg->inline_ptr->whether_to_draw();
|
|
|
|
inline_wdg->item_pos = item_pos;
|
|
inline_wdg->column_pos = column_pos;
|
|
inline_wdg->inline_ptr->activate(*inline_wdg->indicator, item_pos);
|
|
|
|
::nana::size sz{ wdg_w, essence_->item_height() };
|
|
inline_wdg->pane_widget.size(sz);
|
|
inline_wdg->inline_ptr->resize(sz);
|
|
|
|
inline_wdg->inline_ptr->notify_status(status_type::selected, item.flags.selected);
|
|
inline_wdg->inline_ptr->notify_status(status_type::checked, item.flags.checked);
|
|
|
|
inline_wdg->indicator->attach(item_pos, inline_wdg);
|
|
|
|
//To reduce the memory usage, the cells may not be allocated
|
|
if (cells.size() > column_pos)
|
|
inline_wdg->inline_ptr->set(cells[column_pos].text);
|
|
else
|
|
inline_wdg->inline_ptr->set({});
|
|
|
|
API::show_window(inline_wdg->pane_bottom, visible_state);
|
|
|
|
essence_->lister.append_active_panes(inline_wdg);
|
|
}
|
|
}
|
|
|
|
auto col_bgcolor = bgcolor;
|
|
auto col_fgcolor = fgcolor;
|
|
|
|
if (cells.size() > column_pos) // process only if the cell is visible
|
|
{
|
|
auto & m_cell = cells[column_pos];
|
|
review_utf8(m_cell.text);
|
|
|
|
if (m_cell.custom_format) // adapt to costum format if need
|
|
{
|
|
col_fgcolor = m_cell.custom_format->fgcolor;
|
|
|
|
bground_r = rectangle{ column_x, coord.y, col.width_px, essence_->item_height() };
|
|
col_bgcolor = this->_m_draw_item_bground(bground_r, bgcolor, m_cell.custom_format->bgcolor, state, item);
|
|
}
|
|
else
|
|
col_bgcolor = state_bgcolor;
|
|
|
|
if (draw_column)
|
|
{
|
|
paint::aligner text_aligner{*graph, col.alignment};
|
|
|
|
unsigned text_margin_right = 0;
|
|
if (align::left == col.alignment)
|
|
content_pos += essence_->scheme_ptr->text_margin;
|
|
else if (align::right == col.alignment)
|
|
text_margin_right = essence_->scheme_ptr->text_margin;
|
|
|
|
graph->palette(true, col_fgcolor);
|
|
text_aligner.draw(m_cell.text, { column_x + content_pos, coord.y + txtoff }, col.width_px - content_pos - text_margin_right);
|
|
}
|
|
}
|
|
|
|
if (0 == display_order)
|
|
{
|
|
if (essence_->checkable)
|
|
crook_renderer_.draw(*graph, col_bgcolor, col_fgcolor, essence_->checkarea(column_x, coord.y), estate);
|
|
if (item.img)
|
|
item.img.stretch(rectangle{ item.img.size() }, *graph, img_r);
|
|
}
|
|
|
|
if (display_order > 0)
|
|
graph->line({ column_x - 1, coord.y }, { column_x - 1, coord.y + static_cast<int>(essence_->item_height()) - 1 }, static_cast<color_rgb>(0xEBF4F9));
|
|
}
|
|
|
|
column_x += col.width_px;
|
|
}
|
|
|
|
//Draw selecting inner rectangle
|
|
if (item.flags.selected)
|
|
_m_draw_item_border(coord.y);
|
|
}
|
|
|
|
inline_pane * _m_get_inline_pane(const category_t& cat, std::size_t column_pos) const
|
|
{
|
|
if (column_pos < cat.factories.size())
|
|
{
|
|
auto & factory = cat.factories[column_pos];
|
|
if (factory)
|
|
{
|
|
return essence_->open_inline(factory.get(), cat.indicators[column_pos].get());
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
inline_pane* _m_find_inline_pane(const index_pair& pos, std::size_t column_pos) const
|
|
{
|
|
auto & cat = *essence_->lister.get(pos.cat);
|
|
|
|
if (column_pos >= cat.factories.size())
|
|
return nullptr;
|
|
|
|
auto& factory = cat.factories[column_pos];
|
|
if (!factory)
|
|
return nullptr;
|
|
|
|
auto i = essence_->inline_table.find(factory.get());
|
|
if (i == essence_->inline_table.end())
|
|
return nullptr;
|
|
|
|
for (auto & inl_widget : i->second)
|
|
{
|
|
if (inl_widget->item_pos == pos && inl_widget->column_pos == column_pos)
|
|
return inl_widget.get();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void _m_draw_item_border(int item_top) const
|
|
{
|
|
//Draw selecting inner rectangle
|
|
rectangle r{
|
|
essence_->content_area().x - essence_->content_view->origin().x + static_cast<int>(essence_->header.margin()),
|
|
item_top,
|
|
essence_->header.width_px(),
|
|
essence_->item_height()
|
|
};
|
|
|
|
essence_->graph->rectangle(r, false, static_cast<color_rgb>(0x99defd));
|
|
|
|
essence_->graph->palette(false, colors::white);
|
|
paint::draw(*essence_->graph).corner(r, 1);
|
|
|
|
essence_->graph->rectangle(r.pare_off(1), false);
|
|
}
|
|
private:
|
|
essence * const essence_;
|
|
mutable facade<element::crook> crook_renderer_;
|
|
};
|
|
|
|
//class trigger: public drawer_trigger
|
|
trigger::trigger()
|
|
: essence_(new essence),
|
|
drawer_header_(new drawer_header_impl(essence_)),
|
|
drawer_lister_(new drawer_lister_impl(essence_))
|
|
{}
|
|
|
|
trigger::~trigger()
|
|
{
|
|
delete drawer_lister_;
|
|
delete drawer_header_;
|
|
delete essence_;
|
|
}
|
|
|
|
essence& trigger::ess() const noexcept
|
|
{
|
|
return *essence_;
|
|
}
|
|
|
|
void trigger::attached(widget_reference widget, graph_reference graph)
|
|
{
|
|
essence_->listbox_ptr = static_cast<nana::listbox*>(&widget);
|
|
essence_->scheme_ptr = static_cast<::nana::listbox::scheme_type*>(API::dev::get_scheme(widget));
|
|
essence_->graph = &graph;
|
|
|
|
essence_->lister.bind(essence_, widget);
|
|
widget.bgcolor(colors::white);
|
|
|
|
essence_->content_view.reset(new widgets::skeletons::content_view{ widget.handle() });
|
|
essence_->resize_disp_area();
|
|
|
|
//Set the content_view wheel speed with the listbox scheme.
|
|
essence_->content_view->set_wheel_speed([this] {
|
|
return essence_->scheme_ptr->mouse_wheel.lines;
|
|
});
|
|
|
|
essence_->content_view->events().hover_outside = [this](const point& cur_pos) {
|
|
essence_->update_mouse_selection(cur_pos);
|
|
};
|
|
}
|
|
|
|
void trigger::detached()
|
|
{
|
|
essence_->graph = nullptr;
|
|
essence_->listbox_ptr = nullptr;
|
|
}
|
|
|
|
void trigger::typeface_changed(graph_reference graph)
|
|
{
|
|
essence_->text_height = 0;
|
|
unsigned as, ds, il;
|
|
if (graph.text_metrics(as, ds, il))
|
|
essence_->text_height = as + ds;
|
|
|
|
essence_->scheme_ptr->suspension_width = graph.text_extent_size("...").width;
|
|
essence_->calc_content_size(true);
|
|
}
|
|
|
|
void trigger::refresh(graph_reference graph)
|
|
{
|
|
if (API::is_destroying(essence_->lister.wd_ptr()->handle()))
|
|
return;
|
|
|
|
nana::rectangle r;
|
|
|
|
if (essence_->rect_lister(r))
|
|
drawer_lister_->draw(r);
|
|
|
|
if (essence_->header.attrib().visible && essence_->rect_header(r))
|
|
drawer_header_->draw(graph, r);
|
|
|
|
essence_->draw_peripheral();
|
|
}
|
|
|
|
void trigger::mouse_move(graph_reference graph, const arg_mouse& arg)
|
|
{
|
|
using item_state = essence::item_state;
|
|
using parts = essence::parts;
|
|
|
|
if ((operation_states::msup_deselect == essence_->operation.state) && API::dev::window_draggable(arg.window_handle))
|
|
essence_->operation.state = operation_states::none;
|
|
|
|
bool need_refresh = false;
|
|
|
|
point pos_in_header = arg.pos;
|
|
essence_->widget_to_header(pos_in_header);
|
|
|
|
if(essence_->ptr_state == item_state::pressed)
|
|
{
|
|
if((essence_->pointer_where.first == parts::header) && essence_->header.attrib().movable)
|
|
{ // moving a pressed header : grab it
|
|
essence_->ptr_state = item_state::grabbed;
|
|
|
|
//Start to move a header column or resize a header column(depends on item_spliter_)
|
|
drawer_header_->grab(pos_in_header, true);
|
|
|
|
essence_->lister.wd_ptr()->set_capture(true);
|
|
need_refresh = true;
|
|
}
|
|
}
|
|
|
|
if(essence_->ptr_state == item_state::grabbed)
|
|
{
|
|
// moving a grabbed header
|
|
need_refresh = drawer_header_->grab_move(pos_in_header);
|
|
}
|
|
else if(essence_->calc_where(arg.pos))
|
|
{
|
|
essence_->ptr_state = item_state::highlighted;
|
|
need_refresh = true;
|
|
}
|
|
|
|
//Detects column splitter
|
|
if(essence_->header.attrib().resizable &&
|
|
(essence_->pointer_where.first == parts::header) &&
|
|
drawer_header_->detect_splitter(arg.pos.x))
|
|
{
|
|
essence_->lister.wd_ptr()->cursor(cursor::size_we);
|
|
}
|
|
else if(essence_->ptr_state != item_state::grabbed)
|
|
{
|
|
if((drawer_header_->splitter() != npos) || (essence_->lister.wd_ptr()->cursor() == cursor::size_we))
|
|
{
|
|
essence_->lister.wd_ptr()->cursor(cursor::arrow);
|
|
drawer_header_->cancel_splitter();
|
|
need_refresh = true;
|
|
}
|
|
}
|
|
|
|
if (essence_->mouse_selection.started)
|
|
{
|
|
essence_->update_mouse_selection(arg.pos);
|
|
need_refresh = true;
|
|
}
|
|
|
|
if (need_refresh)
|
|
{
|
|
refresh(graph);
|
|
API::dev::lazy_refresh();
|
|
}
|
|
}
|
|
|
|
void trigger::mouse_leave(graph_reference graph, const arg_mouse&)
|
|
{
|
|
using item_state = essence::item_state;
|
|
using parts = essence::parts;
|
|
|
|
if((essence_->pointer_where.first != parts::unknown) || (essence_->ptr_state != item_state::normal))
|
|
{
|
|
if (essence_->ptr_state != item_state::grabbed)
|
|
{
|
|
essence_->pointer_where.first = parts::unknown;
|
|
essence_->ptr_state = item_state::normal;
|
|
}
|
|
|
|
refresh(graph);
|
|
API::dev::lazy_refresh();
|
|
}
|
|
}
|
|
|
|
void trigger::mouse_down(graph_reference graph, const arg_mouse& arg)
|
|
{
|
|
using item_state = essence::item_state;
|
|
using parts = essence::parts;
|
|
bool update = false;
|
|
|
|
essence_->mouse_selection.reverse_selection = false;
|
|
|
|
auto & lister = essence_->lister;
|
|
|
|
rectangle head_r, list_r;
|
|
auto const good_head_r = essence_->rect_header(head_r);
|
|
auto const good_list_r = essence_->rect_lister(list_r);
|
|
|
|
auto & ptr_where = essence_->pointer_where;
|
|
if((ptr_where.first == parts::header) && (ptr_where.second != npos || (drawer_header_->splitter() != npos)))
|
|
{
|
|
essence_->ptr_state = item_state::pressed;
|
|
if(good_head_r)
|
|
{
|
|
drawer_header_->draw(graph, head_r);
|
|
update = true;
|
|
}
|
|
}
|
|
else if(ptr_where.first == parts::list || ptr_where.first == parts::checker)
|
|
{
|
|
index_pair item_pos = lister.advance(essence_->first_display(), static_cast<int>(ptr_where.second));
|
|
|
|
if ((essence_->column_from_pos(arg.pos.x) != npos) && !item_pos.empty())
|
|
{
|
|
auto * item_ptr = (item_pos.is_category() ? nullptr : &lister.at(item_pos));
|
|
|
|
const auto abs_item_pos = lister.index_cast_noexcept(item_pos, true, item_pos); //convert display position to absolute position
|
|
|
|
if(ptr_where.first == parts::list)
|
|
{
|
|
//adjust the display of selected into the list rectangle if the part of the item is beyond the top/bottom edge
|
|
if (good_list_r)
|
|
{
|
|
auto const item_top = this->essence_->item_window_top(abs_item_pos);
|
|
auto const item_bottom = item_top + static_cast<int>(essence_->item_height());
|
|
|
|
int move_top = 0;
|
|
|
|
if (item_top < list_r.y && list_r.y < item_bottom)
|
|
move_top = item_top - list_r.y;
|
|
else if (item_top < list_r.bottom() && list_r.bottom() < item_bottom)
|
|
move_top = item_bottom - list_r.bottom();
|
|
|
|
if (0 != move_top)
|
|
{
|
|
essence_->content_view->move_origin({ 0, move_top });
|
|
essence_->content_view->sync(false);
|
|
}
|
|
}
|
|
|
|
bool new_selected_status = true;
|
|
|
|
if (!lister.single_status(true)) //multiply selection enabled
|
|
{
|
|
if (arg.shift)
|
|
{
|
|
//Set the first item as the begin of selected item if there
|
|
//is not a latest selected item.(#154 reported by RenaudAlpes)
|
|
if (lister.latest_selected_abs.empty())
|
|
lister.latest_selected_abs = lister.first();
|
|
|
|
auto before = lister.latest_selected_abs;
|
|
|
|
lister.select_display_range_if(lister.latest_selected_abs, item_pos, true, [](const index_pair&)
|
|
{
|
|
return true;
|
|
});
|
|
|
|
lister.latest_selected_abs = before;
|
|
}
|
|
else if (arg.ctrl)
|
|
{
|
|
essence_->mouse_selection.reverse_selection = true;
|
|
new_selected_status = !essence_->cs_status(abs_item_pos, true);
|
|
}
|
|
else
|
|
{
|
|
auto selected = lister.pick_items(true);
|
|
if (selected.cend() != std::find(selected.cbegin(), selected.cend(), item_pos))
|
|
{
|
|
//If the current selected one has been selected before selecting, remains the selection states for all
|
|
//selected items. But these items will be unselected when the mouse is released.
|
|
|
|
//Other items will be unselected if multiple items are selected.
|
|
if (selected.size() > 1)
|
|
{
|
|
essence_->operation.item = abs_item_pos;
|
|
|
|
//Don't deselect the selections, let it determine in mouse_move event depending on whether dnd is enabled.
|
|
essence_->operation.state = operation_states::msup_deselect;
|
|
}
|
|
}
|
|
else
|
|
lister.select_for_all(false, abs_item_pos);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//Clicking on a category is ignored when single selection is enabled.
|
|
//Fixed by Greentwip(issue #121)
|
|
if (item_ptr)
|
|
new_selected_status = !item_proxy(essence_, abs_item_pos).selected();
|
|
}
|
|
|
|
if(item_ptr)
|
|
{
|
|
if (item_ptr->flags.selected != new_selected_status)
|
|
{
|
|
if (new_selected_status)
|
|
{
|
|
//Deselects the previously selected item.
|
|
lister.cancel_others_if_single_enabled(true, abs_item_pos);
|
|
essence_->lister.latest_selected_abs = abs_item_pos;
|
|
}
|
|
else if (essence_->lister.latest_selected_abs == abs_item_pos)
|
|
essence_->lister.latest_selected_abs.set_both(npos);
|
|
|
|
item_ptr->flags.selected = new_selected_status;
|
|
lister.emit_cs(abs_item_pos, true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//A category was clicked. Sets all child items to be selected only if multiply selection is enabled.
|
|
if(!lister.single_status(true))
|
|
lister.cat_status(item_pos.cat, true, true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (item_ptr)
|
|
{
|
|
item_ptr->flags.checked = !item_ptr->flags.checked;
|
|
lister.emit_cs(abs_item_pos, false);
|
|
|
|
if (item_ptr->flags.checked)
|
|
lister.cancel_others_if_single_enabled(false, abs_item_pos);
|
|
}
|
|
else if (!lister.single_status(false)) //not single checked
|
|
lister.cat_status_reverse(item_pos.cat, false);
|
|
}
|
|
update = true;
|
|
}
|
|
|
|
if(update)
|
|
{
|
|
if (good_list_r)
|
|
{
|
|
drawer_lister_->draw(list_r);
|
|
if (good_head_r)
|
|
drawer_header_->draw(graph, head_r);
|
|
}
|
|
else
|
|
update = false;
|
|
}
|
|
}
|
|
else if (ptr_where.first == parts::list_blank) //not selected
|
|
{
|
|
//Start box selection if mulit-selection is enabled
|
|
if (arg.is_left_button() && (!lister.single_status(true)))
|
|
essence_->start_mouse_selection(arg);
|
|
|
|
//Deselection of all items is deferred to the mouse up event when ctrl or shift is not pressed
|
|
//Pressing ctrl or shift is to selects other items without deselecting current selections.
|
|
if (!(arg.ctrl || arg.shift))
|
|
{
|
|
essence_->operation.state = operation_states::msup_deselect;
|
|
essence_->operation.item = index_pair{nana::npos, nana::npos};
|
|
}
|
|
}
|
|
|
|
if(update)
|
|
{
|
|
essence_->draw_peripheral();
|
|
API::dev::lazy_refresh();
|
|
}
|
|
}
|
|
|
|
void trigger::mouse_up(graph_reference graph, const arg_mouse& arg)
|
|
{
|
|
using item_state = essence::item_state;
|
|
using parts = essence::parts;
|
|
|
|
auto prev_state = essence_->ptr_state;
|
|
essence_->ptr_state = item_state::highlighted;
|
|
|
|
bool need_refresh = false;
|
|
|
|
//Don't sort the column when the mouse is due to released for stopping resizing column.
|
|
if ((drawer_header_->splitter() == npos) && essence_->header.attrib().sortable && essence_->pointer_where.first == parts::header && prev_state == item_state::pressed)
|
|
{
|
|
//Try to sort the column
|
|
if(essence_->pointer_where.second < essence_->header.cont().size())
|
|
need_refresh = essence_->lister.sort_column(essence_->pointer_where.second, nullptr);
|
|
}
|
|
else if (item_state::grabbed == prev_state)
|
|
{
|
|
nana::point pos = arg.pos;
|
|
essence_->widget_to_header(pos);
|
|
drawer_header_->grab(pos, false);
|
|
need_refresh = true;
|
|
essence_->lister.wd_ptr()->release_capture();
|
|
}
|
|
|
|
if (essence_->mouse_selection.started)
|
|
{
|
|
essence_->stop_mouse_selection();
|
|
need_refresh = true;
|
|
}
|
|
|
|
if (operation_states::msup_deselect == essence_->operation.state)
|
|
{
|
|
essence_->operation.state = operation_states::none;
|
|
need_refresh |= essence_->lister.select_for_all(false, essence_->operation.item);
|
|
}
|
|
|
|
if (need_refresh)
|
|
{
|
|
refresh(graph);
|
|
API::dev::lazy_refresh();
|
|
}
|
|
}
|
|
|
|
void trigger::dbl_click(graph_reference graph, const arg_mouse&)
|
|
{
|
|
using parts = essence::parts;
|
|
|
|
if (parts::header == essence_->pointer_where.first)
|
|
{
|
|
if (cursor::size_we == essence_->lister.wd_ptr()->cursor())
|
|
{
|
|
//adjust the width of column to fit its content.
|
|
auto split_pos = drawer_header_->splitter();
|
|
if (split_pos != npos)
|
|
{
|
|
essence_->header.at(split_pos).fit_content();
|
|
refresh(graph);
|
|
API::dev::lazy_refresh();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (parts::list != essence_->pointer_where.first)
|
|
return;
|
|
|
|
auto & lister = essence_->lister;
|
|
//Get the item which the mouse is placed.
|
|
|
|
auto item_pos = lister.advance(essence_->first_display(), static_cast<int>(essence_->pointer_where.second));
|
|
if (!item_pos.empty())
|
|
{
|
|
if (!item_pos.is_category()) //being the npos of item.second is a category
|
|
return;
|
|
|
|
arg_listbox_category arg_cat(cat_proxy(essence_, item_pos.cat));
|
|
lister.wd_ptr()->events().category_dbl_click.emit(arg_cat, lister.wd_ptr()->handle());
|
|
|
|
if (!arg_cat.block_operation)
|
|
{
|
|
bool do_expand = (lister.expand(item_pos.cat) == false);
|
|
lister.expand(item_pos.cat, do_expand);
|
|
|
|
essence_->calc_content_size(false);
|
|
essence_->content_view->sync(false);
|
|
refresh(graph);
|
|
API::dev::lazy_refresh();
|
|
}
|
|
}
|
|
}
|
|
|
|
void trigger::resized(graph_reference graph, const arg_resized&)
|
|
{
|
|
essence_->resize_disp_area();
|
|
|
|
refresh(graph);
|
|
API::dev::lazy_refresh();
|
|
}
|
|
|
|
void trigger::key_press(graph_reference graph, const arg_keyboard& arg)
|
|
{
|
|
auto & list = essence_->lister;
|
|
// Exit if list is empty
|
|
if (list.first().empty())
|
|
return;
|
|
|
|
switch(arg.key)
|
|
{
|
|
case keyboard::os_arrow_up:
|
|
case keyboard::os_arrow_down:
|
|
list.move_select((keyboard::os_arrow_up == arg.key), !arg.shift, true);
|
|
break;
|
|
case L' ':
|
|
{
|
|
bool items_checked{ false };
|
|
auto items = list.pick_items(true, false, &items_checked);
|
|
items_checked = !items_checked;
|
|
for (auto i : items)
|
|
item_proxy(essence_, i).check(items_checked);
|
|
}
|
|
break;
|
|
case keyboard::os_pageup :
|
|
case keyboard::os_pagedown:
|
|
{
|
|
auto const upward = (keyboard::os_pageup == arg.key);
|
|
auto const item_px = essence_->item_height();
|
|
auto picked_items = list.pick_items(true, true);
|
|
index_pair init_idx = (picked_items.empty() ? list.first() : picked_items[0]);
|
|
|
|
essence_->lister.select_for_all(false);
|
|
|
|
//Get the pixels between the init item and top edge or bottom edge
|
|
auto logic_top = static_cast<int>(list.distance(list.first(), init_idx) * item_px);
|
|
|
|
auto const screen_top = essence_->content_view->origin().y;
|
|
auto const screen_bottom = screen_top + essence_->content_view->view_area().height;
|
|
index_pair target_idx;
|
|
|
|
//Check if it scrolls in current screen window
|
|
//condition: top of target item is not less than top edge of content view and
|
|
//the bottom of target item is not greater than bottom edge of content view.
|
|
if ((screen_top + static_cast<int>(item_px) <= logic_top) && (logic_top + static_cast<int>(item_px) + static_cast<int>(item_px) <= static_cast<int>(screen_bottom)))
|
|
{
|
|
int offset = (static_cast<int>(upward ? screen_top : screen_bottom - item_px) - logic_top) / static_cast<int>(item_px);
|
|
target_idx = list.advance(init_idx, offset);
|
|
}
|
|
else
|
|
{
|
|
//turn page
|
|
auto page_item_count = (std::max)(1, static_cast<int>(essence_->count_of_exposed(false)));
|
|
|
|
auto origin = essence_->content_view->origin();
|
|
if (upward)
|
|
{
|
|
target_idx = list.advance(init_idx, -page_item_count);
|
|
if (target_idx.empty())
|
|
target_idx = list.first();
|
|
|
|
origin.y = static_cast<int>(list.distance(list.first(), target_idx) * item_px);
|
|
}
|
|
else
|
|
{
|
|
target_idx = list.advance(init_idx, page_item_count);
|
|
if (target_idx.empty())
|
|
target_idx = list.last();
|
|
|
|
origin.y = static_cast<int>((list.distance(list.first(), target_idx) + 1) * item_px);
|
|
origin.y = (std::max)(origin.y - static_cast<int>(screen_bottom - screen_top), 0);
|
|
}
|
|
|
|
essence_->content_view->move_origin(origin - essence_->content_view->origin());
|
|
}
|
|
|
|
if (!target_idx.is_category())
|
|
item_proxy::from_display(essence_, target_idx).select(true);
|
|
else if (!list.single_status(true)) //not selected
|
|
list.cat_status(target_idx.cat, true, true);
|
|
}
|
|
break;
|
|
case keyboard::os_home:
|
|
case keyboard::os_end:
|
|
{
|
|
list.select_for_all(false);
|
|
|
|
auto pos = (keyboard::os_home == arg.key ? list.first() : list.last());
|
|
if (!pos.empty())
|
|
{
|
|
//When the pos indicates an empty category, then search forwards/backwards(depending on arg.key whether it is Home or End) for a non empty category.
|
|
//When a non-empty category is found, assign the pos to the first/last item of the category if the category is expanded.
|
|
if (pos.is_category())
|
|
{
|
|
if (keyboard::os_home == arg.key)
|
|
{
|
|
while (0 == list.size_item(pos.cat))
|
|
{
|
|
if (++pos.cat >= list.cat_container().size())
|
|
{
|
|
pos = index_pair{ npos, npos };
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while (0 == list.size_item(pos.cat))
|
|
{
|
|
if (pos.cat-- == 0)
|
|
{
|
|
pos = index_pair{ npos, npos };
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!pos.empty())
|
|
{
|
|
if (list.expand(pos.cat))
|
|
pos.item = 0;
|
|
}
|
|
}
|
|
|
|
if (!pos.empty())
|
|
{
|
|
if (pos.is_category())
|
|
{
|
|
if (!list.single_status(true)) //multiple selection is not enabled
|
|
list.cat_status(pos.cat, true, true);
|
|
}
|
|
else
|
|
item_proxy::from_display(essence_, pos).select(true);
|
|
|
|
list.scroll_into_view(pos, view_action::auto_view);
|
|
}
|
|
}
|
|
|
|
}
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
refresh(graph);
|
|
API::dev::lazy_refresh();
|
|
}
|
|
|
|
void trigger::key_char(graph_reference graph, const arg_keyboard& arg)
|
|
{
|
|
switch(arg.key)
|
|
{
|
|
case keyboard::copy:
|
|
{
|
|
auto exp_opt = essence_->def_exp_options;
|
|
exp_opt.columns_order = essence_->header.get_headers(true);
|
|
exp_opt.only_selected_items = true;
|
|
::nana::system::dataexch().set(essence_->to_string(exp_opt), API::root(essence_->listbox_ptr->handle()));
|
|
return;
|
|
}
|
|
case keyboard::select_all :
|
|
if (!essence_->lister.single_status(true))
|
|
{
|
|
essence_->lister.select_for_all(true);
|
|
refresh(graph);
|
|
API::dev::lazy_refresh();
|
|
}
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
//end class trigger
|
|
|
|
//class item_proxy
|
|
item_proxy::item_proxy(essence * ess, const index_pair& pos)
|
|
: ess_(ess),
|
|
pos_(pos)
|
|
{
|
|
//get the cat of the item specified by pos
|
|
if (ess && !pos.empty())
|
|
cat_ = &(*ess->lister.get(pos.cat));
|
|
}
|
|
|
|
/// the main porpose of this it to make obvious that item_proxy operate with absolute positions, and dont get moved during sort()
|
|
item_proxy item_proxy::from_display(essence *ess, const index_pair &relative)
|
|
{
|
|
return item_proxy{ ess, ess->lister.index_cast(relative, true) };
|
|
}
|
|
|
|
item_proxy item_proxy::from_display(const index_pair &relative) const
|
|
{
|
|
return item_proxy{ess_, ess_->lister.index_cast(relative, true)};
|
|
}
|
|
|
|
/// posible use: last_selected_display = last_selected.to_display().item; use with caution, it get invalidated after a sort()
|
|
index_pair item_proxy::to_display() const
|
|
{
|
|
return ess_->lister.index_cast(pos_, false); //convert absolute position to display position
|
|
}
|
|
|
|
bool item_proxy::displayed() const
|
|
{
|
|
if (!ess_->lister.get(pos_.cat)->expand)
|
|
return false;
|
|
|
|
auto pos = to_display();
|
|
if (ess_->first_display() > pos)
|
|
return false;
|
|
|
|
auto last = ess_->lister.advance(ess_->first_display(), static_cast<int>(ess_->count_of_exposed(false)));
|
|
|
|
return (last > pos || last == pos);
|
|
}
|
|
|
|
bool item_proxy::empty() const noexcept
|
|
{
|
|
return !ess_;
|
|
}
|
|
|
|
item_proxy & item_proxy::check(bool ck, bool scroll_view)
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & m = cat_->items.at(pos_.item);
|
|
if(m.flags.checked != ck)
|
|
{
|
|
m.flags.checked = ck;
|
|
ess_->lister.emit_cs(pos_, false);
|
|
if (scroll_view)
|
|
{
|
|
if (ess_->lister.get(pos_.cat)->expand)
|
|
ess_->lister.get(pos_.cat)->expand = false;
|
|
|
|
if (!this->displayed())
|
|
ess_->lister.scroll_into_view(pos_, (ess_->first_display() > this->to_display() ? view_action::top_view : view_action::bottom_view));
|
|
}
|
|
|
|
ess_->update();
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
bool item_proxy::checked() const
|
|
{
|
|
return cat_->items.at(pos_.item).flags.checked;
|
|
}
|
|
|
|
/// is ignored if no change (maybe set last_selected anyway??), but if change emit event, deselect others if need ans set/unset last_selected
|
|
item_proxy & item_proxy::select(bool s, bool scroll_view)
|
|
{
|
|
internal_scope_guard lock;
|
|
|
|
//pos_ never represents a category if this item_proxy is available.
|
|
auto & m = cat_->items.at(pos_.item); // a ref to the real item
|
|
|
|
//ignore if no change
|
|
if(m.flags.selected == s)
|
|
return *this;
|
|
|
|
m.flags.selected = s; // actually change selection
|
|
|
|
ess_->lister.emit_cs(this->pos_, true);
|
|
|
|
if (m.flags.selected)
|
|
{
|
|
ess_->lister.cancel_others_if_single_enabled(true, pos_); //Cancel all selections except pos_ if single_selection is enabled.
|
|
ess_->lister.latest_selected_abs = pos_;
|
|
}
|
|
else if (ess_->lister.latest_selected_abs == pos_)
|
|
ess_->lister.latest_selected_abs.set_both(npos);
|
|
|
|
if (scroll_view && (!this->displayed()))
|
|
ess_->lister.scroll_into_view(pos_, (ess_->first_display() > this->to_display() ? view_action::top_view : view_action::bottom_view));
|
|
|
|
ess_->update();
|
|
return *this;
|
|
}
|
|
|
|
bool item_proxy::selected() const
|
|
{
|
|
return cat_->items.at(pos_.item).flags.selected;
|
|
}
|
|
|
|
item_proxy & item_proxy::bgcolor(const nana::color& col)
|
|
{
|
|
cat_->items.at(pos_.item).bgcolor = col;
|
|
ess_->update();
|
|
return *this;
|
|
}
|
|
|
|
nana::color item_proxy::bgcolor() const
|
|
{
|
|
return cat_->items.at(pos_.item).bgcolor;
|
|
}
|
|
|
|
item_proxy& item_proxy::fgcolor(const nana::color& col)
|
|
{
|
|
cat_->items.at(pos_.item).fgcolor = col;
|
|
ess_->update();
|
|
return *this;
|
|
}
|
|
|
|
nana::color item_proxy::fgcolor() const
|
|
{
|
|
return cat_->items.at(pos_.item).fgcolor;
|
|
}
|
|
|
|
std::size_t item_proxy::columns() const noexcept
|
|
{
|
|
return ess_->header.cont().size();
|
|
}
|
|
|
|
size_type item_proxy::column_cast(size_type pos, bool disp_order) const
|
|
{
|
|
return ess_->header.cast(pos, disp_order);
|
|
}
|
|
|
|
item_proxy& item_proxy::text(size_type col, cell cl)
|
|
{
|
|
ess_->lister.text(cat_, pos_.item, col, std::move(cl), columns());
|
|
ess_->update();
|
|
return *this;
|
|
}
|
|
|
|
item_proxy& item_proxy::text(size_type col, std::string str)
|
|
{
|
|
ess_->lister.text(cat_, pos_.item, col, std::move(str), columns());
|
|
ess_->update();
|
|
return *this;
|
|
}
|
|
|
|
item_proxy& item_proxy::text(size_type col, const std::wstring& str)
|
|
{
|
|
ess_->lister.text(cat_, pos_.item, col, to_utf8(str), columns());
|
|
ess_->update();
|
|
return *this;
|
|
}
|
|
|
|
std::string item_proxy::text(size_type col) const
|
|
{
|
|
return cat_->cells(pos_.item).at(col).text;
|
|
}
|
|
|
|
void item_proxy::icon(const nana::paint::image& img)
|
|
{
|
|
if (img)
|
|
{
|
|
auto & item = cat_->items.at(pos_.item);
|
|
item.img = img;
|
|
nana::fit_zoom(img.size(), nana::size(16, 16), item.img_show_size);
|
|
|
|
ess_->if_image = true;
|
|
ess_->update();
|
|
}
|
|
}
|
|
|
|
//Behavior of Iterator's value_type
|
|
#ifdef _nana_std_has_string_view
|
|
bool item_proxy::operator==(std::string_view sv) const
|
|
{
|
|
return (text(0) == sv);
|
|
}
|
|
|
|
bool item_proxy::operator==(std::wstring_view sv) const
|
|
{
|
|
return (text(0) == to_utf8(sv));
|
|
}
|
|
#else
|
|
bool item_proxy::operator==(const char * s) const
|
|
{
|
|
return this->operator==(std::string(s));
|
|
}
|
|
|
|
bool item_proxy::operator==(const wchar_t * s) const
|
|
{
|
|
return this->operator==(std::wstring(s));
|
|
}
|
|
|
|
bool item_proxy::operator==(const std::string& s) const
|
|
{
|
|
return (text(0) == s);
|
|
}
|
|
|
|
bool item_proxy::operator==(const std::wstring& s) const
|
|
{
|
|
return (text(0) == to_utf8(s));
|
|
}
|
|
#endif
|
|
|
|
item_proxy & item_proxy::operator=(const item_proxy& rhs)
|
|
{
|
|
if(this != &rhs)
|
|
{
|
|
ess_ = rhs.ess_;
|
|
cat_ = rhs.cat_;
|
|
pos_ = rhs.pos_;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
item_proxy & item_proxy::operator++()
|
|
{
|
|
if (++pos_.item >= cat_->items.size())
|
|
cat_ = nullptr;
|
|
|
|
return *this;
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
item_proxy item_proxy::operator++(int)
|
|
{
|
|
item_proxy ip(*this);
|
|
|
|
if (++pos_.item >= cat_->items.size())
|
|
cat_ = nullptr;
|
|
return ip;
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
item_proxy& item_proxy::operator*()
|
|
{
|
|
return *this;
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
const item_proxy& item_proxy::operator*() const
|
|
{
|
|
return *this;
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
item_proxy* item_proxy::operator->()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
const item_proxy* item_proxy::operator->() const
|
|
{
|
|
return this;
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
bool item_proxy::operator==(const item_proxy& rhs) const
|
|
{
|
|
if((ess_ != rhs.ess_) || (cat_ != rhs.cat_))
|
|
return false;
|
|
|
|
//They both are end iterator when cat_ == 0
|
|
return (!cat_ || (pos_ == rhs.pos_));
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
bool item_proxy::operator!=(const item_proxy& rhs) const
|
|
{
|
|
return ! this->operator==(rhs);
|
|
}
|
|
|
|
//Undocumented methods
|
|
essence * item_proxy::_m_ess() const noexcept
|
|
{
|
|
return ess_;
|
|
}
|
|
|
|
index_pair item_proxy::pos() const noexcept
|
|
{
|
|
return pos_;
|
|
}
|
|
|
|
auto item_proxy::_m_cells() const -> std::vector<cell>
|
|
{
|
|
return cat_->cells(pos_.item);
|
|
}
|
|
|
|
nana::any * item_proxy::_m_value(bool alloc_if_empty)
|
|
{
|
|
return ess_->lister.anyobj(pos_, alloc_if_empty);
|
|
}
|
|
|
|
const nana::any * item_proxy::_m_value() const
|
|
{
|
|
return ess_->lister.anyobj(pos_, false);
|
|
}
|
|
//end class item_proxy
|
|
|
|
//class cat_proxy
|
|
|
|
//the member cat_ is used for fast accessing to the category
|
|
cat_proxy::cat_proxy(essence * ess, size_type pos) noexcept
|
|
: ess_(ess),
|
|
pos_(pos)
|
|
{
|
|
_m_cat_by_pos();
|
|
}
|
|
|
|
cat_proxy::cat_proxy(essence* ess, category_t* cat) noexcept
|
|
: ess_(ess),
|
|
cat_(cat)
|
|
{
|
|
for (auto & m : ess->lister.cat_container())
|
|
{
|
|
if (&m == cat)
|
|
break;
|
|
++pos_;
|
|
}
|
|
}
|
|
|
|
model_guard cat_proxy::model()
|
|
{
|
|
if (!cat_->model_ptr)
|
|
throw std::runtime_error("nana::listbox has not a model for the category");
|
|
|
|
return{ cat_->model_ptr.get() };
|
|
}
|
|
|
|
void cat_proxy::append(std::initializer_list<std::string> arg)
|
|
{
|
|
const auto items = columns();
|
|
push_back({});
|
|
item_proxy ip{ ess_, index_pair(pos_, size() - 1) };
|
|
size_type pos = 0;
|
|
for (auto & txt : arg)
|
|
{
|
|
ip.text(pos++, txt);
|
|
if (pos >= items)
|
|
break;
|
|
}
|
|
}
|
|
|
|
void cat_proxy::append(std::initializer_list<std::wstring> arg)
|
|
{
|
|
const auto items = columns();
|
|
push_back({});
|
|
item_proxy ip{ ess_, index_pair(pos_, size() - 1) };
|
|
size_type pos = 0;
|
|
for (auto & txt : arg)
|
|
{
|
|
ip.text(pos++, txt);
|
|
if (pos >= items)
|
|
break;
|
|
}
|
|
}
|
|
|
|
cat_proxy & cat_proxy::select(bool sel)
|
|
{
|
|
for (item_proxy &it : *this )
|
|
it.select(sel);
|
|
|
|
ess_->lister.latest_selected_abs = index_pair {this->pos_, npos};
|
|
|
|
return *this;
|
|
}
|
|
|
|
bool cat_proxy::selected() const
|
|
{
|
|
for (item_proxy &it : *this )
|
|
if (!it.selected())
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
cat_proxy& cat_proxy::display_number(bool display)
|
|
{
|
|
if (cat_->display_number != display)
|
|
{
|
|
cat_->display_number = display;
|
|
ess_->update();
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
bool cat_proxy::expanded() const
|
|
{
|
|
return cat_->expand;
|
|
}
|
|
|
|
cat_proxy& cat_proxy::expanded(bool expand)
|
|
{
|
|
//The first category isn't allowed to be collapsed
|
|
if ((expand != cat_->expand) && pos_)
|
|
{
|
|
cat_->expand = expand;
|
|
ess_->update();
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
auto cat_proxy::columns() const -> size_type
|
|
{
|
|
return ess_->header.cont().size();
|
|
}
|
|
|
|
cat_proxy& cat_proxy::text(std::string s)
|
|
{
|
|
auto text = to_nstring(s);
|
|
internal_scope_guard lock;
|
|
if (text != cat_->text)
|
|
{
|
|
cat_->text = std::move(text);
|
|
ess_->update();
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
cat_proxy& cat_proxy::text(std::wstring s)
|
|
{
|
|
auto text = to_nstring(s);
|
|
internal_scope_guard lock;
|
|
if (text != cat_->text)
|
|
{
|
|
cat_->text = std::move(text);
|
|
ess_->update();
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
std::string cat_proxy::text() const
|
|
{
|
|
internal_scope_guard lock;
|
|
return to_utf8(cat_->text);
|
|
}
|
|
|
|
void cat_proxy::push_back(std::string s)
|
|
{
|
|
internal_scope_guard lock;
|
|
|
|
ess_->lister.throw_if_immutable_model(index_pair{ pos_ });
|
|
|
|
cat_->sorted.push_back(cat_->items.size());
|
|
|
|
if (cat_->model_ptr)
|
|
{
|
|
const auto cont = cat_->model_ptr->container();
|
|
auto pos = cont->size();
|
|
cont->emplace_back();
|
|
auto cells = cont->to_cells(pos);
|
|
if (cells.size())
|
|
cells.front().text = std::move(s);
|
|
else
|
|
cells.emplace_back(std::move(s));
|
|
|
|
cont->assign(pos, cells);
|
|
cat_->items.emplace_back();
|
|
}
|
|
else
|
|
cat_->items.emplace_back(std::move(s));
|
|
|
|
ess_->update();
|
|
}
|
|
|
|
//Behavior of a container
|
|
item_proxy cat_proxy::begin() const
|
|
{
|
|
auto i = ess_->lister.get(pos_);
|
|
if (i->items.empty())
|
|
return end();
|
|
|
|
return item_proxy(ess_, index_pair(pos_, 0));
|
|
}
|
|
|
|
//Behavior of a container
|
|
item_proxy cat_proxy::end() const
|
|
{
|
|
return item_proxy(ess_);
|
|
}
|
|
|
|
//Behavior of a container
|
|
item_proxy cat_proxy::cbegin() const
|
|
{
|
|
return begin();
|
|
}
|
|
|
|
//Behavior of a container
|
|
item_proxy cat_proxy::cend() const
|
|
{
|
|
return end();
|
|
}
|
|
|
|
item_proxy cat_proxy::at(size_type pos_abs) const
|
|
{
|
|
check_range(pos_abs, size());
|
|
return item_proxy(ess_, index_pair(pos_, pos_abs));
|
|
}
|
|
|
|
item_proxy cat_proxy::back() const
|
|
{
|
|
if (cat_->items.empty())
|
|
throw std::runtime_error("listbox.back() no element in the container.");
|
|
|
|
return item_proxy(ess_, index_pair(pos_, cat_->items.size() - 1));
|
|
}
|
|
|
|
size_type cat_proxy::index_cast(size_type from, bool from_display_order) const
|
|
{
|
|
return ess_->lister.index_cast(index_pair{ pos_, from }, from_display_order).item;
|
|
}
|
|
|
|
size_type cat_proxy::position() const
|
|
{
|
|
return pos_;
|
|
}
|
|
|
|
size_type cat_proxy::size() const
|
|
{
|
|
return cat_->items.size();
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
cat_proxy& cat_proxy::operator=(const cat_proxy& r)
|
|
{
|
|
if(this != &r)
|
|
{
|
|
ess_ = r.ess_;
|
|
cat_ = r.cat_;
|
|
pos_ = r.pos_;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
cat_proxy & cat_proxy::operator++()
|
|
{
|
|
++pos_;
|
|
_m_cat_by_pos();
|
|
|
|
return *this;
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
cat_proxy cat_proxy::operator++(int)
|
|
{
|
|
cat_proxy ip(*this);
|
|
++pos_;
|
|
_m_cat_by_pos();
|
|
|
|
return ip;
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
cat_proxy& cat_proxy::operator*()
|
|
{
|
|
return *this;
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
const cat_proxy& cat_proxy::operator*() const
|
|
{
|
|
return *this;
|
|
}
|
|
|
|
/// Behavior of Iterator
|
|
cat_proxy* cat_proxy::operator->()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
/// Behavior of Iterator
|
|
const cat_proxy* cat_proxy::operator->() const
|
|
{
|
|
return this;
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
bool cat_proxy::operator==(const cat_proxy& r) const
|
|
{
|
|
if(ess_ != r.ess_)
|
|
return false;
|
|
|
|
if(ess_) //Not empty
|
|
return (pos_ == r.pos_);
|
|
|
|
return true; //Both are empty
|
|
}
|
|
|
|
// Behavior of Iterator
|
|
bool cat_proxy::operator!=(const cat_proxy& r) const
|
|
{
|
|
return ! this->operator==(r);
|
|
}
|
|
|
|
void cat_proxy::inline_factory(size_type column, pat::cloneable<pat::abstract_factory<inline_notifier_interface>> factory)
|
|
{
|
|
check_range(column, this->columns());
|
|
|
|
if (column >= cat_->factories.size())
|
|
{
|
|
cat_->factories.resize(column + 1);
|
|
cat_->indicators.resize(column + 1);
|
|
}
|
|
|
|
cat_->factories[column] = std::move(factory);
|
|
cat_->indicators[column].reset(new inline_indicator(ess_, column));
|
|
}
|
|
|
|
void cat_proxy::_m_append(std::vector<cell> && cells)
|
|
{
|
|
//check invalid cells
|
|
for (auto & cl : cells)
|
|
{
|
|
if (cl.text.size() == 1 && cl.text[0] == wchar_t(0))
|
|
{
|
|
cl.text.clear();
|
|
cl.custom_format.reset();
|
|
}
|
|
}
|
|
|
|
internal_scope_guard lock;
|
|
|
|
if (cat_->model_ptr)
|
|
{
|
|
es_lister::throw_if_immutable_model(cat_->model_ptr.get());
|
|
|
|
auto container = cat_->model_ptr->container();
|
|
|
|
auto item_index = container->size();
|
|
cat_->items.emplace_back();
|
|
container->emplace_back();
|
|
|
|
container->assign(item_index, cells);
|
|
}
|
|
else
|
|
{
|
|
cells.resize(columns());
|
|
cat_->items.emplace_back(std::move(cells));
|
|
}
|
|
|
|
cat_->sorted.push_back(cat_->items.size() - 1);
|
|
}
|
|
|
|
void cat_proxy::_m_try_append_model(const const_virtual_pointer& dptr)
|
|
{
|
|
//Throws when appends an object to a listbox which should have a model.
|
|
if (!cat_->model_ptr)
|
|
throw std::runtime_error("nana::listbox hasn't a model");
|
|
|
|
ess_->lister.throw_if_immutable_model(cat_->model_ptr.get());
|
|
|
|
if (!cat_->model_ptr->container()->push_back(dptr))
|
|
throw std::invalid_argument("nana::listbox, the type of operand object is mismatched with model container value_type");
|
|
|
|
cat_->sorted.push_back(cat_->items.size());
|
|
cat_->items.emplace_back();
|
|
}
|
|
|
|
void cat_proxy::_m_cat_by_pos() noexcept
|
|
{
|
|
if (pos_ >= ess_->lister.cat_container().size())
|
|
{
|
|
ess_ = nullptr;
|
|
cat_ = nullptr;
|
|
return;
|
|
}
|
|
|
|
auto i = ess_->lister.get(pos_);
|
|
cat_ = &(*i);
|
|
}
|
|
|
|
//A fix for auto_draw, to make sure the inline widget set() issued after value() and value_ptr() are actually set.
|
|
//Fixed by leobackes(pr#86)
|
|
void cat_proxy::_m_update() noexcept
|
|
{
|
|
ess_->update();
|
|
}
|
|
|
|
void cat_proxy::_m_reset_model(model_interface* p)
|
|
{
|
|
if (ess_->listbox_ptr)
|
|
{
|
|
cat_->model_ptr.reset(p);
|
|
cat_->items.clear();
|
|
|
|
cat_->items.resize(cat_->model_ptr->container()->size());
|
|
|
|
cat_->make_sort_order();
|
|
ess_->lister.sort();
|
|
|
|
//Don't ignore the auto-draw flag for performance enhancement.
|
|
ess_->update();
|
|
}
|
|
}
|
|
//end class cat_proxy
|
|
}
|
|
}//end namespace drawerbase
|
|
|
|
arg_listbox::arg_listbox(const drawerbase::listbox::item_proxy& m) noexcept
|
|
: item(m)
|
|
{
|
|
}
|
|
|
|
|
|
//Implementation of arg_listbox_category
|
|
//Contributed by leobackes(pr#97)
|
|
arg_listbox_category::arg_listbox_category(const nana::drawerbase::listbox::cat_proxy& cat) noexcept
|
|
: category(cat)
|
|
{
|
|
}
|
|
|
|
//class listbox
|
|
|
|
listbox::listbox(window wd, bool visible)
|
|
{
|
|
create(wd, rectangle(), visible);
|
|
}
|
|
|
|
listbox::listbox(window wd, const rectangle& r, bool visible)
|
|
{
|
|
create(wd, r, visible);
|
|
}
|
|
|
|
bool listbox::assoc_ordered(bool enable)
|
|
{
|
|
internal_scope_guard lock;
|
|
|
|
if (_m_ess().lister.enable_ordered(enable))
|
|
_m_ess().update();
|
|
|
|
return true;
|
|
}
|
|
|
|
void listbox::auto_draw(bool enabled) noexcept
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
if (ess.auto_draw != enabled)
|
|
{
|
|
ess.auto_draw = enabled;
|
|
ess.update();
|
|
}
|
|
}
|
|
|
|
void listbox::scroll(bool to_bottom, size_type cat_pos)
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
auto cats = this->size_categ();
|
|
|
|
if (::nana::npos != cat_pos)
|
|
{
|
|
if (cat_pos >= cats)
|
|
throw std::invalid_argument("listbox: invalid category");
|
|
}
|
|
else
|
|
cat_pos = cats - 1;
|
|
|
|
index_pair pos(cat_pos);
|
|
if (to_bottom)
|
|
{
|
|
auto items = ess.lister.size_item(cat_pos);
|
|
pos.item = (0 == items ? ::nana::npos : items - 1);
|
|
}
|
|
else
|
|
pos.item = ess.lister.size_item(cat_pos) ? 0 : ::nana::npos;
|
|
|
|
this->scroll(to_bottom, pos);
|
|
}
|
|
|
|
void listbox::scroll(bool to_bottom, const index_pair& abs_pos)
|
|
{
|
|
using view_action = drawerbase::listbox::view_action;
|
|
_m_ess().lister.scroll_into_view(abs_pos, (to_bottom ? view_action::bottom_view : view_action::top_view));
|
|
_m_ess().update();
|
|
}
|
|
|
|
listbox::size_type listbox::append_header(std::string s, unsigned width)
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
auto pos = ess.header.create(&ess, to_nstring(std::move(s)), width);
|
|
ess.update();
|
|
return pos;
|
|
}
|
|
|
|
listbox::size_type listbox::append_header(std::wstring s, unsigned width)
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
auto pos = ess.header.create(&ess, to_nstring(std::move(s)), width);
|
|
ess.update();
|
|
return pos;
|
|
}
|
|
|
|
void listbox::clear_headers()
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
ess.lister.erase();
|
|
ess.header.clear();
|
|
ess.update();
|
|
}
|
|
|
|
listbox::cat_proxy listbox::append(std::string s)
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
auto new_cat_ptr = ess.lister.create_category(to_nstring(std::move(s)));
|
|
ess.update();
|
|
|
|
return cat_proxy{ &ess, new_cat_ptr };
|
|
}
|
|
|
|
listbox::cat_proxy listbox::append(std::wstring s)
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
auto new_cat_ptr = ess.lister.create_category(to_nstring(std::move(s)));
|
|
ess.update();
|
|
return cat_proxy{ &ess, new_cat_ptr };
|
|
}
|
|
|
|
void listbox::append(std::initializer_list<std::string> categories)
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
|
|
for (auto & arg : categories)
|
|
ess.lister.create_category(native_string_type(to_nstring(arg)));
|
|
ess.update();
|
|
}
|
|
|
|
void listbox::append(std::initializer_list<std::wstring> categories)
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
|
|
for (auto & arg : categories)
|
|
ess.lister.create_category(native_string_type(to_nstring(arg)));
|
|
ess.update();
|
|
}
|
|
|
|
rectangle listbox::content_area() const
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
auto carea = ess.content_area();
|
|
carea.x += ess.header.margin();
|
|
carea.width -= (std::min)(carea.width, ess.header.margin());
|
|
return carea;
|
|
}
|
|
|
|
auto listbox::insert(cat_proxy cat, std::string str) -> cat_proxy
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
auto new_cat_ptr = ess.lister.create_category(to_nstring(std::move(str)), cat.position());
|
|
return cat_proxy{ &ess, new_cat_ptr };
|
|
}
|
|
|
|
auto listbox::insert(cat_proxy cat, std::wstring str) -> cat_proxy
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
auto new_cat_ptr = ess.lister.create_category(to_nstring(std::move(str)), cat.position());
|
|
return cat_proxy{ &ess, new_cat_ptr };
|
|
}
|
|
|
|
|
|
void listbox::insert_item(const index_pair& pos, std::string text)
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
ess.lister.insert(pos, std::move(text), this->column_size());
|
|
|
|
if (!empty())
|
|
{
|
|
auto & item = ess.lister.at(pos);
|
|
item.bgcolor = bgcolor();
|
|
item.fgcolor = fgcolor();
|
|
ess.update();
|
|
}
|
|
}
|
|
|
|
void listbox::insert_item(const index_pair& pos, const std::wstring& text)
|
|
{
|
|
insert_item(pos, to_utf8(text));
|
|
}
|
|
|
|
void listbox::insert_item(index_pair abs_pos, const listbox& rhs, const index_pairs& indexes)
|
|
{
|
|
auto const columns = (std::min)(this->column_size(), rhs.column_size());
|
|
|
|
if (0 == columns)
|
|
return;
|
|
|
|
item_proxy it_new = this->at(abs_pos.cat).end();
|
|
for (auto & idx : indexes)
|
|
{
|
|
auto it_src = rhs.at(idx.cat).at(idx.item);
|
|
|
|
if (abs_pos.item < this->at(abs_pos.cat).size())
|
|
{
|
|
this->insert_item(abs_pos, it_src.text(0));
|
|
it_new = this->at(abs_pos);
|
|
}
|
|
else
|
|
{
|
|
it_new = this->at(abs_pos.cat).append(it_src.text(0));
|
|
}
|
|
|
|
for (std::size_t col = 1; col < columns; ++col)
|
|
it_new.text(col, it_src.text(col));
|
|
|
|
++abs_pos.item;
|
|
}
|
|
}
|
|
|
|
listbox::cat_proxy listbox::at(size_type pos)
|
|
{
|
|
internal_scope_guard lock;
|
|
check_range(pos, size_categ());
|
|
return{ &_m_ess(), pos };
|
|
}
|
|
|
|
const listbox::cat_proxy listbox::at(size_type pos) const
|
|
{
|
|
internal_scope_guard lock;
|
|
check_range(pos, size_categ());
|
|
return{ &_m_ess(), pos };
|
|
}
|
|
|
|
listbox::item_proxy listbox::at(const index_pair& abs_pos)
|
|
{
|
|
internal_scope_guard lock;
|
|
return at(abs_pos.cat).at(abs_pos.item);
|
|
}
|
|
|
|
const listbox::item_proxy listbox::at(const index_pair& pos_abs) const
|
|
{
|
|
internal_scope_guard lock;
|
|
return at(pos_abs.cat).at(pos_abs.item);
|
|
}
|
|
|
|
// Contributed by leobackes(pr#97)
|
|
listbox::index_pair listbox::cast( const point& pos ) const
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess=_m_ess();
|
|
auto _where = ess.where(pos);
|
|
|
|
if (drawerbase::listbox::essence::parts::list == _where.first)
|
|
return ess.lister.advance(ess.first_display(), static_cast<int>(_where.second));
|
|
|
|
return index_pair{ npos, npos };
|
|
}
|
|
|
|
listbox::index_pair listbox::hovered(bool return_end) const
|
|
{
|
|
using parts = drawerbase::listbox::essence::parts;
|
|
|
|
internal_scope_guard lock;
|
|
|
|
auto cur_pos = API::cursor_position();
|
|
API::calc_window_point(handle(), cur_pos);
|
|
|
|
auto pt_where = _m_ess().where(cur_pos);
|
|
|
|
if ((pt_where.first == parts::list || pt_where.first == parts::checker) && pt_where.second != npos)
|
|
{
|
|
auto pos = _m_ess().lister.advance(_m_ess().first_display(), static_cast<int>(pt_where.second));
|
|
if (return_end && pos.is_category())
|
|
{
|
|
if (0 < pos.cat)
|
|
--pos.cat;
|
|
pos.item = this->size_item(pos.cat);
|
|
}
|
|
return pos;
|
|
|
|
}
|
|
else if (return_end)
|
|
return index_pair{ this->size_categ() - 1, this->size_item(this->size_categ() - 1) };
|
|
|
|
return index_pair{ npos, npos };
|
|
}
|
|
|
|
auto listbox::column_at(size_type pos, bool disp_order) -> column_interface&
|
|
{
|
|
internal_scope_guard lock;
|
|
return _m_ess().header.at(pos, disp_order);
|
|
}
|
|
|
|
auto listbox::column_at(size_type pos, bool disp_order) const -> const column_interface&
|
|
{
|
|
internal_scope_guard lock;
|
|
return _m_ess().header.at(pos, disp_order);
|
|
}
|
|
|
|
auto listbox::column_size() const ->size_type
|
|
{
|
|
internal_scope_guard lock;
|
|
return _m_ess().header.cont().size();
|
|
}
|
|
|
|
|
|
void listbox::column_resizable(bool resizable)
|
|
{
|
|
internal_scope_guard lock;
|
|
_m_ess().header.attrib().resizable = resizable;
|
|
}
|
|
|
|
bool listbox::column_resizable() const
|
|
{
|
|
internal_scope_guard lock;
|
|
return _m_ess().header.attrib().resizable;
|
|
}
|
|
|
|
void listbox::column_movable(bool movable)
|
|
{
|
|
internal_scope_guard lock;
|
|
_m_ess().header.attrib().movable = movable;
|
|
}
|
|
|
|
bool listbox::column_movable() const
|
|
{
|
|
internal_scope_guard lock;
|
|
return _m_ess().header.attrib().movable;
|
|
}
|
|
|
|
//Contributed by leobackes(pr#97)
|
|
listbox::size_type listbox::column_from_pos ( const point& pos ) const
|
|
{
|
|
internal_scope_guard lock;
|
|
return _m_ess().column_from_pos(pos.x);
|
|
}
|
|
|
|
void listbox::checkable(bool chkable)
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
if(ess.checkable != chkable)
|
|
{
|
|
ess.checkable = chkable;
|
|
ess.update();
|
|
}
|
|
}
|
|
|
|
auto listbox::checked() const -> index_pairs
|
|
{
|
|
internal_scope_guard lock;
|
|
return _m_ess().lister.pick_items(false);
|
|
}
|
|
|
|
void listbox::clear(size_type cat)
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
|
|
auto origin = ess.content_view->origin();
|
|
|
|
int new_pos = origin.y;
|
|
|
|
auto start_pos = static_cast<int>(ess.lister.distance(ess.lister.first(), index_pair{cat, npos}) * ess.item_height());
|
|
auto count = static_cast<int>(ess.lister.size_item(cat) * ess.item_height());
|
|
if (start_pos + count <= origin.y)
|
|
new_pos = origin.y - static_cast<int>(count);
|
|
else if (start_pos < origin.y && origin.y < start_pos + count)
|
|
new_pos = start_pos + ess.item_height();
|
|
|
|
ess.lister.clear(cat);
|
|
|
|
ess.calc_content_size(false);
|
|
ess.content_view->change_position(new_pos, false, false);
|
|
ess.content_view->sync(false);
|
|
ess.update();
|
|
}
|
|
|
|
void listbox::clear()
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
|
|
ess.lister.clear();
|
|
unsort(); // apperar to be espected
|
|
|
|
ess.calc_content_size(false);
|
|
ess.content_view->change_position(0, false, false);
|
|
ess.content_view->sync(false);
|
|
|
|
ess.update();
|
|
}
|
|
|
|
void listbox::erase(size_type cat)
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
|
|
auto origin = ess.content_view->origin();
|
|
|
|
auto start_pos = ess.content_position(index_pair{ cat, npos });
|
|
|
|
int new_pos = origin.y;
|
|
auto count = static_cast<int>((ess.lister.size_item(cat) + 1) * ess.item_height());
|
|
if (start_pos + count <= origin.y)
|
|
new_pos = origin.y - static_cast<int>(count);
|
|
else if (start_pos < origin.y && origin.y < start_pos + count)
|
|
new_pos = start_pos;
|
|
|
|
ess.lister.erase(cat);
|
|
|
|
ess.calc_content_size(false);
|
|
ess.content_view->change_position(new_pos, false, false);
|
|
ess.content_view->sync(false);
|
|
|
|
ess.update();
|
|
}
|
|
|
|
void listbox::erase()
|
|
{
|
|
internal_scope_guard lock;
|
|
auto & ess = _m_ess();
|
|
ess.lister.erase();
|
|
ess.calc_content_size();
|
|
ess.update();
|
|
}
|
|
|
|
void listbox::erase(index_pairs indexes)
|
|
{
|
|
internal_scope_guard lock;
|
|
|
|
std::sort(indexes.begin(), indexes.end(), [](const index_pair& pos1, const index_pair& pos2)
|
|
{
|
|
return (pos1 > pos2);
|
|
});
|
|
|
|
auto & ess = _m_ess();
|
|
|
|
auto const origin = ess.content_view->origin();
|
|
|
|
for (auto & pos : indexes)
|
|
{
|
|
auto & cat = *ess.lister.get(pos.cat);
|
|
if (pos.item < cat.items.size())
|
|
{
|
|
if (cat.model_ptr)
|
|
{
|
|
drawerbase::listbox::es_lister::throw_if_immutable_model(cat.model_ptr.get());
|
|
cat.model_ptr->container()->erase(pos.item);
|
|
}
|
|
|
|
cat.items.erase(cat.items.begin() + pos.item);
|
|
}
|
|
}
|
|
|
|
//Rebuild the sorted order
|
|
std::size_t this_cat = this->size_categ();
|
|
for (auto & pos : indexes)
|
|
{
|
|
if (this_cat != pos.cat)
|
|
{
|
|
this_cat = pos.cat;
|
|
ess.lister.get(this_cat)->make_sort_order();
|
|
}
|
|
}
|
|
|
|
ess.calc_content_size(false);
|
|
ess.content_view->change_position(origin.y, false, false);
|
|
ess.content_view->sync(false);
|
|
|
|
ess.lister.sort();
|
|
|
|
ess.update();
|
|
}
|
|
|
|
listbox::item_proxy listbox::erase(item_proxy ip)
|
|
{
|
|
if(ip.empty())
|
|
return ip;
|
|
|
|
internal_scope_guard lock;
|
|
auto * ess = ip._m_ess();
|
|
auto _where = ip.pos();
|
|
|
|
auto origin = ess->content_view->origin();
|
|
auto start_pos = ess->content_position(_where);
|
|
if (start_pos < origin.y)
|
|
origin.y -= ess->item_height();
|
|
|
|
ess->lister.erase(_where);
|
|
|
|
ess->calc_content_size(false);
|
|
ess->content_view->change_position(origin.y, false, false);
|
|
ess->content_view->sync(false);
|
|
|
|
ess->update();
|
|
if(_where.item < ess->lister.size_item(_where.cat))
|
|
return ip;
|
|
return item_proxy(ess);
|
|
}
|
|
|
|
bool listbox::sortable() const
|
|
{
|
|
internal_scope_guard lock;
|
|
return _m_ess().header.attrib().sortable;
|
|
}
|
|
|
|
void listbox::sortable(bool enable)
|
|
{
|
|
internal_scope_guard lock;
|
|
_m_ess().header.attrib().sortable = enable;
|
|
}
|
|
|
|
void listbox::set_sort_compare(size_type col, std::function<bool(const std::string&, nana::any*, const std::string&, nana::any*, bool reverse)> strick_ordering)
|
|
{
|
|
internal_scope_guard lock;
|
|
_m_ess().header.at(col).weak_ordering = std::move(strick_ordering);
|
|
}
|
|
|
|
/// sort() and ivalidate any existing reference from display position to absolute item, that is: after sort() display offset point to different items
|
|
void listbox::sort_col(size_type col, bool reverse)
|
|
{
|
|
internal_scope_guard lock;
|
|
_m_ess().lister.sort_column(col, &reverse);
|
|
}
|
|
|
|
auto listbox::sort_col() const -> size_type
|
|
{
|
|
internal_scope_guard lock;
|
|
return _m_ess().lister.sort_attrs().column;
|
|
}
|
|
|
|
/// potencially ivalidate any existing reference from display position to absolute item, that is: after sort() display offset point to different items
|
|
void listbox::unsort()
|
|
{
|
|
internal_scope_guard lock;
|
|
this->sort_col(npos, false);
|
|
}
|
|
|
|
bool listbox::freeze_sort(bool freeze)
|
|
{
|
|
internal_scope_guard lock;
|
|
return !_m_ess().lister.active_sort(!freeze);
|
|
}
|
|
|
|
auto listbox::selected() const -> index_pairs
|
|
{
|
|
internal_scope_guard lock;
|
|
return _m_ess().lister.pick_items(true); // absolute positions, no relative to display
|
|
}
|
|
|
|
void listbox::show_header(bool sh)
|
|
{
|
|
internal_scope_guard lock;
|
|
_m_ess().header.attrib().visible = sh;
|
|
_m_ess().update();
|
|
}
|
|
|
|
bool listbox::visible_header() const
|
|
{
|
|
internal_scope_guard lock;
|
|
return _m_ess().header.attrib().visible;
|
|
}
|
|
|
|
void listbox::move_select(bool upwards) ///<Selects an item besides the current selected item in the display.
|
|
{
|
|
internal_scope_guard lock;
|
|
_m_ess().lister.move_select(upwards, true, true);
|
|
_m_ess().update();
|
|
}
|
|
|
|
listbox::size_type listbox::size_categ() const
|
|
{
|
|
return _m_ess().lister.cat_container().size();
|
|
}
|
|
|
|
listbox::size_type listbox::size_item(size_type categ) const
|
|
{
|
|
return _m_ess().lister.size_item(categ);
|
|
}
|
|
|
|
void listbox::enable_single(bool for_selection, bool category_limited)
|
|
{
|
|
internal_scope_guard lock;
|
|
_m_ess().lister.enable_single(for_selection, category_limited);
|
|
}
|
|
|
|
void listbox::disable_single(bool for_selection)
|
|
{
|
|
_m_ess().lister.disable_single(for_selection);
|
|
}
|
|
|
|
listbox::export_options& listbox::def_export_options()
|
|
{
|
|
return _m_ess().def_exp_options;
|
|
}
|
|
|
|
listbox& listbox::category_icon(std::function<void(paint::graphics& graph, const rectangle& rt_icon, bool expanded)> icon_renderer)
|
|
{
|
|
internal_scope_guard lock;
|
|
_m_ess().ctg_icon_renderer.swap(icon_renderer);
|
|
_m_ess().update();
|
|
return *this;
|
|
}
|
|
|
|
listbox& listbox::category_icon(const paint::image& img_expanded, const paint::image&& img_collapsed)
|
|
{
|
|
internal_scope_guard lock;
|
|
_m_ess().ctg_icon_renderer = [img_expanded, img_collapsed](paint::graphics& graph, const rectangle& rt_icon, bool expanded)
|
|
{
|
|
if (expanded)
|
|
{
|
|
img_expanded.stretch(rectangle{ img_expanded.size() }, graph, rt_icon);
|
|
}
|
|
else
|
|
{
|
|
img_collapsed.stretch(rectangle{ img_collapsed.size() }, graph, rt_icon);
|
|
}
|
|
};
|
|
|
|
_m_ess().update();
|
|
return *this;
|
|
}
|
|
|
|
auto listbox::first_visible() const ->index_pair
|
|
{
|
|
return _m_ess().first_display();
|
|
}
|
|
|
|
auto listbox::last_visible() const -> index_pair
|
|
{
|
|
return _m_ess().lister.advance(_m_ess().first_display(), _m_ess().count_of_exposed(true));
|
|
}
|
|
|
|
auto listbox::visibles() const -> index_pairs
|
|
{
|
|
index_pairs indexes;
|
|
|
|
auto idx = _m_ess().first_display();
|
|
|
|
auto n = _m_ess().count_of_exposed(true);
|
|
while (n > 0)
|
|
{
|
|
if (idx.empty())
|
|
break;
|
|
|
|
if (idx.is_category())
|
|
{
|
|
indexes.push_back(idx);
|
|
--n;
|
|
}
|
|
else
|
|
{
|
|
auto const count = (std::min)(_m_ess().lister.size_item(idx.cat) - idx.item, n);
|
|
for (std::size_t i = 0; i < count; ++i)
|
|
{
|
|
indexes.push_back(idx);
|
|
++idx.item;
|
|
}
|
|
if (count)
|
|
{
|
|
n -= count;
|
|
--idx.item;
|
|
}
|
|
}
|
|
|
|
idx = _m_ess().lister.advance(idx, 1);
|
|
}
|
|
|
|
return indexes;
|
|
}
|
|
|
|
drawerbase::listbox::essence & listbox::_m_ess() const
|
|
{
|
|
return get_drawer_trigger().ess();
|
|
}
|
|
|
|
nana::any* listbox::_m_anyobj(size_type cat, size_type index, bool allocate_if_empty) const
|
|
{
|
|
return _m_ess().lister.anyobj(index_pair{cat, index}, allocate_if_empty);
|
|
}
|
|
|
|
drawerbase::listbox::category_t* listbox::_m_assoc(std::shared_ptr<nana::detail::key_interface> ptr, bool create_if_not_exists)
|
|
{
|
|
auto & ess = _m_ess();
|
|
|
|
internal_scope_guard lock;
|
|
|
|
for (auto & m : ess.lister.cat_container())
|
|
{
|
|
if (m.key_ptr && nana::detail::pred_equal(ptr.get(), m.key_ptr.get()))
|
|
return &m;
|
|
}
|
|
|
|
if (!create_if_not_exists)
|
|
return nullptr;
|
|
|
|
drawerbase::listbox::category_t* cat;
|
|
|
|
if (ess.lister.enable_ordered())
|
|
{
|
|
cat = ess.lister.create_category(ptr);
|
|
}
|
|
else
|
|
{
|
|
cat = ess.lister.create_category(native_string_type{});
|
|
cat->key_ptr = ptr;
|
|
}
|
|
ess.update();
|
|
return cat;
|
|
}
|
|
|
|
void listbox::_m_erase_key(nana::detail::key_interface* p) noexcept
|
|
{
|
|
auto & cont = _m_ess().lister.cat_container();
|
|
|
|
internal_scope_guard lock;
|
|
for (auto i = cont.begin(); i != cont.end(); ++i)
|
|
{
|
|
if (i->key_ptr && nana::detail::pred_equal(p, i->key_ptr.get()))
|
|
{
|
|
cont.erase(i);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<scroll_operation_interface> listbox::_m_scroll_operation()
|
|
{
|
|
internal_scope_guard lock;
|
|
return _m_ess().content_view->scroll_operation();
|
|
}
|
|
//end class listbox
|
|
}//end namespace nana
|