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/editor.cpp b/apps/opencs/editor.cpp index 082fa376c..e05ebcdeb 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -1,7 +1,7 @@ #include "editor.hpp" -#include +#include #include "model/doc/document.hpp" #include "model/world/data.hpp" diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index aa315804b..7f6f9302e 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include class Application : public QApplication 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/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index 8a1595a30..6c31fddf3 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -257,7 +257,7 @@ namespace CSMWorld int mIndex; UseValueColumn (int index) - : Column (Columns::ColumnId_UseValue1 + index - 1, ColumnBase::Display_Float), + : Column (Columns::ColumnId_UseValue1 + index, ColumnBase::Display_Float), mIndex (index) {} @@ -339,7 +339,7 @@ namespace CSMWorld int mIndex; AttributesColumn (int index) - : Column (Columns::ColumnId_Attribute1 + index - 1, ColumnBase::Display_Attribute), + : Column (Columns::ColumnId_Attribute1 + index, ColumnBase::Display_Attribute), mIndex (index) {} @@ -372,7 +372,7 @@ namespace CSMWorld SkillsColumn (int index, bool typePrefix = false, bool major = false) : Column ((typePrefix ? ( major ? Columns::ColumnId_MajorSkill1 : Columns::ColumnId_MinorSkill1) : - Columns::ColumnId_Skill1) + index - 1, ColumnBase::Display_String), + Columns::ColumnId_Skill1) + index, ColumnBase::Display_String), mIndex (index), mMajor (major) {} diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index 2198d1b0c..f6eb8fe34 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -166,10 +166,11 @@ namespace CSMWorld { ColumnId_MinorSkill5, "Minor Skill 5" }, { ColumnId_Skill1, "Skill 1" }, - { ColumnId_Skill1, "Skill 2" }, - { ColumnId_Skill1, "Skill 3" }, - { ColumnId_Skill1, "Skill 4" }, - { ColumnId_Skill1, "Skill 5" }, + { ColumnId_Skill2, "Skill 2" }, + { ColumnId_Skill3, "Skill 3" }, + { ColumnId_Skill4, "Skill 4" }, + { ColumnId_Skill5, "Skill 5" }, + { ColumnId_Skill6, "Skill 6" }, { -1, 0 } // end marker }; diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index adde80dc9..28da60e93 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -171,7 +171,8 @@ namespace CSMWorld ColumnId_Skill2 = 0x50001, ColumnId_Skill3 = 0x50002, ColumnId_Skill4 = 0x50003, - ColumnId_Skill5 = 0x50004 + ColumnId_Skill5 = 0x50004, + ColumnId_Skill6 = 0x50005 }; std::string getName (ColumnId column); 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/tablebottombox.cpp b/apps/opencs/view/world/tablebottombox.cpp index 6cf21a132..3edf9af31 100644 --- a/apps/opencs/view/world/tablebottombox.cpp +++ b/apps/opencs/view/world/tablebottombox.cpp @@ -63,12 +63,15 @@ CSVWorld::TableBottomBox::TableBottomBox (const CreatorFactoryBase& creatorFacto mCreator = creatorFactory.makeCreator (data, undoStack, id); - mLayout->addWidget (mCreator); + if (mCreator) + { + mLayout->addWidget (mCreator); - connect (mCreator, SIGNAL (done()), this, SLOT (createRequestDone())); + connect (mCreator, SIGNAL (done()), this, SLOT (createRequestDone())); - connect (mCreator, SIGNAL (requestFocus (const std::string&)), - this, SIGNAL (requestFocus (const std::string&))); + connect (mCreator, SIGNAL (requestFocus (const std::string&)), + this, SIGNAL (requestFocus (const std::string&))); + } } void CSVWorld::TableBottomBox::setEditLock (bool locked) 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/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 147410c15..5903f352d 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -220,9 +220,10 @@ namespace MWBase virtual MWWorld::Ptr getFacedObject() = 0; ///< Return pointer to the object the player is looking at, if it is within activation range - /// Returns a pointer to the object the provided object is facing (if within the - /// specified distance). This will attempt to use the "Bip01 Head" node as a basis. - virtual MWWorld::Ptr getFacedObject(const MWWorld::Ptr &ptr, float distance) = 0; + /// Returns a pointer to the object the provided object would hit (if within the + /// specified distance), and the point where the hit occurs. This will attempt to + /// use the "Head" node as a basis. + virtual std::pair getHitContact(const MWWorld::Ptr &ptr, float distance) = 0; virtual void adjustPosition (const MWWorld::Ptr& ptr) = 0; ///< Adjust position after load to be on ground. Must be called after model load. diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index d9c1da77b..073d1b1b9 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -332,7 +332,8 @@ namespace MWClass float dist = 100.0f * (!weapon.isEmpty() ? weapon.get()->mBase->mData.mReach : gmst.find("fHandToHandReach")->getFloat()); - MWWorld::Ptr victim = world->getFacedObject(ptr, dist); + // TODO: Use second to work out the hit angle and where to spawn the blood effect + MWWorld::Ptr victim = world->getHitContact(ptr, dist).first; if(victim.isEmpty()) // Didn't hit anything return; diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index 33ba7101e..97aa7dffe 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -107,8 +107,7 @@ namespace MWWorld } static Ogre::Vector3 move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time, - bool isSwimming, bool isFlying, float waterlevel, - OEngine::Physic::PhysicEngine *engine) + bool isFlying, float waterlevel, OEngine::Physic::PhysicEngine *engine) { const ESM::Position &refpos = ptr.getRefData().getPosition(); Ogre::Vector3 position(refpos.pos); @@ -135,11 +134,11 @@ namespace MWWorld bool isOnGround = false; Ogre::Vector3 inertia(0.0f); Ogre::Vector3 velocity; - if(isSwimming || isFlying) + if(position.z < waterlevel || isFlying) { - velocity = (Ogre::Quaternion(Ogre::Radian( -refpos.rot[2]), Ogre::Vector3::UNIT_Z)* - Ogre::Quaternion(Ogre::Radian( -refpos.rot[1]), Ogre::Vector3::UNIT_Y)* - Ogre::Quaternion(Ogre::Radian( refpos.rot[0]), Ogre::Vector3::UNIT_X)) * + velocity = (Ogre::Quaternion(Ogre::Radian(-refpos.rot[2]), Ogre::Vector3::UNIT_Z)* + Ogre::Quaternion(Ogre::Radian(-refpos.rot[1]), Ogre::Vector3::UNIT_Y)* + Ogre::Quaternion(Ogre::Radian( refpos.rot[0]), Ogre::Vector3::UNIT_X)) * movement; } else @@ -173,7 +172,7 @@ namespace MWWorld { Ogre::Vector3 nextpos = newPosition + velocity*remainingTime; - if(isSwimming && !isFlying && + if(newPosition.z < waterlevel && !isFlying && nextpos.z > waterlevel && newPosition.z <= waterlevel) { const Ogre::Vector3 down(0,0,-1); @@ -197,7 +196,7 @@ namespace MWWorld // We hit something. Try to step up onto it. if(stepMove(colobj, newPosition, velocity, remainingTime, engine)) - isOnGround = !(isSwimming || isFlying); // Only on the ground if there's gravity + isOnGround = !(newPosition.z < waterlevel || isFlying); // Only on the ground if there's gravity else { // Can't move this way, try to find another spot along the plane @@ -208,7 +207,7 @@ namespace MWWorld // Do not allow sliding upward if there is gravity. Stepping will have taken // care of that. - if(!(isSwimming || isFlying)) + if(!(newPosition.z < waterlevel || isFlying)) velocity.z = std::min(velocity.z, 0.0f); } } @@ -225,7 +224,7 @@ namespace MWWorld isOnGround = false; } - if(isOnGround || isSwimming || isFlying) + if(isOnGround || newPosition.z < waterlevel || isFlying) physicActor->setInertialForce(Ogre::Vector3(0.0f)); else { @@ -311,26 +310,32 @@ namespace MWWorld return results; } - std::pair PhysicsSystem::getFacedHandle(const Ogre::Vector3 &origin_, const Ogre::Quaternion &orient_, float queryDistance) + std::pair PhysicsSystem::getHitContact(const std::string &name, + const Ogre::Vector3 &origin, + const Ogre::Quaternion &orient, + float queryDistance) { - btVector3 origin(origin_.x, origin_.y, origin_.z); + const MWWorld::Store &store = MWBase::Environment::get().getWorld()->getStore().get(); - std::pair result = mEngine->sphereTest(queryDistance,origin); - if(result.first == "") return std::make_pair("",0); - btVector3 a = result.second - origin; - Ogre::Vector3 a_ = Ogre::Vector3(a.x(),a.y(),a.z()); - a_ = orient_.Inverse()*a_; - Ogre::Vector2 a_xy = Ogre::Vector2(a_.x,a_.y); - Ogre::Vector2 a_yz = Ogre::Vector2(a_xy.length(),a_.z); - float axy = a_xy.angleBetween(Ogre::Vector2::UNIT_Y).valueDegrees(); - float az = a_yz.angleBetween(Ogre::Vector2::UNIT_X).valueDegrees(); + btConeShape shape(Ogre::Degree(store.find("fCombatAngleXY")->getFloat()/2.0f).valueRadians(), + queryDistance); + shape.setLocalScaling(btVector3(1, 1, Ogre::Degree(store.find("fCombatAngleZ")->getFloat()/2.0f).valueRadians() / + shape.getRadius())); - float fCombatAngleXY = MWBase::Environment::get().getWorld()->getStore().get().find("fCombatAngleXY")->getFloat(); - float fCombatAngleZ = MWBase::Environment::get().getWorld()->getStore().get().find("fCombatAngleZ")->getFloat(); - if(abs(axy) < fCombatAngleXY && abs(az) < fCombatAngleZ) - return std::make_pair (result.first,result.second.length()); - else - return std::make_pair("",0); + // The shape origin is its center, so we have to move it forward by half the length. The + // real origin will be provided to getFilteredContact to find the closest. + Ogre::Vector3 center = origin + (orient * Ogre::Vector3(0.0f, queryDistance*0.5f, 0.0f)); + + btCollisionObject object; + object.setCollisionShape(&shape); + object.setWorldTransform(btTransform(btQuaternion(orient.x, orient.y, orient.z, orient.w), + btVector3(center.x, center.y, center.z))); + + std::pair result = mEngine->getFilteredContact( + name, btVector3(origin.x, origin.y, origin.z), &object); + if(!result.first) + return std::make_pair(std::string(), Ogre::Vector3(&result.second[0])); + return std::make_pair(result.first->mName, Ogre::Vector3(&result.second[0])); } @@ -584,15 +589,13 @@ namespace MWWorld for(;iter != mMovementQueue.end();iter++) { float waterlevel = -std::numeric_limits::max(); - const MWWorld::CellStore *cellstore = iter->first.getCell(); - if(cellstore->mCell->hasWater()) - waterlevel = cellstore->mCell->mWater; + const ESM::Cell *cell = iter->first.getCell()->mCell; + if(cell->hasWater()) + waterlevel = cell->mWater; - Ogre::Vector3 newpos; - newpos = MovementSolver::move(iter->first, iter->second, mTimeAccum, - world->isSwimming(iter->first), - world->isFlying(iter->first), - waterlevel, mEngine); + Ogre::Vector3 newpos = MovementSolver::move(iter->first, iter->second, mTimeAccum, + world->isFlying(iter->first), + waterlevel, mEngine); mMovementResults.push_back(std::make_pair(iter->first, newpos)); } diff --git a/apps/openmw/mwworld/physicssystem.hpp b/apps/openmw/mwworld/physicssystem.hpp index 669eb74bd..f76b4d29c 100644 --- a/apps/openmw/mwworld/physicssystem.hpp +++ b/apps/openmw/mwworld/physicssystem.hpp @@ -57,9 +57,10 @@ namespace MWWorld Ogre::Vector3 traceDown(const MWWorld::Ptr &ptr); std::pair getFacedHandle (MWWorld::World& world, float queryDistance); - std::pair getFacedHandle(const Ogre::Vector3 &origin, - const Ogre::Quaternion &orientation, - float queryDistance); + std::pair getHitContact(const std::string &name, + const Ogre::Vector3 &origin, + const Ogre::Quaternion &orientation, + float queryDistance); std::vector < std::pair > getFacedHandles (float queryDistance); std::vector < std::pair > getFacedHandles (float mouseX, float mouseY, float queryDistance); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 73fa3f620..5c08af612 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -784,7 +784,7 @@ namespace MWWorld return object; } - MWWorld::Ptr World::getFacedObject(const MWWorld::Ptr &ptr, float distance) + std::pair World::getHitContact(const MWWorld::Ptr &ptr, float distance) { const ESM::Position &posdata = ptr.getRefData().getPosition(); Ogre::Vector3 pos(posdata.pos); @@ -799,11 +799,12 @@ namespace MWWorld pos += node->_getDerivedPosition(); } - std::pair result = mPhysics->getFacedHandle(pos, rot, distance); + std::pair result = mPhysics->getHitContact(ptr.getRefData().getHandle(), + pos, rot, distance); if(result.first.empty()) - return MWWorld::Ptr(); + return std::make_pair(MWWorld::Ptr(), Ogre::Vector3(0.0f)); - return searchPtrViaHandle(result.first); + return std::make_pair(searchPtrViaHandle(result.first), result.second); } void World::deleteObject (const Ptr& ptr) diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 3c13c33c1..30ffcda40 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -252,9 +252,10 @@ namespace MWWorld virtual MWWorld::Ptr getFacedObject(); ///< Return pointer to the object the player is looking at, if it is within activation range - /// Returns a pointer to the object the provided object is facing (if within the - /// specified distance). This will attempt to use the "Bip01 Head" node as a basis. - virtual MWWorld::Ptr getFacedObject(const MWWorld::Ptr &ptr, float distance); + /// Returns a pointer to the object the provided object would hit (if within the + /// specified distance), and the point where the hit occurs. This will attempt to + /// use the "Head" node as a basis. + virtual std::pair getHitContact(const MWWorld::Ptr &ptr, float distance); virtual void deleteObject (const Ptr& ptr); 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); diff --git a/libs/openengine/bullet/physic.cpp b/libs/openengine/bullet/physic.cpp index e488d6e99..e33edda18 100644 --- a/libs/openengine/bullet/physic.cpp +++ b/libs/openengine/bullet/physic.cpp @@ -543,62 +543,64 @@ namespace Physic #endif }; - struct AabbResultCallback : public btBroadphaseAabbCallback { - std::vector hits; - //AabbResultCallback(){} - virtual bool process(const btBroadphaseProxy* proxy) { - RigidBody* collisionObject = static_cast(proxy->m_clientObject); - if(proxy->m_collisionFilterGroup == CollisionType_Actor && (collisionObject->mName != "player")) - this->hits.push_back(collisionObject); - return true; + class DeepestNotMeContactTestResultCallback : public btCollisionWorld::ContactResultCallback + { + const std::string &mFilter; + // Store the real origin, since the shape's origin is its center + btVector3 mOrigin; + + public: + const RigidBody *mObject; + btVector3 mContactPoint; + btScalar mLeastDistSqr; + + DeepestNotMeContactTestResultCallback(const std::string &filter, const btVector3 &origin) + : mFilter(filter), mOrigin(origin), mObject(0), mContactPoint(0,0,0), + mLeastDistSqr(std::numeric_limits::max()) + { } + +#if defined(BT_COLLISION_OBJECT_WRAPPER_H) + virtual btScalar addSingleResult(btManifoldPoint& cp, + const btCollisionObjectWrapper* col0Wrap,int partId0,int index0, + const btCollisionObjectWrapper* col1Wrap,int partId1,int index1) + { + const RigidBody* body = dynamic_cast(col1Wrap->m_collisionObject); + if(body && body->mName != mFilter) + { + btScalar distsqr = mOrigin.distance2(cp.getPositionWorldOnA()); + if(!mObject || distsqr < mLeastDistSqr) + { + mObject = body; + mLeastDistSqr = distsqr; + mContactPoint = cp.getPositionWorldOnA(); + } + } + + return 0.f; } +#else + virtual btScalar addSingleResult(btManifoldPoint& cp, + const btCollisionObject* col0, int partId0, int index0, + const btCollisionObject* col1, int partId1, int index1) + { + const RigidBody* body = dynamic_cast(col1); + if(body && body->mName != mFilter) + { + btScalar distsqr = mOrigin.distance2(cp.getPositionWorldOnA()); + if(!mObject || distsqr < mLeastDistSqr) + { + mObject = body; + mLeastDistSqr = distsqr; + mContactPoint = cp.getPositionWorldOnA(); + } + } + + return 0.f; + } +#endif }; - std::pair PhysicEngine::sphereTest(float radius,btVector3& pos) - { - AabbResultCallback callback; - /*btDefaultMotionState* newMotionState = - new btDefaultMotionState(btTransform(btQuaternion(0,0,0,1),pos)); - btCollisionShape * shape = new btSphereShape(radius); - btRigidBody::btRigidBodyConstructionInfo CI = btRigidBody::btRigidBodyConstructionInfo - (0,newMotionState, shape); - RigidBody* body = new RigidBody(CI,"hitDetectionShpere__"); - btTransform tr = body->getWorldTransform(); - tr.setOrigin(pos); - body->setWorldTransform(tr); - dynamicsWorld->addRigidBody(body,CollisionType_Actor,CollisionType_World|CollisionType_World); - body->setWorldTransform(tr);*/ - - btVector3 aabbMin = pos - radius*btVector3(1.0f, 1.0f, 1.0f); - btVector3 aabbMax = pos + radius*btVector3(1.0f, 1.0f, 1.0f); - - broadphase->aabbTest(aabbMin,aabbMax,callback); - for(int i=0;i (callback.hits.size()); ++i) - { - float d = (callback.hits[i]->getWorldTransform().getOrigin()-pos).length(); - if(d rayResult = this->rayTest(pos,callback.hits[i]->getWorldTransform().getOrigin()); - if(rayResult.second>d || rayResult.first == callback.hits[i]->mName) - return std::make_pair(callback.hits[i]->mName,callback.hits[i]->getWorldTransform().getOrigin()); - } - } - //ContactTestResultCallback callback; - //dynamicsWorld->contactTest(body, callback); - //dynamicsWorld->removeRigidBody(body); - //delete body; - //delete shape; - //if(callback.mResultName.empty()) return std::make_pair(std::string(""),btVector3(0,0,0)); - /*for(int i=0;i(callback.mResultName[i],callback.mResultContact[i]); - */ - return std::make_pair(std::string(""),btVector3(0,0,0)); - } - std::vector PhysicEngine::getCollisions(const std::string& name) { RigidBody* body = getRigidBody(name); @@ -607,6 +609,17 @@ namespace Physic return callback.mResult; } + + std::pair PhysicEngine::getFilteredContact(const std::string &filter, + const btVector3 &origin, + btCollisionObject *object) + { + DeepestNotMeContactTestResultCallback callback(filter, origin); + dynamicsWorld->contactTest(object, callback); + return std::make_pair(callback.mObject, callback.mContactPoint); + } + + void PhysicEngine::stepSimulation(double deltaT) { // This seems to be needed for character controller objects diff --git a/libs/openengine/bullet/physic.hpp b/libs/openengine/bullet/physic.hpp index 46dafda76..f28f95ccb 100644 --- a/libs/openengine/bullet/physic.hpp +++ b/libs/openengine/bullet/physic.hpp @@ -321,10 +321,14 @@ public: std::pair sphereCast (float radius, btVector3& from, btVector3& to); ///< @return (hit, relative distance) - std::pair sphereTest(float radius,btVector3& pos); - std::vector getCollisions(const std::string& name); + // Get the nearest object that's inside the given object, filtering out objects of the + // provided name + std::pair getFilteredContact(const std::string &filter, + const btVector3 &origin, + btCollisionObject *object); + //event list of non player object std::list NPEventList;