added 'SetXProp' action and (@PROP=foo) clientpattern

these two allow 'tagging' of arbitrary windows with 'tags' (or 'labels'). such 'tagged' windows can then be used in ':NextWindow (@PROP=foo)' commands to quickly cycle through a subset of available windows.
since the 'tags' are applied as real xproperties to a window they survive a restart of fluxbox or even another windowmanager. the user can also set the tags by using xprop(1).

the next step regarding the UI should be to visualize the tags of a window.
This commit is contained in:
Mathias Gumz 2010-09-11 17:09:07 +02:00
parent 4e2c7e2167
commit faa4c97888
9 changed files with 206 additions and 83 deletions

View file

@ -64,6 +64,9 @@ The following values are accepted for 'propertyname':::
*Layer*;;
The string name of the window's layer, which is one of
*AboveDock*, *Dock*, *Top*, *Normal*, *Bottom*, *Desktop*
*@XPROP*;;
A string, corresponding to any xproperty (Use either the *xprop(1)*
utility or the 'SetXProp' command to set a xproperty to a window)
.Matches any windows with the CLASSNAME of "xterm"
..........
@ -79,3 +82,9 @@ The following values are accepted for 'propertyname':::
...........
(Head=[mouse]) (Layer!=[current])
...........
.Matches any windows having a xproperty named FOO with "bar" in it
..............
(@FOO=.*bar.*)
..............

View file

