diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index f7b7daee4..ad7b5b19c 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -107,10 +107,18 @@ opencs_units_noqt (model/settings settingsitem ) +opencs_units_noqt (model/filter + node unarynode narynode leafnode booleannode parser andnode ornode notnode textnode valuenode + ) + opencs_hdrs_noqt (model/filter filter ) +opencs_units (view/filter + filtercreator filterbox recordfilterbox editwidget + ) + set (OPENCS_US ) diff --git a/apps/opencs/model/filter/andnode.cpp b/apps/opencs/model/filter/andnode.cpp new file mode 100644 index 000000000..dfaa56e78 --- /dev/null +++ b/apps/opencs/model/filter/andnode.cpp @@ -0,0 +1,20 @@ + +#include "andnode.hpp" + +#include + +CSMFilter::AndNode::AndNode (const std::vector >& nodes) +: NAryNode (nodes, "and") +{} + +bool CSMFilter::AndNode::test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const +{ + int size = getSize(); + + for (int i=0; i >& nodes); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + }; +} + +#endif diff --git a/apps/opencs/model/filter/booleannode.cpp b/apps/opencs/model/filter/booleannode.cpp new file mode 100644 index 000000000..267e06a64 --- /dev/null +++ b/apps/opencs/model/filter/booleannode.cpp @@ -0,0 +1,15 @@ + +#include "booleannode.hpp" + +CSMFilter::BooleanNode::BooleanNode (bool true_) : mTrue (true_) {} + +bool CSMFilter::BooleanNode::test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const +{ + return mTrue; +} + +std::string CSMFilter::BooleanNode::toString (bool numericColumns) const +{ + return mTrue ? "true" : "false"; +} \ No newline at end of file diff --git a/apps/opencs/model/filter/booleannode.hpp b/apps/opencs/model/filter/booleannode.hpp new file mode 100644 index 000000000..d19219e35 --- /dev/null +++ b/apps/opencs/model/filter/booleannode.hpp @@ -0,0 +1,29 @@ +#ifndef CSM_FILTER_BOOLEANNODE_H +#define CSM_FILTER_BOOLEANNODE_H + +#include "leafnode.hpp" + +namespace CSMFilter +{ + class BooleanNode : public LeafNode + { + bool mTrue; + + public: + + BooleanNode (bool true_); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + + virtual std::string toString (bool numericColumns) const; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. + + }; +} + +#endif \ No newline at end of file diff --git a/apps/opencs/model/filter/filter.hpp b/apps/opencs/model/filter/filter.hpp index bddc9cc2d..62170ca80 100644 --- a/apps/opencs/model/filter/filter.hpp +++ b/apps/opencs/model/filter/filter.hpp @@ -11,15 +11,14 @@ namespace CSMFilter /// \brief Wrapper for Filter record struct Filter : public ESM::Filter { - enum scope + enum Scope { - Global = 0, - Local = 1, - Session = 2, - Content = 3 + Scope_Project = 0, // per project + Scope_Session = 1, // exists only for one editing session; not saved + Scope_Content = 2 // embedded in the edited content file }; - scope mScope; + Scope mScope; }; } diff --git a/apps/opencs/model/filter/leafnode.cpp b/apps/opencs/model/filter/leafnode.cpp new file mode 100644 index 000000000..055a1747c --- /dev/null +++ b/apps/opencs/model/filter/leafnode.cpp @@ -0,0 +1,8 @@ + +#include "leafnode.hpp" + +std::vector CSMFilter::LeafNode::getReferencedColumns() const +{ + return std::vector(); +} + diff --git a/apps/opencs/model/filter/leafnode.hpp b/apps/opencs/model/filter/leafnode.hpp new file mode 100644 index 000000000..2f3d91070 --- /dev/null +++ b/apps/opencs/model/filter/leafnode.hpp @@ -0,0 +1,20 @@ +#ifndef CSM_FILTER_LEAFNODE_H +#define CSM_FILTER_LEAFNODE_H + +#include + +#include "node.hpp" + +namespace CSMFilter +{ + class LeafNode : public Node + { + public: + + virtual std::vector getReferencedColumns() const; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + }; +} + +#endif diff --git a/apps/opencs/model/filter/narynode.cpp b/apps/opencs/model/filter/narynode.cpp new file mode 100644 index 000000000..98f706c87 --- /dev/null +++ b/apps/opencs/model/filter/narynode.cpp @@ -0,0 +1,60 @@ + +#include "narynode.hpp" + +#include + +CSMFilter::NAryNode::NAryNode (const std::vector >& nodes, + const std::string& name) +: mNodes (nodes), mName (name) +{} + +int CSMFilter::NAryNode::getSize() const +{ + return mNodes.size(); +} + +const CSMFilter::Node& CSMFilter::NAryNode::operator[] (int index) const +{ + return *mNodes.at (index); +} + +std::vector CSMFilter::NAryNode::getReferencedColumns() const +{ + std::vector columns; + + for (std::vector >::const_iterator iter (mNodes.begin()); + iter!=mNodes.end(); ++iter) + { + std::vector columns2 = (*iter)->getReferencedColumns(); + + columns.insert (columns.end(), columns2.begin(), columns2.end()); + } + + return columns; +} + +std::string CSMFilter::NAryNode::toString (bool numericColumns) const +{ + std::ostringstream stream; + + stream << mName << " ("; + + bool first = true; + int size = getSize(); + + for (int i=0; i +#include + +#include + +#include "node.hpp" + +namespace CSMFilter +{ + class NAryNode : public Node + { + std::vector > mNodes; + std::string mName; + + public: + + NAryNode (const std::vector >& nodes, const std::string& name); + + int getSize() const; + + const Node& operator[] (int index) const; + + virtual std::vector getReferencedColumns() const; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + + virtual std::string toString (bool numericColumns) const; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. + }; +} + +#endif diff --git a/apps/opencs/model/filter/node.cpp b/apps/opencs/model/filter/node.cpp new file mode 100644 index 000000000..276861cdc --- /dev/null +++ b/apps/opencs/model/filter/node.cpp @@ -0,0 +1,6 @@ + +#include "node.hpp" + +CSMFilter::Node::Node() {} + +CSMFilter::Node::~Node() {} \ No newline at end of file diff --git a/apps/opencs/model/filter/node.hpp b/apps/opencs/model/filter/node.hpp new file mode 100644 index 000000000..ef18353a4 --- /dev/null +++ b/apps/opencs/model/filter/node.hpp @@ -0,0 +1,53 @@ +#ifndef CSM_FILTER_NODE_H +#define CSM_FILTER_NODE_H + +#include +#include +#include + +#include + +#include + +namespace CSMWorld +{ + class IdTable; +} + +namespace CSMFilter +{ + /// \brief Root class for the filter node hierarchy + /// + /// \note When the function documentation for this class mentions "this node", this should be + /// interpreted as "the node and all its children". + class Node + { + // not implemented + Node (const Node&); + Node& operator= (const Node&); + + public: + + Node(); + + virtual ~Node(); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const = 0; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + + virtual std::vector getReferencedColumns() const = 0; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + + virtual std::string toString (bool numericColumns) const = 0; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. + }; +} + +Q_DECLARE_METATYPE (boost::shared_ptr) + +#endif diff --git a/apps/opencs/model/filter/notnode.cpp b/apps/opencs/model/filter/notnode.cpp new file mode 100644 index 000000000..1b22ea7a6 --- /dev/null +++ b/apps/opencs/model/filter/notnode.cpp @@ -0,0 +1,10 @@ + +#include "notnode.hpp" + +CSMFilter::NotNode::NotNode (boost::shared_ptr child) : UnaryNode (child, "not") {} + +bool CSMFilter::NotNode::test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const +{ + return !getChild().test (table, row, columns); +} \ No newline at end of file diff --git a/apps/opencs/model/filter/notnode.hpp b/apps/opencs/model/filter/notnode.hpp new file mode 100644 index 000000000..b9e80b8c6 --- /dev/null +++ b/apps/opencs/model/filter/notnode.hpp @@ -0,0 +1,21 @@ +#ifndef CSM_FILTER_NOTNODE_H +#define CSM_FILTER_NOTNODE_H + +#include "unarynode.hpp" + +namespace CSMFilter +{ + class NotNode : public UnaryNode + { + public: + + NotNode (boost::shared_ptr child); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + }; +} + +#endif diff --git a/apps/opencs/model/filter/ornode.cpp b/apps/opencs/model/filter/ornode.cpp new file mode 100644 index 000000000..4fc34e1d5 --- /dev/null +++ b/apps/opencs/model/filter/ornode.cpp @@ -0,0 +1,20 @@ + +#include "ornode.hpp" + +#include + +CSMFilter::OrNode::OrNode (const std::vector >& nodes) +: NAryNode (nodes, "or") +{} + +bool CSMFilter::OrNode::test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const +{ + int size = getSize(); + + for (int i=0; i >& nodes); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + }; +} + +#endif diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp new file mode 100644 index 000000000..5e580b6e1 --- /dev/null +++ b/apps/opencs/model/filter/parser.cpp @@ -0,0 +1,544 @@ + +#include "parser.hpp" + +#include +#include +#include + +#include + +#include "../world/columns.hpp" + +#include "booleannode.hpp" +#include "ornode.hpp" +#include "andnode.hpp" +#include "notnode.hpp" +#include "textnode.hpp" +#include "valuenode.hpp" + +namespace CSMFilter +{ + struct Token + { + enum Type + { + Type_EOS, + Type_None, + Type_String, + Type_Number, + Type_Open, + Type_Close, + Type_OpenSquare, + Type_CloseSquare, + Type_Comma, + Type_Keyword_True, ///< \attention Keyword enums must be arranged continuously. + Type_Keyword_False, + Type_Keyword_And, + Type_Keyword_Or, + Type_Keyword_Not, + Type_Keyword_Text, + Type_Keyword_Value + }; + + Type mType; + std::string mString; + double mNumber; + + Token (Type type); + + Token (const std::string& string); + + Token (double number); + + operator bool() const; + }; + + Token::Token (Type type) : mType (type) {} + + Token::Token (const std::string& string) : mType (Type_String), mString (string) {} + + Token::Token (double number) : mType (Type_Number), mNumber (number) {} + + Token::operator bool() const + { + return mType!=Type_None; + } + + bool operator== (const Token& left, const Token& right) + { + if (left.mType!=right.mType) + return false; + + switch (left.mType) + { + case Token::Type_String: return left.mString==right.mString; + case Token::Type_Number: return left.mNumber==right.mNumber; + + default: return true; + } + } +} + +CSMFilter::Token CSMFilter::Parser::getStringToken() +{ + std::string string; + + int size = static_cast (mInput.size()); + + for (; mIndex1) + { + ++mIndex; + break; + } + }; + + if (!string.empty()) + { + if (string[0]=='"' && (string[string.size()-1]!='"' || string.size()<2) ) + { + error(); + return Token (Token::Type_None); + } + + if (string[0]!='"' && string[string.size()-1]=='"') + { + error(); + return Token (Token::Type_None); + } + + if (string[0]=='"') + string = string.substr (1, string.size()-2); + } + + return checkKeywords (string); +} + +CSMFilter::Token CSMFilter::Parser::getNumberToken() +{ + std::string string; + + int size = static_cast (mInput.size()); + + bool hasDecimalPoint = false; + bool hasDigit = false; + + for (; mIndex> value; + + return value; +} + +CSMFilter::Token CSMFilter::Parser::checkKeywords (const Token& token) +{ + static const char *sKeywords[] = + { + "true", "false", + "and", "or", "not", + "text", "value", + 0 + }; + + std::string string = Misc::StringUtils::lowerCase (token.mString); + + for (int i=0; sKeywords[i]; ++i) + if (sKeywords[i]==string) + return Token (static_cast (i+Token::Type_Keyword_True)); + + return token; +} + +CSMFilter::Token CSMFilter::Parser::getNextToken() +{ + int size = static_cast (mInput.size()); + + char c = 0; + + for (; mIndex=size) + return Token (Token::Type_EOS); + + switch (c) + { + case '(': ++mIndex; return Token (Token::Type_Open); + case ')': ++mIndex; return Token (Token::Type_Close); + case '[': ++mIndex; return Token (Token::Type_OpenSquare); + case ']': ++mIndex; return Token (Token::Type_CloseSquare); + case ',': ++mIndex; return Token (Token::Type_Comma); + } + + if (c=='"' || c=='_' || std::isalpha (c)) + return getStringToken(); + + if (c=='-' || c=='.' || std::isdigit (c)) + return getNumberToken(); + + error(); + return Token (Token::Type_None); +} + +boost::shared_ptr CSMFilter::Parser::parseImp (bool allowEmpty) +{ + if (Token token = getNextToken()) + { + switch (token.mType) + { + case Token::Type_Keyword_True: + + return boost::shared_ptr (new BooleanNode (true)); + + case Token::Type_Keyword_False: + + return boost::shared_ptr (new BooleanNode (false)); + + case Token::Type_Keyword_And: + case Token::Type_Keyword_Or: + + return parseNAry (token); + + case Token::Type_Keyword_Not: + { + boost::shared_ptr node = parseImp(); + + if (mError) + return boost::shared_ptr(); + + return boost::shared_ptr (new NotNode (node)); + } + + case Token::Type_Keyword_Text: + + return parseText(); + + case Token::Type_Keyword_Value: + + return parseValue(); + + case Token::Type_EOS: + + if (!allowEmpty) + error(); + + return boost::shared_ptr(); + + default: + + error(); + } + } + + return boost::shared_ptr(); +} + +boost::shared_ptr CSMFilter::Parser::parseNAry (const Token& keyword) +{ + std::vector > nodes; + + Token token = getNextToken(); + + if (token.mType!=Token::Type_Open) + { + error(); + return boost::shared_ptr(); + } + + for (;;) + { + boost::shared_ptr node = parseImp(); + + if (mError) + return boost::shared_ptr(); + + nodes.push_back (node); + + Token token = getNextToken(); + + if (!token || (token.mType!=Token::Type_Close && token.mType!=Token::Type_Comma)) + { + error(); + return boost::shared_ptr(); + } + + if (token.mType==Token::Type_Close) + break; + } + + if (nodes.empty()) + { + error(); + return boost::shared_ptr(); + } + + switch (keyword.mType) + { + case Token::Type_Keyword_And: return boost::shared_ptr (new AndNode (nodes)); + case Token::Type_Keyword_Or: return boost::shared_ptr (new OrNode (nodes)); + default: error(); return boost::shared_ptr(); + } +} + +boost::shared_ptr CSMFilter::Parser::parseText() +{ + Token token = getNextToken(); + + if (token.mType!=Token::Type_Open) + { + error(); + return boost::shared_ptr(); + } + + token = getNextToken(); + + if (!token) + return boost::shared_ptr(); + + // parse column ID + int columnId = -1; + + if (token.mType==Token::Type_Number) + { + if (static_cast (token.mNumber)==token.mNumber) + columnId = static_cast (token.mNumber); + } + else if (token.mType==Token::Type_String) + { + columnId = CSMWorld::Columns::getId (token.mString); + } + + if (columnId<0) + { + error(); + return boost::shared_ptr(); + } + + token = getNextToken(); + + if (token.mType!=Token::Type_Comma) + { + error(); + return boost::shared_ptr(); + } + + // parse text pattern + token = getNextToken(); + + if (token.mType!=Token::Type_String) + { + error(); + return boost::shared_ptr(); + } + + std::string text = token.mString; + + token = getNextToken(); + + if (token.mType!=Token::Type_Close) + { + error(); + return boost::shared_ptr(); + } + + return boost::shared_ptr (new TextNode (columnId, text)); +} + +boost::shared_ptr CSMFilter::Parser::parseValue() +{ + Token token = getNextToken(); + + if (token.mType!=Token::Type_Open) + { + error(); + return boost::shared_ptr(); + } + + token = getNextToken(); + + if (!token) + return boost::shared_ptr(); + + // parse column ID + int columnId = -1; + + if (token.mType==Token::Type_Number) + { + if (static_cast (token.mNumber)==token.mNumber) + columnId = static_cast (token.mNumber); + } + else if (token.mType==Token::Type_String) + { + columnId = CSMWorld::Columns::getId (token.mString); + } + + if (columnId<0) + { + error(); + return boost::shared_ptr(); + } + + token = getNextToken(); + + if (token.mType!=Token::Type_Comma) + { + error(); + return boost::shared_ptr(); + } + + // parse value + double lower = 0; + double upper = 0; + bool min = false; + bool max = false; + + token = getNextToken(); + + if (token.mType==Token::Type_Number) + { + // single value + min = max = true; + lower = upper = token.mNumber; + } + else + { + // interval + if (token.mType==Token::Type_OpenSquare) + min = true; + else if (token.mType!=Token::Type_CloseSquare && token.mType!=Token::Type_Open) + { + error(); + return boost::shared_ptr(); + } + + token = getNextToken(); + + if (token.mType!=Token::Type_Number) + { + error(); + return boost::shared_ptr(); + } + + lower = token.mNumber; + + token = getNextToken(); + + if (token.mType!=Token::Type_Comma) + { + error(); + return boost::shared_ptr(); + } + + token = getNextToken(); + + if (token.mType!=Token::Type_Number) + { + error(); + return boost::shared_ptr(); + } + + upper = token.mNumber; + + token = getNextToken(); + + if (token.mType==Token::Type_CloseSquare) + max = true; + else if (token.mType!=Token::Type_OpenSquare && token.mType!=Token::Type_Close) + { + error(); + return boost::shared_ptr(); + } + } + + token = getNextToken(); + + if (token.mType!=Token::Type_Close) + { + error(); + return boost::shared_ptr(); + } + + return boost::shared_ptr (new ValueNode (columnId, lower, upper, min, max)); +} + +void CSMFilter::Parser::error() +{ + mError = true; +} + +CSMFilter::Parser::Parser() : mIndex (0), mError (false) {} + +bool CSMFilter::Parser::parse (const std::string& filter) +{ + // reset + mFilter.reset(); + mError = false; + mInput = filter; + mIndex = 0; + + boost::shared_ptr node = parseImp (true); + + if (mError) + return false; + + if (getNextToken()!=Token (Token::Type_EOS)) + return false; + + if (node) + mFilter = node; + else + { + // Empty filter string equals to filter "true". + mFilter.reset (new BooleanNode (true)); + } + + return true; +} + +boost::shared_ptr CSMFilter::Parser::getFilter() const +{ + if (mError) + throw std::logic_error ("No filter available"); + + return mFilter; +} \ No newline at end of file diff --git a/apps/opencs/model/filter/parser.hpp b/apps/opencs/model/filter/parser.hpp new file mode 100644 index 000000000..1600992b7 --- /dev/null +++ b/apps/opencs/model/filter/parser.hpp @@ -0,0 +1,53 @@ +#ifndef CSM_FILTER_PARSER_H +#define CSM_FILTER_PARSER_H + +#include + +#include "node.hpp" + +namespace CSMFilter +{ + struct Token; + + class Parser + { + boost::shared_ptr mFilter; + std::string mInput; + int mIndex; + bool mError; + + Token getStringToken(); + + Token getNumberToken(); + + Token getNextToken(); + + Token checkKeywords (const Token& token); + ///< Turn string token into keyword token, if possible. + + boost::shared_ptr parseImp (bool allowEmpty = false); + ///< Will return a null-pointer, if there is nothing more to parse. + + boost::shared_ptr parseNAry (const Token& keyword); + + boost::shared_ptr parseText(); + + boost::shared_ptr parseValue(); + + void error(); + + public: + + Parser(); + + bool parse (const std::string& filter); + ///< Discards any previous calls to parse + /// + /// \return Success? + + boost::shared_ptr getFilter() const; + ///< Throws an exception if the last call to parse did not return true. + }; +} + +#endif diff --git a/apps/opencs/model/filter/textnode.cpp b/apps/opencs/model/filter/textnode.cpp new file mode 100644 index 000000000..9987c66d2 --- /dev/null +++ b/apps/opencs/model/filter/textnode.cpp @@ -0,0 +1,62 @@ + +#include "textnode.hpp" + +#include +#include + +#include + +#include "../world/columns.hpp" +#include "../world/idtable.hpp" + +CSMFilter::TextNode::TextNode (int columnId, const std::string& text) +: mColumnId (columnId), mText (text) +{} + +bool CSMFilter::TextNode::test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const +{ + const std::map::const_iterator iter = columns.find (mColumnId); + + if (iter==columns.end()) + throw std::logic_error ("invalid column in text node test"); + + if (iter->second==-1) + return true; + + QModelIndex index = table.index (row, iter->second); + + QVariant data = table.data (index); + + if (data.type()!=QVariant::String) + return false; + + /// \todo make pattern syntax configurable + QRegExp regExp (QString::fromUtf8 (mText.c_str()), Qt::CaseInsensitive); + + return regExp.exactMatch (data.toString()); +} + +std::vector CSMFilter::TextNode::getReferencedColumns() const +{ + return std::vector (1, mColumnId); +} + +std::string CSMFilter::TextNode::toString (bool numericColumns) const +{ + std::ostringstream stream; + + stream << "text ("; + + if (numericColumns) + stream << mColumnId; + else + stream + << "\"" + << CSMWorld::Columns::getName (static_cast (mColumnId)) + << "\""; + + stream << ", \"" << mText << "\")"; + + return stream.str(); +} \ No newline at end of file diff --git a/apps/opencs/model/filter/textnode.hpp b/apps/opencs/model/filter/textnode.hpp new file mode 100644 index 000000000..663fa7382 --- /dev/null +++ b/apps/opencs/model/filter/textnode.hpp @@ -0,0 +1,33 @@ +#ifndef CSM_FILTER_TEXTNODE_H +#define CSM_FILTER_TEXTNODE_H + +#include "leafnode.hpp" + +namespace CSMFilter +{ + class TextNode : public LeafNode + { + int mColumnId; + std::string mText; + + public: + + TextNode (int columnId, const std::string& text); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + + virtual std::vector getReferencedColumns() const; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + + virtual std::string toString (bool numericColumns) const; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. + }; +} + +#endif diff --git a/apps/opencs/model/filter/unarynode.cpp b/apps/opencs/model/filter/unarynode.cpp new file mode 100644 index 000000000..43a24b76a --- /dev/null +++ b/apps/opencs/model/filter/unarynode.cpp @@ -0,0 +1,26 @@ + +#include "unarynode.hpp" + +CSMFilter::UnaryNode::UnaryNode (boost::shared_ptr child, const std::string& name) +: mChild (child), mName (name) +{} + +const CSMFilter::Node& CSMFilter::UnaryNode::getChild() const +{ + return *mChild; +} + +CSMFilter::Node& CSMFilter::UnaryNode::getChild() +{ + return *mChild; +} + +std::vector CSMFilter::UnaryNode::getReferencedColumns() const +{ + return mChild->getReferencedColumns(); +} + +std::string CSMFilter::UnaryNode::toString (bool numericColumns) const +{ + return mName + " " + mChild->toString (numericColumns); +} \ No newline at end of file diff --git a/apps/opencs/model/filter/unarynode.hpp b/apps/opencs/model/filter/unarynode.hpp new file mode 100644 index 000000000..6bbc96092 --- /dev/null +++ b/apps/opencs/model/filter/unarynode.hpp @@ -0,0 +1,34 @@ +#ifndef CSM_FILTER_UNARYNODE_H +#define CSM_FILTER_UNARYNODE_H + +#include + +#include "node.hpp" + +namespace CSMFilter +{ + class UnaryNode : public Node + { + boost::shared_ptr mChild; + std::string mName; + + public: + + UnaryNode (boost::shared_ptr child, const std::string& name); + + const Node& getChild() const; + + Node& getChild(); + + virtual std::vector getReferencedColumns() const; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + + virtual std::string toString (bool numericColumns) const; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. + }; +} + +#endif diff --git a/apps/opencs/model/filter/valuenode.cpp b/apps/opencs/model/filter/valuenode.cpp new file mode 100644 index 000000000..f6cb20e4c --- /dev/null +++ b/apps/opencs/model/filter/valuenode.cpp @@ -0,0 +1,71 @@ + +#include "valuenode.hpp" + +#include +#include + +#include "../world/columns.hpp" +#include "../world/idtable.hpp" + +CSMFilter::ValueNode::ValueNode (int columnId, + double lower, double upper, bool min, bool max) +: mColumnId (columnId), mLower (lower), mUpper (upper), mMin (min), mMax (max) +{} + +bool CSMFilter::ValueNode::test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const +{ + const std::map::const_iterator iter = columns.find (mColumnId); + + if (iter==columns.end()) + throw std::logic_error ("invalid column in test value test"); + + if (iter->second==-1) + return true; + + QModelIndex index = table.index (row, iter->second); + + QVariant data = table.data (index); + + if (data.type()!=QVariant::Double && data.type()!=QVariant::Bool && data.type()!=QVariant::Int && + data.type()!=QVariant::UInt) + return false; + + double value = data.toDouble(); + + if (mLower==mUpper && mMin && mMax) + return value==mLower; + + return (mMin ? value>=mLower : value>mLower) && (mMax ? value<=mUpper : value CSMFilter::ValueNode::getReferencedColumns() const +{ + return std::vector (1, mColumnId); +} + +std::string CSMFilter::ValueNode::toString (bool numericColumns) const +{ + std::ostringstream stream; + + stream << "value ("; + + if (numericColumns) + stream << mColumnId; + else + stream + << "\"" + << CSMWorld::Columns::getName (static_cast (mColumnId)) + << "\""; + + stream << ", \""; + + if (mLower==mUpper && mMin && mMax) + stream << mLower; + else + stream << (mMin ? "[" : "(") << mLower << ", " << mUpper << (mMax ? "]" : ")"); + + stream << ")"; + + return stream.str(); +} \ No newline at end of file diff --git a/apps/opencs/model/filter/valuenode.hpp b/apps/opencs/model/filter/valuenode.hpp new file mode 100644 index 000000000..faaa1e2ff --- /dev/null +++ b/apps/opencs/model/filter/valuenode.hpp @@ -0,0 +1,37 @@ +#ifndef CSM_FILTER_VALUENODE_H +#define CSM_FILTER_VALUENODE_H + +#include "leafnode.hpp" + +namespace CSMFilter +{ + class ValueNode : public LeafNode + { + int mColumnId; + std::string mText; + double mLower; + double mUpper; + bool mMin; + bool mMax; + + public: + + ValueNode (int columnId, double lower, double upper, bool min, bool max); + + virtual bool test (const CSMWorld::IdTable& table, int row, + const std::map& columns) const; + ///< \return Can the specified table row pass through to filter? + /// \param columns column ID to column index mapping + + virtual std::vector getReferencedColumns() const; + ///< Return a list of the IDs of the columns referenced by this node. The column mapping + /// passed into test as columns must contain all columns listed here. + + virtual std::string toString (bool numericColumns) const; + ///< Return a string that represents this node. + /// + /// \param numericColumns Use numeric IDs instead of string to represent columns. + }; +} + +#endif diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 9c5e13562..4a5dcb38f 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -150,6 +150,7 @@ CSMWorld::Data::Data() : mRefs (mCells) mFilters.addColumn (new StringIdColumn); mFilters.addColumn (new RecordStateColumn); + mFilters.addColumn (new DescriptionColumn); addModel (new IdTable (&mGlobals), UniversalId::Type_Globals, UniversalId::Type_Global); addModel (new IdTable (&mGmsts), UniversalId::Type_Gmsts, UniversalId::Type_Gmst); diff --git a/apps/opencs/model/world/idtableproxymodel.cpp b/apps/opencs/model/world/idtableproxymodel.cpp index e99e1575c..2b757adfe 100644 --- a/apps/opencs/model/world/idtableproxymodel.cpp +++ b/apps/opencs/model/world/idtableproxymodel.cpp @@ -1,8 +1,36 @@ #include "idtableproxymodel.hpp" +#include + #include "idtable.hpp" +void CSMWorld::IdTableProxyModel::updateColumnMap() +{ + mColumnMap.clear(); + + if (mFilter) + { + std::vector columns = mFilter->getReferencedColumns(); + + const IdTable& table = dynamic_cast (*sourceModel()); + + for (std::vector::const_iterator iter (columns.begin()); iter!=columns.end(); ++iter) + mColumnMap.insert (std::make_pair (*iter, + table.searchColumnIndex (static_cast (*iter)))); + } +} + +bool CSMWorld::IdTableProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) + const +{ + if (!mFilter) + return true; + + return mFilter->test ( + dynamic_cast (*sourceModel()), sourceRow, mColumnMap); +} + CSMWorld::IdTableProxyModel::IdTableProxyModel (QObject *parent) : QSortFilterProxyModel (parent) {} @@ -10,4 +38,11 @@ CSMWorld::IdTableProxyModel::IdTableProxyModel (QObject *parent) QModelIndex CSMWorld::IdTableProxyModel::getModelIndex (const std::string& id, int column) const { return mapFromSource (dynamic_cast (*sourceModel()).getModelIndex (id, column)); +} + +void CSMWorld::IdTableProxyModel::setFilter (const boost::shared_ptr& filter) +{ + mFilter = filter; + updateColumnMap(); + invalidateFilter(); } \ No newline at end of file diff --git a/apps/opencs/model/world/idtableproxymodel.hpp b/apps/opencs/model/world/idtableproxymodel.hpp index 200b99fe2..b63dccd5e 100644 --- a/apps/opencs/model/world/idtableproxymodel.hpp +++ b/apps/opencs/model/world/idtableproxymodel.hpp @@ -1,9 +1,15 @@ #ifndef CSM_WOLRD_IDTABLEPROXYMODEL_H #define CSM_WOLRD_IDTABLEPROXYMODEL_H +#include + +#include + +#include + #include -#include +#include "../filter/node.hpp" namespace CSMWorld { @@ -11,11 +17,22 @@ namespace CSMWorld { Q_OBJECT + boost::shared_ptr mFilter; + std::map mColumnMap; // column ID, column index in this model (or -1) + + private: + + void updateColumnMap(); + + bool filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const; + public: IdTableProxyModel (QObject *parent = 0); virtual QModelIndex getModelIndex (const std::string& id, int column) const; + + void setFilter (const boost::shared_ptr& filter); }; } diff --git a/apps/opencs/view/filter/editwidget.cpp b/apps/opencs/view/filter/editwidget.cpp new file mode 100644 index 000000000..b691a5e16 --- /dev/null +++ b/apps/opencs/view/filter/editwidget.cpp @@ -0,0 +1,26 @@ + +#include "editwidget.hpp" + +CSVFilter::EditWidget::EditWidget (QWidget *parent) +: QLineEdit (parent) +{ + mPalette = palette(); + connect (this, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); +} + +void CSVFilter::EditWidget::textChanged (const QString& text) +{ + if (mParser.parse (text.toUtf8().constData())) + { + setPalette (mPalette); + emit filterChanged (mParser.getFilter()); + } + else + { + QPalette palette (mPalette); + palette.setColor (QPalette::Text, Qt::red); + setPalette (palette); + + /// \todo improve error reporting; mark only the faulty part + } +} \ No newline at end of file diff --git a/apps/opencs/view/filter/editwidget.hpp b/apps/opencs/view/filter/editwidget.hpp new file mode 100644 index 000000000..76b484de9 --- /dev/null +++ b/apps/opencs/view/filter/editwidget.hpp @@ -0,0 +1,35 @@ +#ifndef CSV_FILTER_EDITWIDGET_H +#define CSV_FILTER_EDITWIDGET_H + +#include + +#include +#include + +#include "../../model/filter/parser.hpp" +#include "../../model/filter/node.hpp" + +namespace CSVFilter +{ + class EditWidget : public QLineEdit + { + Q_OBJECT + + CSMFilter::Parser mParser; + QPalette mPalette; + + public: + + EditWidget (QWidget *parent = 0); + + signals: + + void filterChanged (boost::shared_ptr filter); + + private slots: + + void textChanged (const QString& text); + }; +} + +#endif diff --git a/apps/opencs/view/filter/filterbox.cpp b/apps/opencs/view/filter/filterbox.cpp new file mode 100644 index 000000000..495abf871 --- /dev/null +++ b/apps/opencs/view/filter/filterbox.cpp @@ -0,0 +1,24 @@ + +#include "filterbox.hpp" + +#include + +#include "recordfilterbox.hpp" + +CSVFilter::FilterBox::FilterBox (QWidget *parent) +: QWidget (parent) +{ + QHBoxLayout *layout = new QHBoxLayout (this); + + layout->setContentsMargins (0, 0, 0, 0); + + RecordFilterBox *recordFilterBox = new RecordFilterBox (this); + + layout->addWidget (recordFilterBox); + + setLayout (layout); + + connect (recordFilterBox, + SIGNAL (filterChanged (boost::shared_ptr)), + this, SIGNAL (recordFilterChanged (boost::shared_ptr))); +} \ No newline at end of file diff --git a/apps/opencs/view/filter/filterbox.hpp b/apps/opencs/view/filter/filterbox.hpp new file mode 100644 index 000000000..d3806876d --- /dev/null +++ b/apps/opencs/view/filter/filterbox.hpp @@ -0,0 +1,25 @@ +#ifndef CSV_FILTER_FILTERBOX_H +#define CSV_FILTER_FILTERBOX_H + +#include + +#include "../../model/filter/node.hpp" + +namespace CSVFilter +{ + class FilterBox : public QWidget + { + Q_OBJECT + + public: + + FilterBox (QWidget *parent = 0); + + signals: + + void recordFilterChanged (boost::shared_ptr filter); + }; + +} + +#endif diff --git a/apps/opencs/view/filter/filtercreator.cpp b/apps/opencs/view/filter/filtercreator.cpp new file mode 100644 index 000000000..47925ea57 --- /dev/null +++ b/apps/opencs/view/filter/filtercreator.cpp @@ -0,0 +1,63 @@ + +#include "filtercreator.hpp" + +#include +#include + +#include "../../model/filter/filter.hpp" + +std::string CSVFilter::FilterCreator::getNamespace() const +{ + switch (mScope->currentIndex()) + { + case CSMFilter::Filter::Scope_Project: return "project::"; + case CSMFilter::Filter::Scope_Session: return "session::"; + } + + return ""; +} + +void CSVFilter::FilterCreator::update() +{ + mNamespace->setText (QString::fromUtf8 (getNamespace().c_str())); + GenericCreator::update(); +} + +std::string CSVFilter::FilterCreator::getId() const +{ + return getNamespace() + GenericCreator::getId(); +} + +CSVFilter::FilterCreator::FilterCreator (CSMWorld::Data& data, QUndoStack& undoStack, + const CSMWorld::UniversalId& id) +: GenericCreator (data, undoStack, id) +{ + mNamespace = new QLabel ("::", this); + insertAtBeginning (mNamespace, false); + + mScope = new QComboBox (this); + + mScope->addItem ("Project"); + mScope->addItem ("Session"); + /// \ŧodo re-enable for OpenMW 1.1 + // mScope->addItem ("Content"); + + connect (mScope, SIGNAL (currentIndexChanged (int)), this, SLOT (setScope (int))); + + insertAtBeginning (mScope, false); + + QLabel *label = new QLabel ("Scope", this); + insertAtBeginning (label, false); + + mScope->setCurrentIndex (1); +} + +void CSVFilter::FilterCreator::reset() +{ + GenericCreator::reset(); +} + +void CSVFilter::FilterCreator::setScope (int index) +{ + update(); +} diff --git a/apps/opencs/view/filter/filtercreator.hpp b/apps/opencs/view/filter/filtercreator.hpp new file mode 100644 index 000000000..82d38d22c --- /dev/null +++ b/apps/opencs/view/filter/filtercreator.hpp @@ -0,0 +1,41 @@ +#ifndef CSV_FILTER_FILTERCREATOR_H +#define CSV_FILTER_FILTERCREATOR_H + +class QComboBox; +class QLabel; + +#include "../world/genericcreator.hpp" + +namespace CSVFilter +{ + class FilterCreator : public CSVWorld::GenericCreator + { + Q_OBJECT + + QComboBox *mScope; + QLabel *mNamespace; + + private: + + std::string getNamespace() const; + + protected: + + void update(); + + virtual std::string getId() const; + + public: + + FilterCreator (CSMWorld::Data& data, QUndoStack& undoStack, + const CSMWorld::UniversalId& id); + + virtual void reset(); + + private slots: + + void setScope (int index); + }; +} + +#endif diff --git a/apps/opencs/view/filter/recordfilterbox.cpp b/apps/opencs/view/filter/recordfilterbox.cpp new file mode 100644 index 000000000..3b5f73f47 --- /dev/null +++ b/apps/opencs/view/filter/recordfilterbox.cpp @@ -0,0 +1,27 @@ + +#include "recordfilterbox.hpp" + +#include +#include + +#include "editwidget.hpp" + +CSVFilter::RecordFilterBox::RecordFilterBox (QWidget *parent) +: QWidget (parent) +{ + QHBoxLayout *layout = new QHBoxLayout (this); + + layout->setContentsMargins (0, 0, 0, 0); + + layout->addWidget (new QLabel ("Record Filter", this)); + + EditWidget *editWidget = new EditWidget (this); + + layout->addWidget (editWidget); + + setLayout (layout); + + connect ( + editWidget, SIGNAL (filterChanged (boost::shared_ptr)), + this, SIGNAL (filterChanged (boost::shared_ptr))); +} diff --git a/apps/opencs/view/filter/recordfilterbox.hpp b/apps/opencs/view/filter/recordfilterbox.hpp new file mode 100644 index 000000000..64c1848a8 --- /dev/null +++ b/apps/opencs/view/filter/recordfilterbox.hpp @@ -0,0 +1,29 @@ +#ifndef CSV_FILTER_RECORDFILTERBOX_H +#define CSV_FILTER_RECORDFILTERBOX_H + +#include + +#include + +#include + +#include "../../model/filter/node.hpp" + +namespace CSVFilter +{ + class RecordFilterBox : public QWidget + { + Q_OBJECT + + public: + + RecordFilterBox (QWidget *parent = 0); + + signals: + + void filterChanged (boost::shared_ptr filter); + }; + +} + +#endif \ No newline at end of file diff --git a/apps/opencs/view/world/subviews.cpp b/apps/opencs/view/world/subviews.cpp index 2ca711a59..d22e07d89 100644 --- a/apps/opencs/view/world/subviews.cpp +++ b/apps/opencs/view/world/subviews.cpp @@ -3,6 +3,8 @@ #include "../doc/subviewfactoryimp.hpp" +#include "../filter/filtercreator.hpp" + #include "tablesubview.hpp" #include "dialoguesubview.hpp" #include "scriptsubview.hpp" @@ -33,7 +35,6 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) CSMWorld::UniversalId::Type_Regions, CSMWorld::UniversalId::Type_Birthsigns, CSMWorld::UniversalId::Type_Spells, - CSMWorld::UniversalId::Type_Filters, CSMWorld::UniversalId::Type_None // end marker }; @@ -56,4 +57,9 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) // Other stuff (combined record tables) manager.add (CSMWorld::UniversalId::Type_RegionMap, new CSVDoc::SubViewFactory); + + manager.add (CSMWorld::UniversalId::Type_Filters, + new CSVDoc::SubViewFactoryWithCreator >); + } \ No newline at end of file diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index 4ae25d10b..72e78c738 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -257,9 +257,9 @@ void CSVWorld::Table::tableSizeUpdate() int deleted = 0; int modified = 0; - if (mModel->columnCount()>0) + if (mProxyModel->columnCount()>0) { - int rows = mModel->rowCount(); + int rows = mProxyModel->rowCount(); for (int i=0; i filter) +{ + mProxyModel->setFilter (filter); } \ No newline at end of file diff --git a/apps/opencs/view/world/table.hpp b/apps/opencs/view/world/table.hpp index 0c24e7b54..d93109056 100644 --- a/apps/opencs/view/world/table.hpp +++ b/apps/opencs/view/world/table.hpp @@ -6,6 +6,8 @@ #include +#include "../../model/filter/node.hpp" + class QUndoStack; class QAction; @@ -85,6 +87,7 @@ namespace CSVWorld void requestFocus (const std::string& id); + void recordFilterChanged (boost::shared_ptr filter); }; } diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index af3d186e8..a43ae2dac 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -5,6 +5,8 @@ #include "../../model/doc/document.hpp" +#include "../filter/filterbox.hpp" + #include "table.hpp" #include "tablebottombox.hpp" #include "creator.hpp" @@ -23,6 +25,10 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::D layout->insertWidget (0, mTable = new Table (id, document.getData(), document.getUndoStack(), mBottom->canCreateAndDelete()), 2); + CSVFilter::FilterBox *filterBox = new CSVFilter::FilterBox (this); + + layout->insertWidget (0, filterBox); + QWidget *widget = new QWidget; widget->setLayout (layout); @@ -44,6 +50,10 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::D connect (mBottom, SIGNAL (requestFocus (const std::string&)), mTable, SLOT (requestFocus (const std::string&))); + + connect (filterBox, + SIGNAL (recordFilterChanged (boost::shared_ptr)), + mTable, SLOT (recordFilterChanged (boost::shared_ptr))); } void CSVWorld::TableSubView::setEditLock (bool locked) diff --git a/components/esm/filter.cpp b/components/esm/filter.cpp index 205332f6b..7d4851a5f 100644 --- a/components/esm/filter.cpp +++ b/components/esm/filter.cpp @@ -7,14 +7,17 @@ void ESM::Filter::load (ESMReader& esm) { mFilter = esm.getHNString ("FILT"); + mDescription = esm.getHNString ("DESC"); } void ESM::Filter::save (ESMWriter& esm) { esm.writeHNCString ("FILT", mFilter); + esm.writeHNCString ("DESC", mDescription); } void ESM::Filter::blank() { mFilter.clear(); + mDescription.clear(); } diff --git a/components/esm/filter.hpp b/components/esm/filter.hpp index 2dde92fb0..0fd564361 100644 --- a/components/esm/filter.hpp +++ b/components/esm/filter.hpp @@ -12,6 +12,8 @@ namespace ESM { std::string mId; + std::string mDescription; + std::string mFilter; void load (ESMReader& esm);