Show relevant ignore pattern for items in file browser
This commit is contained in:
parent
fc47cea9e5
commit
3ffe62b289
|
@ -19,6 +19,7 @@ set(HEADER_FILES
|
||||||
syncthingconnectionsettings.h
|
syncthingconnectionsettings.h
|
||||||
syncthingnotifier.h
|
syncthingnotifier.h
|
||||||
syncthingconfig.h
|
syncthingconfig.h
|
||||||
|
syncthingignorepattern.h
|
||||||
syncthingprocess.h
|
syncthingprocess.h
|
||||||
syncthingservice.h
|
syncthingservice.h
|
||||||
qstringhash.h
|
qstringhash.h
|
||||||
|
@ -31,6 +32,7 @@ set(SRC_FILES
|
||||||
syncthingconnectionsettings.cpp
|
syncthingconnectionsettings.cpp
|
||||||
syncthingnotifier.cpp
|
syncthingnotifier.cpp
|
||||||
syncthingconfig.cpp
|
syncthingconfig.cpp
|
||||||
|
syncthingignorepattern.cpp
|
||||||
syncthingprocess.cpp
|
syncthingprocess.cpp
|
||||||
syncthingservice.cpp
|
syncthingservice.cpp
|
||||||
utils.cpp)
|
utils.cpp)
|
||||||
|
|
|
@ -59,6 +59,11 @@ enum class SyncthingItemType {
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem {
|
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem {
|
||||||
|
/// \brief The matching ignore pattern was not initialized for this item.
|
||||||
|
static constexpr auto ignorePatternNotInitialized = std::numeric_limits<std::size_t>::max();
|
||||||
|
/// \brief The item did not match any of the current ignore patterns.
|
||||||
|
static constexpr auto ignorePatternNoMatch = ignorePatternNotInitialized - 1;
|
||||||
|
|
||||||
/// \brief The name of the filesystem item or error/loading message in case of those item types.
|
/// \brief The name of the filesystem item or error/loading message in case of those item types.
|
||||||
QString name;
|
QString name;
|
||||||
/// \brief The modification time. Only populated with a meaningful value for files and directories.
|
/// \brief The modification time. Only populated with a meaningful value for files and directories.
|
||||||
|
@ -75,6 +80,9 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem {
|
||||||
QString path;
|
QString path;
|
||||||
/// \brief The index of the item within its parent.
|
/// \brief The index of the item within its parent.
|
||||||
std::size_t index = std::size_t();
|
std::size_t index = std::size_t();
|
||||||
|
/// \brief The index of the ignore pattern (in the current list of ignore patterns) this item matches.
|
||||||
|
/// \remarks Not populated by default.
|
||||||
|
std::size_t ignorePattern = ignorePatternNotInitialized;
|
||||||
/// \brief The level of nesting, does *not* include levels of the prefix.
|
/// \brief The level of nesting, does *not* include levels of the prefix.
|
||||||
int level = 0;
|
int level = 0;
|
||||||
/// \brief Whether children are populated (depends on the requested level).
|
/// \brief Whether children are populated (depends on the requested level).
|
||||||
|
|
|
@ -0,0 +1,408 @@
|
||||||
|
#include "./syncthingignorepattern.h"
|
||||||
|
|
||||||
|
namespace Data {
|
||||||
|
|
||||||
|
/// \cond
|
||||||
|
namespace SyncthingIgnorePatternState {
|
||||||
|
enum State {
|
||||||
|
Escaping, // passed the esacping character "\"
|
||||||
|
AppendRangeLower, // passed a "[" marking the start of a character range
|
||||||
|
AppendRangeUpper, // passed a "-" within a range marking the start of an upper-bound range character
|
||||||
|
AppendAlternative, // passed a "{" marking the start of an alternative set
|
||||||
|
MatchVerbatimly, // initial/default state
|
||||||
|
MatchRange, // passed a "]" marking the end of a character range
|
||||||
|
MatchAlternatives, // passed a "}" marking the end of an alternative set
|
||||||
|
MatchAny, // passed the "?" character that allows matching any character but the path separator
|
||||||
|
MatchManyAny, // passed the "*" character that allows matching many arbitrary characters except the path separator
|
||||||
|
MatchManyAnyIncludingDirSep, // passed a sequence of two "*" characters allowing to match also the path separator
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Asterisk {
|
||||||
|
QString::const_iterator pos;
|
||||||
|
SyncthingIgnorePatternState::State state;
|
||||||
|
bool visited = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CharacterRange {
|
||||||
|
QChar lowerBound, upperBound;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AlternativeRange {
|
||||||
|
explicit AlternativeRange(QString::const_iterator beg)
|
||||||
|
: beg(beg)
|
||||||
|
, end(beg)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
QString::const_iterator beg, end;
|
||||||
|
};
|
||||||
|
} // namespace SyncthingIgnorePatternState
|
||||||
|
/// \endcond
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Parses the specified \a pattern populating the struct's fields.
|
||||||
|
*/
|
||||||
|
SyncthingIgnorePattern::SyncthingIgnorePattern(QString &&pattern)
|
||||||
|
: pattern(std::move(pattern))
|
||||||
|
{
|
||||||
|
if (glob.startsWith(QLatin1String("//"))) {
|
||||||
|
comment = true;
|
||||||
|
ignore = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
glob = this->pattern;
|
||||||
|
for (;;) {
|
||||||
|
if (glob.startsWith(QLatin1String("!"))) {
|
||||||
|
ignore = !ignore;
|
||||||
|
glob.remove(0, 1);
|
||||||
|
} else if (glob.startsWith(QLatin1String("(?i)"))) {
|
||||||
|
caseInsensitive = true;
|
||||||
|
glob.remove(0, 4);
|
||||||
|
} else if (glob.startsWith(QLatin1String("(?d)"))) {
|
||||||
|
allowRemovalOnParentDirRemoval = true;
|
||||||
|
glob.remove(0, 4);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Moves the ignore pattern.
|
||||||
|
*/
|
||||||
|
SyncthingIgnorePattern::SyncthingIgnorePattern(SyncthingIgnorePattern &&) = default;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Destroys the ignore pattern.
|
||||||
|
*/
|
||||||
|
SyncthingIgnorePattern::~SyncthingIgnorePattern()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Matches the assigned glob against the specified \a path.
|
||||||
|
* \remarks
|
||||||
|
* - Returns always false if the pattern is flagged as comment or the glob is empty.
|
||||||
|
* - This function tries to follow rules outlined on https://docs.syncthing.net/users/ignoring.html.
|
||||||
|
* - The specified \a path is *not* supposed to start with a "/". It must always be a path relative to the root of the
|
||||||
|
* Syncthing folder it is contained by. A pattern that is only supposed to match from the root of the Syncthing folder
|
||||||
|
* is supposed to start with a "/", though.
|
||||||
|
* - This function probably doesn't work if the pattern or \a path contain a surrogate pair.
|
||||||
|
*/
|
||||||
|
bool SyncthingIgnorePattern::matches(const QString &path) const
|
||||||
|
{
|
||||||
|
if (comment || glob.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get iterators
|
||||||
|
auto globIter = glob.begin(), globEnd = glob.end();
|
||||||
|
auto pathIter = path.begin(), pathEnd = path.end();
|
||||||
|
|
||||||
|
// handle pattners starting with "/" indicating the pattern must match from the root (see last remark in docstring)
|
||||||
|
static constexpr auto pathSep = QChar('/');
|
||||||
|
const auto matchFromRoot = *globIter == pathSep;
|
||||||
|
if (matchFromRoot) {
|
||||||
|
++globIter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// define variables to track the state when processing the glob pattern
|
||||||
|
using namespace SyncthingIgnorePatternState;
|
||||||
|
auto state = MatchVerbatimly;
|
||||||
|
auto escapedState = MatchVerbatimly;
|
||||||
|
auto inAsterisk = false;
|
||||||
|
asterisks.clear();
|
||||||
|
|
||||||
|
// define behavior to handle the current character in the glob pattern not matching the current pattern in the path
|
||||||
|
const auto handleMismatch = [&, this] {
|
||||||
|
// fail the match immediately if the glob pattern started with a "/" indicating it is supposed to match only from the root
|
||||||
|
if (matchFromRoot) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// deal with the mismatch by trying to match previous asterisks more greedily
|
||||||
|
while (!asterisks.empty() && asterisks.back().visited) {
|
||||||
|
// do not consider asterisks we have already visited, though (as it would lead to an endless loop)
|
||||||
|
asterisks.pop_back();
|
||||||
|
}
|
||||||
|
if (!asterisks.empty()) {
|
||||||
|
// rewind back to when we have passed the last non-visited asterisk
|
||||||
|
auto &asterisk = asterisks.back();
|
||||||
|
globIter = asterisk.pos;
|
||||||
|
inAsterisk = asterisk.visited = true;
|
||||||
|
state = asterisk.state;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// deal with the mismatch by checking the path as of the next path element
|
||||||
|
for (; pathIter != pathEnd; ++pathIter) {
|
||||||
|
// forward to the next path separator
|
||||||
|
if (*pathIter != pathSep) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// skip the path separator itself and give up when the end of the path is reached
|
||||||
|
if (++pathIter == pathEnd) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// give up when the end of the path is reached
|
||||||
|
if (pathIter == pathEnd) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// start matching the glob pattern from the beginning
|
||||||
|
globIter = glob.begin();
|
||||||
|
asterisks.clear();
|
||||||
|
inAsterisk = false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// define function to handle single match
|
||||||
|
const auto handleSingleMatch = [&, this] {
|
||||||
|
// proceed with the next characters on a match
|
||||||
|
++globIter;
|
||||||
|
inAsterisk = false;
|
||||||
|
if (!asterisks.empty()) {
|
||||||
|
asterisks.back().visited = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// define function to match single character against the current character in the path
|
||||||
|
const auto matchSingleChar
|
||||||
|
= [&, this](QChar singleChar) { return caseInsensitive ? pathIter->toCaseFolded() == singleChar.toCaseFolded() : *pathIter == singleChar; };
|
||||||
|
|
||||||
|
// try to match each character of the glob against a character in the path
|
||||||
|
match:
|
||||||
|
while (globIter != globEnd) {
|
||||||
|
// decide what to do next depending on the current glob pattern character and state transitioning the state accordingly
|
||||||
|
switch (state) {
|
||||||
|
case Escaping:
|
||||||
|
// treat every character as-is in "escaping" state
|
||||||
|
state = escapedState;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// transition state according to special meaning of the current glob pattern character
|
||||||
|
switch (globIter->unicode()) {
|
||||||
|
case '\\':
|
||||||
|
// transition into "escaping" state
|
||||||
|
escapedState = state;
|
||||||
|
state = Escaping;
|
||||||
|
break;
|
||||||
|
case '[':
|
||||||
|
state = AppendRangeLower;
|
||||||
|
characterRange.clear();
|
||||||
|
++globIter;
|
||||||
|
continue;
|
||||||
|
case ']':
|
||||||
|
switch (state) {
|
||||||
|
case AppendRangeLower:
|
||||||
|
case AppendRangeUpper:
|
||||||
|
state = MatchRange;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state = MatchVerbatimly;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '-':
|
||||||
|
switch (state) {
|
||||||
|
case AppendRangeLower:
|
||||||
|
state = AppendRangeUpper;
|
||||||
|
++globIter;
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
state = MatchVerbatimly;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '{':
|
||||||
|
switch (state) {
|
||||||
|
case AppendAlternative:
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
state = AppendAlternative;
|
||||||
|
alternatives.clear();
|
||||||
|
alternatives.emplace_back(++globIter);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
case '}':
|
||||||
|
switch (state) {
|
||||||
|
case AppendAlternative:
|
||||||
|
alternatives.back().end = globIter;
|
||||||
|
state = MatchAlternatives;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state = MatchVerbatimly;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ',':
|
||||||
|
switch (state) {
|
||||||
|
case AppendAlternative:
|
||||||
|
alternatives.back().end = globIter;
|
||||||
|
alternatives.emplace_back(++globIter);
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
state = MatchVerbatimly;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '?':
|
||||||
|
// transition into "match any" state
|
||||||
|
state = MatchAny;
|
||||||
|
break;
|
||||||
|
case '*':
|
||||||
|
// transition into one of the "match many any" state (depending on current state)
|
||||||
|
switch (state) {
|
||||||
|
case MatchManyAny:
|
||||||
|
state = MatchManyAnyIncludingDirSep;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state = MatchManyAny;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// try to match/append all other non-special characters as-is
|
||||||
|
switch (state) {
|
||||||
|
case AppendRangeLower:
|
||||||
|
case AppendRangeUpper:
|
||||||
|
case AppendAlternative:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state = MatchVerbatimly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proceed according to state
|
||||||
|
switch (state) {
|
||||||
|
case Escaping:
|
||||||
|
// proceed with the next character in the glob pattern which will be matched as-is (even if it is special)
|
||||||
|
[[fallthrough]];
|
||||||
|
case AppendAlternative:
|
||||||
|
// just move on to the next character (alternatives are populated completely in the previous switch-case)
|
||||||
|
++globIter;
|
||||||
|
break;
|
||||||
|
case AppendRangeLower:
|
||||||
|
// add the current character in the glob pattern as start of a new range
|
||||||
|
characterRange.emplace_back().lowerBound = *globIter++;
|
||||||
|
break;
|
||||||
|
case AppendRangeUpper:
|
||||||
|
// add the current character in the glob pattern as end of a new or the current range
|
||||||
|
(characterRange.empty() ? characterRange.emplace_back() : characterRange.back()).upperBound = *globIter++;
|
||||||
|
state = AppendRangeLower;
|
||||||
|
break;
|
||||||
|
case MatchVerbatimly:
|
||||||
|
// match the current character in the glob pattern verbatimly against the current character in the path
|
||||||
|
if (pathIter != pathEnd && matchSingleChar(*globIter)) {
|
||||||
|
++pathIter;
|
||||||
|
handleSingleMatch();
|
||||||
|
} else if (inAsterisk && (asterisks.back().state == MatchManyAnyIncludingDirSep || (pathIter == pathEnd || *pathIter != pathSep))) {
|
||||||
|
// consider the path character dealt with despite no match if we have just passed an asterisk in the glob pattern
|
||||||
|
if (pathIter != pathEnd) {
|
||||||
|
++pathIter;
|
||||||
|
} else {
|
||||||
|
inAsterisk = false;
|
||||||
|
}
|
||||||
|
} else if (!handleMismatch()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MatchRange:
|
||||||
|
// match the concluded character range in the glob pattern against the current character in the path
|
||||||
|
if (pathIter != pathEnd) {
|
||||||
|
auto inRange = false;
|
||||||
|
for (const auto &bounds : characterRange) {
|
||||||
|
if ((!bounds.upperBound.isNull() && *pathIter >= bounds.lowerBound && *pathIter <= bounds.upperBound)
|
||||||
|
|| (bounds.upperBound.isNull() && matchSingleChar(bounds.lowerBound))) {
|
||||||
|
inRange = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inRange) {
|
||||||
|
characterRange.clear();
|
||||||
|
state = MatchVerbatimly;
|
||||||
|
++pathIter;
|
||||||
|
handleSingleMatch();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handleMismatch()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MatchAlternatives:
|
||||||
|
// match the current alternatives as of the current character in the path
|
||||||
|
if (pathIter != pathEnd) {
|
||||||
|
const auto pathStart = pathIter;
|
||||||
|
for (auto &alternative : alternatives) {
|
||||||
|
// match characters in the alternative against the path
|
||||||
|
// note: Special characters like "*" are matched verbatimly. Is that the correct behavior?
|
||||||
|
pathIter = pathStart;
|
||||||
|
for (; alternative.beg != alternative.end && pathIter != pathEnd; ++alternative.beg) {
|
||||||
|
if (*alternative.beg == QChar('\\')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!matchSingleChar(*alternative.beg)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++pathIter;
|
||||||
|
}
|
||||||
|
// go with the first alternative that fully matched
|
||||||
|
// note: What is the correct behavior? Should this be most/least greedy (matching the longest/shortest possible alternative) instead?
|
||||||
|
if (alternative.beg == alternative.end) {
|
||||||
|
alternatives.clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alternatives.empty()) {
|
||||||
|
state = MatchVerbatimly;
|
||||||
|
handleSingleMatch();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handleMismatch()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MatchAny:
|
||||||
|
// allow the current character in the path to be anything but a path separator; otherwise consider it as mismatch as in the case for an exact match
|
||||||
|
if (pathIter == pathEnd || *pathIter != pathSep) {
|
||||||
|
++globIter, ++pathIter;
|
||||||
|
} else if (!handleMismatch()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MatchManyAny: {
|
||||||
|
// take record of the asterisks
|
||||||
|
auto &glob = asterisks.emplace_back();
|
||||||
|
glob.pos = ++globIter;
|
||||||
|
glob.state = MatchManyAny;
|
||||||
|
inAsterisk = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MatchManyAnyIncludingDirSep: {
|
||||||
|
// take record of the second asterisks
|
||||||
|
auto &glob = asterisks.back();
|
||||||
|
glob.pos = ++globIter;
|
||||||
|
glob.state = MatchManyAnyIncludingDirSep;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check whether all characters of the glob have been matched against all characters of the path
|
||||||
|
if (globIter == globEnd) {
|
||||||
|
if (pathIter == pathEnd) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// try again as of the next path segment if the glob fully matched but there are still characters in the path to be matched
|
||||||
|
// note: This allows "foo" to match against "foo/foo" even tough the glob characters have already consumed after matching the first path segment.
|
||||||
|
if (!matchFromRoot && *pathIter == pathSep) {
|
||||||
|
state = MatchVerbatimly;
|
||||||
|
++pathIter;
|
||||||
|
globIter = glob.begin();
|
||||||
|
asterisks.clear();
|
||||||
|
inAsterisk = false;
|
||||||
|
goto match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// consider the match a success if there are still characters in the path to be matched but the glob ended with a "*"
|
||||||
|
return state == MatchManyAny || state == MatchManyAnyIncludingDirSep;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Data
|
|
@ -0,0 +1,50 @@
|
||||||
|
#ifndef DATA_SYNCTHINGIGNOREPATTERN_H
|
||||||
|
#define DATA_SYNCTHINGIGNOREPATTERN_H
|
||||||
|
|
||||||
|
#include "./global.h"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace Data {
|
||||||
|
|
||||||
|
namespace SyncthingIgnorePatternState {
|
||||||
|
struct Asterisk;
|
||||||
|
struct CharacterRange;
|
||||||
|
struct AlternativeRange;
|
||||||
|
} // namespace SyncthingIgnorePatternState
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief The SyncthingIgnorePattern struct allows matching a Syncthing ignore pattern against a path.
|
||||||
|
* \sa
|
||||||
|
* - https://docs.syncthing.net/users/ignoring.html
|
||||||
|
* - https://docs.syncthing.net/rest/db-ignores-get.html
|
||||||
|
*/
|
||||||
|
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingIgnorePattern {
|
||||||
|
explicit SyncthingIgnorePattern(QString &&pattern);
|
||||||
|
SyncthingIgnorePattern(const SyncthingIgnorePattern &) = delete;
|
||||||
|
SyncthingIgnorePattern(SyncthingIgnorePattern &&);
|
||||||
|
~SyncthingIgnorePattern();
|
||||||
|
bool matches(const QString &path) const;
|
||||||
|
|
||||||
|
/// \brief The full ignore pattern as passed to the c'tor (unless modified).
|
||||||
|
QString pattern;
|
||||||
|
/// \brief The part of the pattern that will actually be used for globbing by the matches() function.
|
||||||
|
QString glob;
|
||||||
|
/// \brief Whether the pattern is a comment.
|
||||||
|
bool comment = false;
|
||||||
|
/// \brief Whether the pattern will lead to ignoring matching items (the default).
|
||||||
|
bool ignore = true;
|
||||||
|
/// \brief Whether the pattern will match case-insensetively.
|
||||||
|
bool caseInsensitive = false;
|
||||||
|
/// \brief Whether matching items may be removed when the otherwise empty parent directory would be removed.
|
||||||
|
bool allowRemovalOnParentDirRemoval = false;
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable std::vector<SyncthingIgnorePatternState::Asterisk> asterisks;
|
||||||
|
mutable std::vector<SyncthingIgnorePatternState::CharacterRange> characterRange;
|
||||||
|
mutable std::vector<SyncthingIgnorePatternState::AlternativeRange> alternatives;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Data
|
||||||
|
|
||||||
|
#endif // DATA_SYNCTHINGIGNOREPATTERN_H
|
|
@ -1,6 +1,7 @@
|
||||||
#include "../syncthingconfig.h"
|
#include "../syncthingconfig.h"
|
||||||
#include "../syncthingconnection.h"
|
#include "../syncthingconnection.h"
|
||||||
#include "../syncthingconnectionsettings.h"
|
#include "../syncthingconnectionsettings.h"
|
||||||
|
#include "../syncthingignorepattern.h"
|
||||||
#include "../syncthingprocess.h"
|
#include "../syncthingprocess.h"
|
||||||
#include "../syncthingservice.h"
|
#include "../syncthingservice.h"
|
||||||
#include "../utils.h"
|
#include "../utils.h"
|
||||||
|
@ -39,6 +40,7 @@ class MiscTests : public TestFixture {
|
||||||
#endif
|
#endif
|
||||||
CPPUNIT_TEST(testConnectionSettingsAndLoadingSelfSignedCert);
|
CPPUNIT_TEST(testConnectionSettingsAndLoadingSelfSignedCert);
|
||||||
CPPUNIT_TEST(testSyncthingDir);
|
CPPUNIT_TEST(testSyncthingDir);
|
||||||
|
CPPUNIT_TEST(testIgnorePatternMatching);
|
||||||
CPPUNIT_TEST_SUITE_END();
|
CPPUNIT_TEST_SUITE_END();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
@ -52,6 +54,7 @@ public:
|
||||||
#endif
|
#endif
|
||||||
void testConnectionSettingsAndLoadingSelfSignedCert();
|
void testConnectionSettingsAndLoadingSelfSignedCert();
|
||||||
void testSyncthingDir();
|
void testSyncthingDir();
|
||||||
|
void testIgnorePatternMatching();
|
||||||
|
|
||||||
void setUp() override;
|
void setUp() override;
|
||||||
void tearDown() override;
|
void tearDown() override;
|
||||||
|
@ -279,3 +282,122 @@ void MiscTests::testSyncthingDir()
|
||||||
CPPUNIT_ASSERT_MESSAGE("same status again not considered an update",
|
CPPUNIT_ASSERT_MESSAGE("same status again not considered an update",
|
||||||
!dir.assignStatus(QStringLiteral("idle"), updateEvent += 1, updateTime += TimeSpan::fromMinutes(1.5)));
|
!dir.assignStatus(QStringLiteral("idle"), updateEvent += 1, updateTime += TimeSpan::fromMinutes(1.5)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MiscTests::testIgnorePatternMatching()
|
||||||
|
{
|
||||||
|
auto p1 = SyncthingIgnorePattern(QStringLiteral("foo"));
|
||||||
|
CPPUNIT_ASSERT(!p1.comment);
|
||||||
|
CPPUNIT_ASSERT(!p1.caseInsensitive);
|
||||||
|
CPPUNIT_ASSERT(p1.ignore);
|
||||||
|
CPPUNIT_ASSERT(!p1.allowRemovalOnParentDirRemoval);
|
||||||
|
CPPUNIT_ASSERT_EQUAL(QStringLiteral("foo"), p1.glob);
|
||||||
|
CPPUNIT_ASSERT(p1.matches(QStringLiteral("foo")));
|
||||||
|
CPPUNIT_ASSERT(!p1.matches(QStringLiteral("foofoo")));
|
||||||
|
CPPUNIT_ASSERT(p1.matches(QStringLiteral("foo/foo")));
|
||||||
|
|
||||||
|
auto p2 = SyncthingIgnorePattern(QStringLiteral("foo*"));
|
||||||
|
CPPUNIT_ASSERT(p2.matches(QStringLiteral("foo")));
|
||||||
|
CPPUNIT_ASSERT(p2.matches(QStringLiteral("foobar")));
|
||||||
|
CPPUNIT_ASSERT(!p2.matches(QStringLiteral("barfoo")));
|
||||||
|
CPPUNIT_ASSERT(p2.matches(QStringLiteral("bar/foo")));
|
||||||
|
|
||||||
|
auto p3 = SyncthingIgnorePattern(QStringLiteral("fo*ar"));
|
||||||
|
CPPUNIT_ASSERT(!p3.matches(QStringLiteral("foo")));
|
||||||
|
CPPUNIT_ASSERT(p3.matches(QStringLiteral("foar")));
|
||||||
|
CPPUNIT_ASSERT(p3.matches(QStringLiteral("foobar")));
|
||||||
|
CPPUNIT_ASSERT(!p3.matches(QStringLiteral("foobaR")));
|
||||||
|
CPPUNIT_ASSERT(p3.matches(QStringLiteral("foobaRar")));
|
||||||
|
CPPUNIT_ASSERT(p3.matches(QStringLiteral("foo*bar")));
|
||||||
|
|
||||||
|
auto p4 = SyncthingIgnorePattern(QStringLiteral("fo\\*ar"));
|
||||||
|
CPPUNIT_ASSERT(!p4.matches(QStringLiteral("foar")));
|
||||||
|
CPPUNIT_ASSERT(!p4.matches(QStringLiteral("foobar")));
|
||||||
|
CPPUNIT_ASSERT(!p4.matches(QStringLiteral("foo*bar")));
|
||||||
|
CPPUNIT_ASSERT(p4.matches(QStringLiteral("fo*ar")));
|
||||||
|
|
||||||
|
auto p5 = SyncthingIgnorePattern(QStringLiteral("te*ne"));
|
||||||
|
CPPUNIT_ASSERT(p5.matches(QStringLiteral("telephone")));
|
||||||
|
CPPUNIT_ASSERT(p5.matches(QStringLiteral("subdir/telephone")));
|
||||||
|
CPPUNIT_ASSERT(!p5.matches(QStringLiteral("tele/phone")));
|
||||||
|
|
||||||
|
auto p6 = SyncthingIgnorePattern(QStringLiteral("te**ne"));
|
||||||
|
CPPUNIT_ASSERT(p6.matches(QStringLiteral("telephone")));
|
||||||
|
CPPUNIT_ASSERT(p6.matches(QStringLiteral("subdir/telephone")));
|
||||||
|
CPPUNIT_ASSERT(p6.matches(QStringLiteral("tele/phone")));
|
||||||
|
|
||||||
|
auto p7 = SyncthingIgnorePattern(QStringLiteral("te??st"));
|
||||||
|
CPPUNIT_ASSERT(p7.matches(QStringLiteral("tebest")));
|
||||||
|
CPPUNIT_ASSERT(!p7.matches(QStringLiteral("teb/st")));
|
||||||
|
CPPUNIT_ASSERT(!p7.matches(QStringLiteral("test")));
|
||||||
|
|
||||||
|
auto p8 = SyncthingIgnorePattern(QStringLiteral("fo*ar*y"));
|
||||||
|
CPPUNIT_ASSERT(!p8.matches(QStringLiteral("fooy")));
|
||||||
|
CPPUNIT_ASSERT(p8.matches(QStringLiteral("foary")));
|
||||||
|
CPPUNIT_ASSERT(p8.matches(QStringLiteral("foobary")));
|
||||||
|
CPPUNIT_ASSERT(!p8.matches(QStringLiteral("foobaRy")));
|
||||||
|
CPPUNIT_ASSERT(p8.matches(QStringLiteral("foobaRary")));
|
||||||
|
CPPUNIT_ASSERT(!p8.matches(QStringLiteral("foobaRaRy")));
|
||||||
|
CPPUNIT_ASSERT(p8.matches(QStringLiteral("foobaRaRarxy")));
|
||||||
|
CPPUNIT_ASSERT(p8.matches(QStringLiteral("bar/foobaRaRarxy")));
|
||||||
|
|
||||||
|
auto p9 = SyncthingIgnorePattern(QStringLiteral("/foo*"));
|
||||||
|
CPPUNIT_ASSERT(p9.matches(QStringLiteral("foo")));
|
||||||
|
CPPUNIT_ASSERT(p9.matches(QStringLiteral("foobar")));
|
||||||
|
CPPUNIT_ASSERT(!p9.matches(QStringLiteral("barfoo")));
|
||||||
|
CPPUNIT_ASSERT(!p9.matches(QStringLiteral("bar/foo")));
|
||||||
|
|
||||||
|
auto p10 = SyncthingIgnorePattern(QStringLiteral("/fo?"));
|
||||||
|
CPPUNIT_ASSERT(p10.matches(QStringLiteral("foO")));
|
||||||
|
CPPUNIT_ASSERT(!p10.matches(QStringLiteral("foO/")));
|
||||||
|
CPPUNIT_ASSERT(!p10.matches(QStringLiteral("fo/")));
|
||||||
|
CPPUNIT_ASSERT(!p10.matches(QStringLiteral("bar/foO")));
|
||||||
|
|
||||||
|
auto p11 = SyncthingIgnorePattern(QStringLiteral("/fo[o0.]/bar"));
|
||||||
|
CPPUNIT_ASSERT(p11.matches(QStringLiteral("foo/bar")));
|
||||||
|
CPPUNIT_ASSERT(p11.matches(QStringLiteral("fo0/bar")));
|
||||||
|
CPPUNIT_ASSERT(p11.matches(QStringLiteral("fo./bar")));
|
||||||
|
CPPUNIT_ASSERT(!p11.matches(QStringLiteral("foO/bar")));
|
||||||
|
CPPUNIT_ASSERT(!p11.matches(QStringLiteral("fo?/bar")));
|
||||||
|
|
||||||
|
p11.caseInsensitive = true;
|
||||||
|
CPPUNIT_ASSERT(p11.matches(QStringLiteral("foO/bar")));
|
||||||
|
|
||||||
|
auto p12 = SyncthingIgnorePattern(QStringLiteral("/f[oA-C0-3.]o/bar"));
|
||||||
|
CPPUNIT_ASSERT(p12.matches(QStringLiteral("f0o/bar")));
|
||||||
|
CPPUNIT_ASSERT(p12.matches(QStringLiteral("f1o/bar")));
|
||||||
|
CPPUNIT_ASSERT(p12.matches(QStringLiteral("f2o/bar")));
|
||||||
|
CPPUNIT_ASSERT(p12.matches(QStringLiteral("f3o/bar")));
|
||||||
|
CPPUNIT_ASSERT(!p12.matches(QStringLiteral("f4o/bar")));
|
||||||
|
CPPUNIT_ASSERT(p12.matches(QStringLiteral("foo/bar")));
|
||||||
|
CPPUNIT_ASSERT(!p12.matches(QStringLiteral("foO/bar")));
|
||||||
|
CPPUNIT_ASSERT(p12.matches(QStringLiteral("fAo/bar")));
|
||||||
|
CPPUNIT_ASSERT(p12.matches(QStringLiteral("fBo/bar")));
|
||||||
|
CPPUNIT_ASSERT(p12.matches(QStringLiteral("fCo/bar")));
|
||||||
|
CPPUNIT_ASSERT(!p12.matches(QStringLiteral("fDo/bar")));
|
||||||
|
|
||||||
|
auto p13 = SyncthingIgnorePattern(QStringLiteral("/f{o,0o0,l}o/{bar,biz}"));
|
||||||
|
CPPUNIT_ASSERT(p13.matches(QStringLiteral("foo/bar")));
|
||||||
|
CPPUNIT_ASSERT(p13.matches(QStringLiteral("foo/biz")));
|
||||||
|
CPPUNIT_ASSERT(!p13.matches(QStringLiteral("f/biz")));
|
||||||
|
CPPUNIT_ASSERT(!p13.matches(QStringLiteral("f0o0/bar")));
|
||||||
|
CPPUNIT_ASSERT(p13.matches(QStringLiteral("f0o0o/bar")));
|
||||||
|
CPPUNIT_ASSERT(!p13.matches(QStringLiteral("f{o,0o0,}o/{bar,biz}")));
|
||||||
|
|
||||||
|
auto p14 = SyncthingIgnorePattern(QStringLiteral("/rust/**/target"));
|
||||||
|
CPPUNIT_ASSERT(p14.matches(QStringLiteral("rust/formatter/target")));
|
||||||
|
CPPUNIT_ASSERT(!p14.matches(QStringLiteral("rust/formatter/target/CACHEDIR.TAG")));
|
||||||
|
|
||||||
|
auto p14a = SyncthingIgnorePattern(QStringLiteral("/rust/**/target/"));
|
||||||
|
CPPUNIT_ASSERT(!p14a.matches(QStringLiteral("rust/formatter/target")));
|
||||||
|
CPPUNIT_ASSERT(!p14a.matches(QStringLiteral("rust/formatter/target/CACHEDIR.TAG")));
|
||||||
|
|
||||||
|
auto p15 = SyncthingIgnorePattern(QStringLiteral("/fo[\\]o]/bar"));
|
||||||
|
CPPUNIT_ASSERT(p15.matches(QStringLiteral("foo/bar")));
|
||||||
|
CPPUNIT_ASSERT(p15.matches(QStringLiteral("fo]/bar")));
|
||||||
|
CPPUNIT_ASSERT(!p15.matches(QStringLiteral("fo\\/bar")));
|
||||||
|
|
||||||
|
auto p16 = SyncthingIgnorePattern(QStringLiteral("/fo{o\\},o}/bar"));
|
||||||
|
CPPUNIT_ASSERT(p16.matches(QStringLiteral("foo/bar")));
|
||||||
|
CPPUNIT_ASSERT(p16.matches(QStringLiteral("foo}/bar")));
|
||||||
|
CPPUNIT_ASSERT(!p16.matches(QStringLiteral("/foo\\,o}/bar")));
|
||||||
|
}
|
||||||
|
|
|
@ -74,13 +74,16 @@ SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const Sy
|
||||||
m_root->type = SyncthingItemType::Directory;
|
m_root->type = SyncthingItemType::Directory;
|
||||||
m_root->path = QStringLiteral(""); // assign an empty QString that is not null
|
m_root->path = QStringLiteral(""); // assign an empty QString that is not null
|
||||||
m_fetchQueue.append(m_root->path);
|
m_fetchQueue.append(m_root->path);
|
||||||
|
queryIgnores();
|
||||||
processFetchQueue();
|
processFetchQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
SyncthingFileModel::~SyncthingFileModel()
|
SyncthingFileModel::~SyncthingFileModel()
|
||||||
{
|
{
|
||||||
QObject::disconnect(m_pendingRequest.connection);
|
QObject::disconnect(m_pendingRequest.connection);
|
||||||
|
QObject::disconnect(m_ignorePatternsRequest.connection);
|
||||||
delete m_pendingRequest.reply;
|
delete m_pendingRequest.reply;
|
||||||
|
delete m_ignorePatternsRequest.reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
QHash<int, QByteArray> SyncthingFileModel::roleNames() const
|
QHash<int, QByteArray> SyncthingFileModel::roleNames() const
|
||||||
|
@ -99,7 +102,7 @@ QHash<int, QByteArray> SyncthingFileModel::roleNames() const
|
||||||
|
|
||||||
QModelIndex SyncthingFileModel::index(int row, int column, const QModelIndex &parent) const
|
QModelIndex SyncthingFileModel::index(int row, int column, const QModelIndex &parent) const
|
||||||
{
|
{
|
||||||
if (row < 0 || column < 0 || column > 2 || parent.column() > 0) {
|
if (row < 0 || column < 0 || column > 3 || parent.column() > 0) {
|
||||||
return QModelIndex();
|
return QModelIndex();
|
||||||
}
|
}
|
||||||
if (!parent.isValid()) {
|
if (!parent.isValid()) {
|
||||||
|
@ -177,6 +180,8 @@ QVariant SyncthingFileModel::headerData(int section, Qt::Orientation orientation
|
||||||
return tr("Size");
|
return tr("Size");
|
||||||
case 2:
|
case 2:
|
||||||
return tr("Last modified");
|
return tr("Last modified");
|
||||||
|
case 3:
|
||||||
|
return tr("Ignore pattern");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:;
|
default:;
|
||||||
|
@ -236,6 +241,13 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
|
||||||
default:
|
default:
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
case 3:
|
||||||
|
if (item->ignorePattern == SyncthingItem::ignorePatternNotInitialized) {
|
||||||
|
matchItemAgainstIgnorePatterns(*item);
|
||||||
|
}
|
||||||
|
if (item->ignorePattern < m_presentIgnorePatterns.size()) {
|
||||||
|
return m_presentIgnorePatterns[item->ignorePattern].pattern;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Qt::CheckStateRole:
|
case Qt::CheckStateRole:
|
||||||
|
@ -411,7 +423,7 @@ int SyncthingFileModel::rowCount(const QModelIndex &parent) const
|
||||||
int SyncthingFileModel::columnCount(const QModelIndex &parent) const
|
int SyncthingFileModel::columnCount(const QModelIndex &parent) const
|
||||||
{
|
{
|
||||||
Q_UNUSED(parent)
|
Q_UNUSED(parent)
|
||||||
return 3;
|
return 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SyncthingFileModel::canFetchMore(const QModelIndex &parent) const
|
bool SyncthingFileModel::canFetchMore(const QModelIndex &parent) const
|
||||||
|
@ -606,6 +618,44 @@ void SyncthingFileModel::processFetchQueue(const QString &lastItemPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SyncthingFileModel::queryIgnores()
|
||||||
|
{
|
||||||
|
m_connection.ignores(m_dirId, [this](SyncthingIgnores &&ignores, QString &&errorMessage) {
|
||||||
|
m_ignorePatternsRequest.reply = nullptr;
|
||||||
|
m_hasIgnorePatterns = errorMessage.isEmpty();
|
||||||
|
m_presentIgnorePatterns.clear();
|
||||||
|
if (!m_hasIgnorePatterns) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_presentIgnorePatterns.reserve(static_cast<std::size_t>(ignores.ignore.size()));
|
||||||
|
for (auto &ignorePattern : ignores.ignore) {
|
||||||
|
m_presentIgnorePatterns.emplace_back(std::move(ignorePattern));
|
||||||
|
}
|
||||||
|
invalidateAllIndicies(QVector<int>{ Qt::DisplayRole }, 3, QModelIndex());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncthingFileModel::matchItemAgainstIgnorePatterns(SyncthingItem &item) const
|
||||||
|
{
|
||||||
|
if (!m_hasIgnorePatterns) {
|
||||||
|
item.ignorePattern = SyncthingItem::ignorePatternNotInitialized;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
item.ignorePattern = SyncthingItem::ignorePatternNoMatch;
|
||||||
|
if (!item.isFilesystemItem()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto index = std::size_t();
|
||||||
|
for (const auto &ignorePattern : m_presentIgnorePatterns) {
|
||||||
|
if (ignorePattern.matches(item.path)) {
|
||||||
|
item.ignorePattern = index;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SyncthingFileModel::handleLocalLookupFinished()
|
void SyncthingFileModel::handleLocalLookupFinished()
|
||||||
{
|
{
|
||||||
// get refreshed index/item
|
// get refreshed index/item
|
||||||
|
|
|
@ -4,12 +4,14 @@
|
||||||
#include "./syncthingmodel.h"
|
#include "./syncthingmodel.h"
|
||||||
|
|
||||||
#include <syncthingconnector/syncthingconnection.h>
|
#include <syncthingconnector/syncthingconnection.h>
|
||||||
|
#include <syncthingconnector/syncthingignorepattern.h>
|
||||||
|
|
||||||
#include <QFuture>
|
#include <QFuture>
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
QT_FORWARD_DECLARE_CLASS(QAction)
|
QT_FORWARD_DECLARE_CLASS(QAction)
|
||||||
|
|
||||||
|
@ -67,6 +69,8 @@ private Q_SLOTS:
|
||||||
private:
|
private:
|
||||||
void setCheckState(const QModelIndex &index, Qt::CheckState checkState);
|
void setCheckState(const QModelIndex &index, Qt::CheckState checkState);
|
||||||
void processFetchQueue(const QString &lastItemPath = QString());
|
void processFetchQueue(const QString &lastItemPath = QString());
|
||||||
|
void queryIgnores();
|
||||||
|
void matchItemAgainstIgnorePatterns(SyncthingItem &item) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
using SyncthingItems = std::vector<std::unique_ptr<SyncthingItem>>;
|
using SyncthingItems = std::vector<std::unique_ptr<SyncthingItem>>;
|
||||||
|
@ -81,11 +85,14 @@ private:
|
||||||
SyncthingConnection &m_connection;
|
SyncthingConnection &m_connection;
|
||||||
QString m_dirId;
|
QString m_dirId;
|
||||||
QString m_localPath;
|
QString m_localPath;
|
||||||
|
std::vector<SyncthingIgnorePattern> m_presentIgnorePatterns;
|
||||||
QStringList m_fetchQueue;
|
QStringList m_fetchQueue;
|
||||||
|
SyncthingConnection::QueryResult m_ignorePatternsRequest;
|
||||||
QueryResult m_pendingRequest;
|
QueryResult m_pendingRequest;
|
||||||
QFutureWatcher<LocalLookupRes> m_localItemLookup;
|
QFutureWatcher<LocalLookupRes> m_localItemLookup;
|
||||||
std::unique_ptr<SyncthingItem> m_root;
|
std::unique_ptr<SyncthingItem> m_root;
|
||||||
bool m_selectionMode;
|
bool m_selectionMode;
|
||||||
|
bool m_hasIgnorePatterns;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline bool SyncthingFileModel::isSelectionModeEnabled() const
|
inline bool SyncthingFileModel::isSelectionModeEnabled() const
|
||||||
|
|
|
@ -57,6 +57,23 @@ void SyncthingModel::invalidateAllIndicies(const QVector<int> &affectedRoles, co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SyncthingModel::invalidateAllIndicies(const QVector<int> &affectedRoles, int column, const QModelIndex &parentIndex)
|
||||||
|
{
|
||||||
|
const auto rows = rowCount(parentIndex);
|
||||||
|
const auto columns = columnCount(parentIndex);
|
||||||
|
if (rows <= 0 || column >= columns) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto topLeftIndex = index(0, column, parentIndex);
|
||||||
|
const auto bottomRightIndex = index(rows - 1, column, parentIndex);
|
||||||
|
emit dataChanged(topLeftIndex, bottomRightIndex, affectedRoles);
|
||||||
|
for (auto row = 0; row != rows; ++row) {
|
||||||
|
if (const auto idx = index(row, 0, parentIndex); idx.isValid()) {
|
||||||
|
invalidateAllIndicies(affectedRoles, column, idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SyncthingModel::setBrightColors(bool brightColors)
|
void SyncthingModel::setBrightColors(bool brightColors)
|
||||||
{
|
{
|
||||||
if (m_brightColors == brightColors) {
|
if (m_brightColors == brightColors) {
|
||||||
|
|
|
@ -35,6 +35,7 @@ protected:
|
||||||
void invalidateTopLevelIndicies(const QVector<int> &affectedRoles);
|
void invalidateTopLevelIndicies(const QVector<int> &affectedRoles);
|
||||||
void invalidateNestedIndicies(const QVector<int> &affectedRoles);
|
void invalidateNestedIndicies(const QVector<int> &affectedRoles);
|
||||||
void invalidateAllIndicies(const QVector<int> &affectedRoles, const QModelIndex &parentIndex = QModelIndex());
|
void invalidateAllIndicies(const QVector<int> &affectedRoles, const QModelIndex &parentIndex = QModelIndex());
|
||||||
|
void invalidateAllIndicies(const QVector<int> &affectedRoles, int column, const QModelIndex &parentIndex = QModelIndex());
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
virtual void handleConfigInvalidated();
|
virtual void handleConfigInvalidated();
|
||||||
|
|
Loading…
Reference in New Issue