@ -327,6 +327,10 @@ two arguments;;
heads. If this takes the window beyond the total number of heads, it
will wrap around to the beginning.
*SetXProp* 'PROP=value'::
Sets the xproperty 'PROP' of the current window to 'value'. Delete the
content of 'PROP' by using 'PROP='.
Workspace Commands
~~~~~~~~~~~~~~~~~~
These commands affect the entire workspace (or "desktop" as it is sometimes
@ -643,8 +647,12 @@ Mod4 t :If {Some Matches (xterm)} {NextWindow (xterm)} {Exec xterm}
# Set a different wallpaper on every workspace:
ChangeWorkspace :Exec fbsetbg ~/.fluxbox/bg$(xprop -root _NET_CURRENT_DESKTOP | awk '{print $3}').png
# Focusses the next window with it's xproperty 'PROP' set to 'foo'
Mod4 p Mod4 Tab :NextWindow (@PROP=foo)
..................
AUTHORS
-------
- Jim Ramsay <i.am at jimramsay com> (>fluxbox-1.0.0)

View file

@ -66,7 +66,7 @@ struct Name2WinProperty {
ClientPattern::WinProperty prop;
};
Name2WinProperty name_2_winproperties[] = { // sorted for 'bsearch'
const Name2WinProperty name_2_winproperties[] = { // sorted for 'bsearch'
{ "class", ClientPattern::CLASS },
{ "focushidden", ClientPattern::FOCUSHIDDEN },
{ "head", ClientPattern::HEAD },
@ -92,29 +92,43 @@ int name_2_winproperty_cmp(const void* a, const void* b) {
reinterpret_cast<const Name2WinProperty*>(b)->name);
}
const Name2WinProperty* find_winproperty_by_name(const FbTk::FbString& name) {
const Name2WinProperty key = { name.c_str(), ClientPattern::CLASS };
const Name2WinProperty* result = reinterpret_cast<Name2WinProperty*>(
bsearch(&key, name_2_winproperties,
sizeof(name_2_winproperties) / sizeof(Name2WinProperty),
sizeof(Name2WinProperty),
name_2_winproperty_cmp));
return result;
}
struct Prop2String {
ClientPattern::WinProperty prop;
const char* str;
};
Prop2String property_2_strings[] = { // sorted by 'prop'
{ ClientPattern::TITLE, "title=" },
{ ClientPattern::CLASS, "class=" },
{ ClientPattern::NAME, "name=" },
{ ClientPattern::ROLE, "role=" },
{ ClientPattern::TRANSIENT, "transient=" },
{ ClientPattern::MAXIMIZED, "maximized=" },
{ ClientPattern::MINIMIZED, "minimized=" },
{ ClientPattern::SHADED, "shaded=" },
{ ClientPattern::STUCK, "stuck=" },
{ ClientPattern::FOCUSHIDDEN, "focushidden=" },
{ ClientPattern::ICONHIDDEN, "iconhidden=" },
{ ClientPattern::WORKSPACE, "workspace=" },
{ ClientPattern::WORKSPACENAME, "workspacename=" },
{ ClientPattern::HEAD, "head=" },
{ ClientPattern::LAYER, "layer=" },
{ ClientPattern::URGENT, "urgent=" },
{ ClientPattern::SCREEN, "screen=" }
{ ClientPattern::TITLE, "title" },
{ ClientPattern::CLASS, "class" },
{ ClientPattern::NAME, "name" },
{ ClientPattern::ROLE, "role" },
{ ClientPattern::TRANSIENT, "transient" },
{ ClientPattern::MAXIMIZED, "maximized" },
{ ClientPattern::MINIMIZED, "minimized" },
{ ClientPattern::SHADED, "shaded" },
{ ClientPattern::STUCK, "stuck" },
{ ClientPattern::FOCUSHIDDEN, "focushidden" },
{ ClientPattern::ICONHIDDEN, "iconhidden" },
{ ClientPattern::WORKSPACE, "workspace" },
{ ClientPattern::WORKSPACENAME, "workspacename" },
{ ClientPattern::HEAD, "head" },
{ ClientPattern::LAYER, "layer" },
{ ClientPattern::URGENT, "urgent" },
{ ClientPattern::SCREEN, "screen" },
{ ClientPattern::XPROP, "@" },
};
@ -129,16 +143,21 @@ Prop2String property_2_strings[] = { // sorted by 'prop'
*/
struct ClientPattern::Term {
Term(const FbTk::FbString& _regstr, WinProperty _prop, bool _negate) :
orig(_regstr),
Term(const FbTk::FbString& _regstr, WinProperty _prop, bool _negate, const FbTk::FbString& _xprop) :
regstr(_regstr),
xpropstr(_xprop),
regexp(_regstr, true),
prop(_prop),
negate(_negate) {
xprop = XInternAtom(FbTk::App::instance()->display(), xpropstr.c_str(), False);
}
FbTk::FbString orig;
FbTk::RegExp regexp;
// (title=.*bar) or (@FOO=.*bar)
FbTk::FbString regstr; // .*bar
FbTk::FbString xpropstr; // @FOO=.*bar
Atom xprop; // Atom of 'FOO'
FbTk::RegExp regexp; // compiled version of '.*bar'
WinProperty prop;
bool negate;
};
@ -175,51 +194,65 @@ ClientPattern::ClientPattern(const char *str):
err = FbTk::StringUtil::getStringBetween(match,
str + pos,
'(', ')', " \t\n", true);
if (err > 0) {
// need to determine the property used
string memstr, expr;
WinProperty prop;
string::size_type eq = match.find_first_of('=');
if (eq == match.npos) {
memstr = match;
expr = "[current]";
} else {
memstr.assign(match, 0, eq); // memstr = our identifier
expr.assign(match, eq+1, match.length());
}
WinProperty prop = NAME;
std::string expr;
std::string xprop;
bool negate = false;
if (!memstr.empty() && memstr[memstr.length()-1] == '!') {
negate = true;
memstr.assign(memstr, 0, memstr.length()-1);
// need to determine the property used, potential patterns:
//
// A) foo (short for 'title=foo')
// B) foo=bar
// C) foo!=bar
//
// D) @foo=bar (xproperty 'foo' equal to 'bar')
//
string propstr = match;
string::size_type eq = propstr.find_first_of('=');
if (eq == propstr.npos) { // A
expr = "[current]";
} else { // B or C, so strip away the '='
// 'bar'
expr.assign(propstr.begin() + eq + 1, propstr.end());
// 'foo' or 'foo!'
propstr.resize(eq);
if (propstr.rfind("!", propstr.npos, 1) != propstr.npos) { // C 'foo!'
negate = true;
propstr.resize(propstr.size()-1);
}
}
memstr = FbTk::StringUtil::toLower(memstr);
if (propstr[0] != '@') { // not D
Name2WinProperty key = { memstr.c_str(), CLASS };
Name2WinProperty* i = reinterpret_cast<Name2WinProperty*>(
bsearch(&key, name_2_winproperties,
sizeof(name_2_winproperties) / sizeof(Name2WinProperty),
sizeof(Name2WinProperty),
name_2_winproperty_cmp));
const Name2WinProperty* p = find_winproperty_by_name(FbTk::StringUtil::toLower(propstr));
if (i) {
prop = i->prop;
} else {
prop = NAME;
expr = match;
if (p) {
prop = p->prop;
} else {
expr = match;
}
} else { // D
prop = XPROP;
xprop.assign(propstr, 1, propstr.size());
}
had_error = !addTerm(expr, prop, negate);
had_error = !addTerm(expr, prop, negate, xprop);
pos += err;
}
}
if (pos == 0 && !had_error) {
// no match terms given, this is not allowed
if (pos == 0 && !had_error) { // no match terms given, this is not allowed
had_error = true;
}
if (!had_error) {
// otherwise, we check for a number
if (!had_error) { // otherwise, we check for a number
string number;
err = FbTk::StringUtil::getStringBetween(number,
str+pos,
@ -251,23 +284,26 @@ ClientPattern::~ClientPattern() {
// return a string representation of this pattern
string ClientPattern::toString() const {
string pat;
string result;
Terms::const_iterator it = m_terms.begin();
Terms::const_iterator it_end = m_terms.end();
for (; it != it_end; ++it) {
pat.append(" (");
pat.append(property_2_strings[(*it)->prop].str);
pat.append((*it)->orig);
pat.append(")");
const Term& term = *(*it);
result.append(" (");
result.append(property_2_strings[term.prop].str);
if (term.prop == XPROP)
result.append(term.xpropstr);
result.append(term.negate ? "!=" : "=");
result.append(term.regstr);
result.append(")");
}
if (m_matchlimit > 0) {
pat.append(" {");
pat.append(FbTk::StringUtil::number2String(m_matchlimit));
pat.append("}");
result.append(" {");
result.append(FbTk::StringUtil::number2String(m_matchlimit));
result.append("}");
}
return pat;
return result;
}
// does this client match this pattern?
@ -282,7 +318,10 @@ bool ClientPattern::match(const Focusable &win) const {
Terms::const_iterator it_end = m_terms.end();
for (; it != it_end; ++it) {
const Term& term = *(*it);
if (term.orig == "[current]") {
if (term.prop == XPROP) {
if (!term.negate ^ (term.regexp.match(win.getTextProperty(term.xprop))))
return false;
} else if (term.regstr == "[current]") {
WinClient *focused = FocusControl::focusedWindow();
if (term.prop == WORKSPACE) {
if (!term.negate ^ (getProperty(term.prop, win) == FbTk::StringUtil::number2String(win.screen().currentWorkspaceID())))
@ -293,7 +332,7 @@ bool ClientPattern::match(const Focusable &win) const {
return false;
} else if (!focused || (!term.negate ^ (getProperty(term.prop, win) == getProperty(term.prop, *focused))))
return false;
} else if (term.prop == HEAD && term.orig == "[mouse]") {
} else if (term.prop == HEAD && term.regstr == "[mouse]") {
if (!term.negate ^ (getProperty(term.prop, win) == FbTk::StringUtil::number2String(win.screen().getCurrHead())))
return false;
@ -307,7 +346,7 @@ bool ClientPattern::dependsOnFocusedWindow() const {
Terms::const_iterator it = m_terms.begin(), it_end = m_terms.end();
for (; it != it_end; ++it) {
if ((*it)->prop != WORKSPACE && (*it)->prop != WORKSPACENAME &&
(*it)->orig == "[current]")
(*it)->regstr == "[current]")
return true;
}
return false;
@ -317,7 +356,7 @@ bool ClientPattern::dependsOnCurrentWorkspace() const {
Terms::const_iterator it = m_terms.begin(), it_end = m_terms.end();
for (; it != it_end; ++it) {
if (((*it)->prop == WORKSPACE || (*it)->prop == WORKSPACENAME) &&
(*it)->orig == "[current]")
(*it)->regstr == "[current]")
return true;
}
return false;
@ -326,17 +365,16 @@ bool ClientPattern::dependsOnCurrentWorkspace() const {
// add an expression to match against
// The first argument is a regular expression, the second is the member
// function that we wish to match against.
bool ClientPattern::addTerm(const FbTk::FbString &str, WinProperty prop, bool negate) {
bool ClientPattern::addTerm(const FbTk::FbString &str, WinProperty prop, bool negate, const FbTk::FbString& xprop) {
bool rc = false;
Term* term = new Term(str, prop, negate);
Term* term = new Term(str, prop, negate, xprop);
if (!term)
return rc;
if (!term->regexp.error()) {
if (rc = !term->regexp.error()) {
m_terms.push_back(term);
rc = true;
} else {
delete term;
}
@ -424,8 +462,11 @@ bool ClientPattern::operator ==(const ClientPattern &pat) const {
Terms::const_iterator other_it = pat.m_terms.begin();
Terms::const_iterator other_it_end = pat.m_terms.end();
for (; it != it_end && other_it != other_it_end; ++it, ++other_it) {
if ((*it)->orig != (*other_it)->orig ||
(*it)->negate != (*other_it)->negate)
const Term& i = *(*it);
const Term& o = *(*other_it);
if (i.regstr != o.regstr ||
i.negate != o.negate ||
i.xpropstr != o.xpropstr)
return false;
}
if (it != it_end || other_it != other_it_end)
@ -433,3 +474,4 @@ bool ClientPattern::operator ==(const ClientPattern &pat) const {
return true;
}

View file

@ -54,7 +54,8 @@ public:
enum WinProperty {
TITLE = 0, CLASS, NAME, ROLE, TRANSIENT,
MAXIMIZED, MINIMIZED, SHADED, STUCK, FOCUSHIDDEN, ICONHIDDEN,
WORKSPACE, WORKSPACENAME, HEAD, LAYER, URGENT, SCREEN
WORKSPACE, WORKSPACENAME, HEAD, LAYER, URGENT, SCREEN,
XPROP
};
/// Does this client match this pattern?
@ -70,9 +71,11 @@ public:
* Add an expression to match against
* @param str is a regular expression
* @param prop is the member function that we wish to match against
* @param negate is if the term should be negated
* @param xprop is the name of the prop if prop is XPROP
* @return false if the regexp wasn't valid
*/
bool addTerm(const FbTk::FbString &str, WinProperty prop, bool negate = false);
bool addTerm(const FbTk::FbString &str, WinProperty prop, bool negate = false, const FbTk::FbString& xprop = FbTk::FbString());
void addMatch() { ++m_nummatches; }
void removeMatch() { --m_nummatches; }

View file

@ -227,23 +227,75 @@ REGISTER_COMMAND_PARSER(focus, parseFocusCmd, void);
class ActivateTabCmd: public WindowHelperCmd {
public:
ActivateTabCmd() { }
explicit ActivateTabCmd() { }
protected:
void real_execute();
void real_execute() {
WinClient* winclient = fbwindow().winClientOfLabelButtonWindow(
Fluxbox::instance()->lastEvent().xany.window);
if (winclient && winclient != &fbwindow().winClient()) {
fbwindow().setCurrentClient(*winclient, true);
}
}
};
void ActivateTabCmd::real_execute() {
REGISTER_COMMAND(activatetab, ActivateTabCmd, void);
WinClient* winclient = fbwindow().winClientOfLabelButtonWindow(
Fluxbox::instance()->lastEvent().xany.window);
class SetXPropCmd: public WindowHelperCmd {
public:
explicit SetXPropCmd(const FbTk::FbString& name, const FbTk::FbString& value) :
m_name(name), m_value(value) { }
if (winclient && winclient != &fbwindow().winClient()) {
fbwindow().setCurrentClient(*winclient, true);
protected:
void real_execute() {
WinClient& client = fbwindow().winClient();
Atom prop = XInternAtom(client.display(), m_name.c_str(), False);
client.changeProperty(prop, XInternAtom(client.display(), "UTF8_STRING", False), 8,
PropModeReplace, (unsigned char*)m_value.c_str(), m_value.size());
}
private:
FbTk::FbString m_name;
FbTk::FbString m_value;
};
FbTk::Command<void> *parseSetXPropCmd(const string &command, const string &args, bool trusted) {
SetXPropCmd* cmd = 0;
if (trusted) {
FbTk::FbString name = args;
FbTk::StringUtil::removeFirstWhitespace(name);
FbTk::StringUtil::removeTrailingWhitespace(name);
if (name.size() > 1 && name[0] != '=') { // the smallest valid argument is 'X='
FbTk::FbString value;
size_t eq = name.find('=');
if (eq != name.npos && eq != name.size()) {
value.assign(name, eq + 1, name.size());
name.resize(eq);
}
cmd = new SetXPropCmd(name, value);
}
}
return cmd;
}
REGISTER_COMMAND(activatetab, ActivateTabCmd, void);
REGISTER_COMMAND_PARSER(setxprop, parseSetXPropCmd, void);
} // end anonymous namespace
@ -677,6 +729,7 @@ void SetAlphaCmd::real_execute() {
: m_unfocus);
}
REGISTER_COMMAND_WITH_ARGS(matches, MatchCmd, bool);
bool MatchCmd::real_execute() {

View file

@ -88,6 +88,8 @@ public:
/// @return wm role string (for pattern matching)
virtual std::string getWMRole() const { return "Focusable"; }
virtual FbTk::FbString getTextProperty(Atom prop) const { return ""; }
/// @return whether this window is a transient (for pattern matching)
virtual bool isTransient() const { return false; }

View file

@ -96,6 +96,7 @@ public:
std::string getWMRole() const;
WindowState::WindowType getWindowType() const { return m_window_type; }
void setWindowType(WindowState::WindowType type) { m_window_type = type; }
FbTk::FbString getTextProperty(Atom prop) const { return FbTk::FbWindow::textProperty(prop); }
WinClient *transientFor() { return transient_for; }
const WinClient *transientFor() const { return transient_for; }

View file

@ -3363,6 +3363,10 @@ FbTk::FbString FluxboxWindow::getWMRole() const {
return (m_client ? m_client->getWMRole() : "FluxboxWindow");
}
FbTk::FbString FluxboxWindow::getTextProperty(Atom prop) const {
return (m_client ? m_client->getTextProperty(prop) : Focusable::getTextProperty(prop));
}
bool FluxboxWindow::isTransient() const {
return (m_client && m_client->isTransient());
}

View file

@ -422,6 +422,7 @@ public:
const FbTk::FbString &getWMClassName() const;
const FbTk::FbString &getWMClassClass() const;
std::string getWMRole() const;
FbTk::FbString getTextProperty(Atom prop) const;
void setWindowType(WindowState::WindowType type);
bool isTransient() const;