/* * A textbase class implementation * Nana C++ Library(http://www.nanapro.org) * Copyright(C) 2003-2019 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/skeletons/textbase.hpp * @description: This class manages the multi-line text and provides some operation on text */ #ifndef NANA_GUI_WIDGET_DETAIL_TEXTBASE_HPP #define NANA_GUI_WIDGET_DETAIL_TEXTBASE_HPP #include #include #include #include #include #include "textbase_export_interface.hpp" #include #include #include namespace nana { namespace widgets { namespace skeletons { template class textbase : public ::nana::noncopyable { public: using char_type = CharT; using string_type = std::basic_string; using size_type = typename string_type::size_type; using path_type = std::filesystem::path; textbase() { attr_max_.reset(); //Insert an empty string for the first line of empty text. text_cont_.emplace_back(); } void set_event_agent(textbase_event_agent_interface * evt) { evt_agent_ = evt; } bool empty() const { return (text_cont_.empty() || ((text_cont_.size() == 1) && text_cont_.front().empty())); } bool load(const path_type& file) { std::ifstream ifs{ file.string() }; if (!ifs) return false; std::error_code err; auto const bytes = file_size(file, err); if (err) return false; if(bytes >= 2) { int ch = ifs.get(); if(0xEF == ch && bytes >= 3) { //UTF8 ch = ifs.get(); if(0xBB == ch && 0xBF == ifs.get()) { ifs.close(); return load(file, nana::unicode::utf8); } } else if(0xFF == ch) { if(0xFE == ifs.get()) { //UTF16,UTF32 if((bytes >= 4) && (ifs.get() == 0 && ifs.get() == 0)) { ifs.close(); return load(file, nana::unicode::utf32); } ifs.close(); return load(file, nana::unicode::utf16); } } else if(0xFE == ch) { if(ifs.get() == 0xFF) { //UTF16(big-endian) ifs.close(); return load(file, nana::unicode::utf16); } } else if(0 == ch) { if(bytes >= 4 && ifs.get() == 0) { ch = ifs.get(); if(0xFE == ch && ifs.get() == 0xFF) { //UTF32(big_endian) ifs.close(); return load(file, nana::unicode::utf32); } } } } ifs.clear(); ifs.seekg(0, std::ios::beg); text_cont_.clear(); //Clear only if the file can be opened. attr_max_.reset(); std::string str_mbs; while(ifs.good()) { std::getline(ifs, str_mbs); text_cont_.emplace_back(static_cast(nana::charset{ str_mbs })); if (text_cont_.back().size() > attr_max_.size) { attr_max_.size = text_cont_.back().size(); attr_max_.line = text_cont_.size() - 1; } } _m_saved(file); return true; } static void byte_order_translate_2bytes(std::string& str) { char * s = const_cast(str.c_str()); char * end = s + str.size(); for(; s < end; s += 2) { char c = *s; *s = *(s + 1); *(s + 1) = c; } } static void byte_order_translate_4bytes(std::string& str) { char * s = const_cast(str.c_str()); char * end = s + str.size(); for(; s < end; s += 4) { char c = *s; *s = *(s + 3); *(s + 3) = c; c = *(s + 1); *(s + 1) = *(s + 2); *(s + 2) = c; } } bool load(const path_type& file, nana::unicode encoding) { std::ifstream ifs{ file.string() }; if (!ifs) return false; std::string str; bool big_endian = true; if(ifs.good()) { text_cont_.clear(); //Clear only if the file can be opened. attr_max_.reset(); std::getline(ifs, str); std::size_t len_of_BOM = 0; switch(encoding) { case nana::unicode::utf8: len_of_BOM = 3; break; case nana::unicode::utf16: len_of_BOM = 2; break; case nana::unicode::utf32: len_of_BOM = 4; break; default: throw std::runtime_error("Specified a wrong UTF"); } big_endian = (str[0] == 0x00 || str[0] == char(0xFE)); str.erase(0, len_of_BOM); if(big_endian) { if(nana::unicode::utf16 == encoding) byte_order_translate_2bytes(str); else byte_order_translate_4bytes(str); } text_cont_.emplace_back(static_cast(nana::charset{ str, encoding })); attr_max_.size = text_cont_.back().size(); attr_max_.line = 0; } while(ifs.good()) { std::getline(ifs, str); if(big_endian) { if(nana::unicode::utf16 == encoding) byte_order_translate_2bytes(str); else byte_order_translate_4bytes(str); } text_cont_.emplace_back(static_cast(nana::charset{ str, encoding })); if (text_cont_.back().size() > attr_max_.size) { attr_max_.size = text_cont_.back().size(); attr_max_.line = text_cont_.size() - 1; } } _m_saved(file); return true; } void store(const path_type& filename, bool is_unicode, ::nana::unicode encoding) const { std::ofstream ofs(filename.string(), std::ios::binary); if(ofs && text_cont_.size()) { auto i = text_cont_.cbegin(); auto const count = text_cont_.size() - 1; std::string last_mbs; if (is_unicode) { const char * le_boms[] = { "\xEF\xBB\xBF", "\xFF\xFE", "\xFF\xFE\x0\x0" }; //BOM for little-endian int bytes = 0; switch (encoding) { case nana::unicode::utf8: bytes = 3; break; case nana::unicode::utf16: bytes = 2; break; case nana::unicode::utf32: bytes = 4; break; } if (bytes) ofs.write(le_boms[static_cast(encoding)], bytes); for (std::size_t pos = 0; pos < count; ++pos) { auto mbs = nana::charset(*(i++)).to_bytes(encoding); ofs.write(mbs.c_str(), static_cast(mbs.size())); ofs.write("\r\n", 2); } last_mbs = nana::charset(text_cont_.back()).to_bytes(encoding); } else { for (std::size_t pos = 0; pos < count; ++pos) { std::string mbs = nana::charset(*(i++)); ofs.write(mbs.c_str(), mbs.size()); ofs.write("\r\n", 2); } last_mbs = nana::charset(text_cont_.back()); } ofs.write(last_mbs.c_str(), static_cast(last_mbs.size())); _m_saved(filename); } } //Triggers the text_changed event. //It is exposed for outter classes. For a outter class(eg. text_editor), a changing text content operation //may contains multiple textbase operations, therefore, the outter class determines when an event should be triggered. // //Addtional, using text_changed() method, it is possible to allow a outter class performing some updating operations //before triggering text_changed event. void text_changed() { if (!changed_) { _m_emit_first_change(); changed_ = true; } if (edited_) { if (evt_agent_) evt_agent_->text_changed(); edited_ = false; } } size_type lines() const { return text_cont_.size(); } const string_type& getline(size_type pos) const { if (pos < text_cont_.size()) return text_cont_[pos]; return nullstr_; } std::pair max_line() const { return std::make_pair(attr_max_.line, attr_max_.size); } public: void replace(size_type pos, string_type && text) { if (text_cont_.size() <= pos) { text_cont_.emplace_back(std::move(text)); pos = text_cont_.size() - 1; } else _m_at(pos).swap(text); _m_make_max(pos); edited_ = true; } void insert(upoint pos, string_type && str) { if(pos.y < text_cont_.size()) { string_type& lnstr = _m_at(pos.y); if(pos.x < lnstr.size()) lnstr.insert(pos.x, str); else lnstr += str; } else { text_cont_.emplace_back(std::move(str)); pos.y = static_cast(text_cont_.size() - 1); } _m_make_max(pos.y); edited_ = true; } void insertln(size_type pos, string_type&& str) { if (pos < text_cont_.size()) text_cont_.emplace(_m_iat(pos), std::move(str)); else text_cont_.emplace_back(std::move(str)); _m_make_max(pos); edited_ = true; } void erase(size_type line, size_type pos, size_type count) { if (line < text_cont_.size()) { string_type& lnstr = _m_at(line); if ((pos == 0) && (count >= lnstr.size())) lnstr.clear(); else lnstr.erase(pos, count); if (attr_max_.line == line) _m_scan_for_max(); edited_ = true; } } bool erase(size_type pos, std::size_t n) { //Bounds checking if ((pos >= text_cont_.size()) || (0 == n)) return false; if (pos + n > text_cont_.size()) n = text_cont_.size() - pos; text_cont_.erase(_m_iat(pos), _m_iat(pos + n)); if (pos <= attr_max_.line && attr_max_.line < pos + n) _m_scan_for_max(); else if (pos < attr_max_.line) attr_max_.line -= n; edited_ = true; return true; } void erase_all() { text_cont_.clear(); attr_max_.reset(); text_cont_.emplace_back(); //text_cont_ must not be empty _m_saved({}); } void merge(size_type pos) { if(pos + 1 < text_cont_.size()) { auto i = _m_iat(pos + 1); _m_at(pos) += *i; text_cont_.erase(i); _m_make_max(pos); //If the maxline is behind the pos line, //decrease the maxline. Because a line between maxline and pos line //has been deleted. if(pos < attr_max_.line) --attr_max_.line; edited_ = true; } } const path_type& filename() const { return filename_; } bool edited() const { return changed_; } void reset_status(bool remain_saved_filename) { if(!remain_saved_filename) filename_.clear(); changed_ = false; } bool saved() const { return !(changed_ || filename_.empty()); } private: string_type& _m_at(size_type pos) { return *_m_iat(pos); } typename std::deque::iterator _m_iat(size_type pos) { return text_cont_.begin() + pos; } void _m_make_max(std::size_t pos) { const string_type& str = _m_at(pos); if(str.size() > attr_max_.size) { attr_max_.size = str.size(); attr_max_.line = pos; } } void _m_scan_for_max() { attr_max_.reset(); for (std::size_t i = 0; i < text_cont_.size(); ++i) _m_make_max(i); } void _m_emit_first_change() const { if (evt_agent_) evt_agent_->first_change(); } void _m_saved(const path_type& filename) const { if((filename_ != filename) || changed_) { filename_ = filename; _m_emit_first_change(); } changed_ = false; } private: std::deque text_cont_; textbase_event_agent_interface* evt_agent_{ nullptr }; mutable bool changed_{ false }; mutable bool edited_{ false }; mutable path_type filename_; ///< The saved filename const string_type nullstr_; struct attr_max { std::size_t line; ///< The line number of max line std::size_t size; ///< The number of characters in max line void reset() { line = 0; size = 0; } }attr_max_; }; }//end namespace detail }//end namespace widgets }//end namespace nana #include #endif