Skip to content

Commit 722731a

Browse files
authored
Make frame attributes writable (herbstluftwm#1201)
The frame attributes of FrameLeaf and FrameSplit are now writable and allow to adjust the frame locally. As usual, the test cases consume more lines than the actual source code accomplishing this change. In the source code, the getTag() function seemed to be unused. Unfortunately, I had to remove the 'const' qualifier from getFraction() because otherwise the DynAttribute_ constructor failed to match. This is another step towards a full implementation of frame objects (#2) and this fixes #7.
1 parent 28ea7f6 commit 722731a

File tree

4 files changed

+220
-8
lines changed

4 files changed

+220
-8
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Current git version
1313
* Do not draw frame background behind clients (so for semi-transparent
1414
client decorations, one does not see the frame decoration behind but the
1515
wallpaper instead)
16+
* The frame attributes ('selection', 'algorithm', 'fraction', 'split_type')
17+
are now writable.
1618
* The setting 'monitors_locked' is now explicitly an unsigned integer.
1719
* The 'shift' command now moves the window to a neighboured monitor if
1820
the window cannot be moved within a tag in the desired direction.

src/layout.cpp

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ Frame::~Frame() = default;
4242
FrameLeaf::FrameLeaf(HSTag* tag, Settings* settings, weak_ptr<FrameSplit> parent)
4343
: Frame(tag, settings, parent)
4444
, client_count_(this, "client_count", [this]() {return clientCount(); })
45-
, selectionAttr_(this, "selection", &FrameLeaf::getSelection)
46-
, algorithmAttr_(this, "algorithm", &FrameLeaf::getLayout)
45+
, selectionAttr_(this, "selection", &FrameLeaf::getSelection, &FrameLeaf::userSetsSelection)
46+
, algorithmAttr_(this, "algorithm", &FrameLeaf::getLayout, &FrameLeaf::userSetsLayout)
4747
{
4848
auto l = settings->default_frame_layout();
4949
if (l >= layoutAlgorithmCount()) {
@@ -58,9 +58,9 @@ FrameSplit::FrameSplit(HSTag* tag, Settings* settings, weak_ptr<FrameSplit> pare
5858
FixPrecDec fraction, SplitAlign align, shared_ptr<Frame> a,
5959
shared_ptr<Frame> b)
6060
: Frame(tag, settings, parent)
61-
, splitTypeAttr_(this, "split_type", &FrameSplit::getAlign)
62-
, fractionAttr_(this, "fraction", &FrameSplit::getFraction)
63-
, selectionAttr_(this, "selection", [this]() { return selection_;})
61+
, splitTypeAttr_(this, "split_type", &FrameSplit::getAlign, &FrameSplit::userSetsSplitType)
62+
, fractionAttr_(this, "fraction", &FrameSplit::getFraction, &FrameSplit::userSetsFraction)
63+
, selectionAttr_(this, "selection", &FrameSplit::getSelection, &FrameSplit::userSetsSelection)
6464
, aLink_(*this, "0")
6565
, bLink_(*this, "1")
6666
{
@@ -158,6 +158,30 @@ shared_ptr<FrameSplit> FrameSplit::thisSplit() {
158158
return dynamic_pointer_cast<FrameSplit>(shared_from_this());
159159
}
160160

161+
string FrameSplit::userSetsSplitType(SplitAlign align)
162+
{
163+
align_ = align;
164+
relayout();
165+
return {};
166+
}
167+
168+
string FrameSplit::userSetsSelection(int idx)
169+
{
170+
if (idx < 0 || idx > 1) {
171+
return "index out of range";
172+
}
173+
selection_ = idx;
174+
relayout();
175+
return {};
176+
}
177+
178+
string FrameSplit::userSetsFraction(FixPrecDec fraction)
179+
{
180+
setFraction(fraction);
181+
relayout();
182+
return {};
183+
}
184+
161185
string Frame::frameIndex() const
162186
{
163187
auto p = parent_.lock();
@@ -171,6 +195,16 @@ string Frame::frameIndex() const
171195
}
172196
}
173197

198+
/**
199+
* @brief trigger relayouting of the frame tree.
200+
* This should only be called if the user changes something
201+
* via the attribute system.
202+
*/
203+
void Frame::relayout()
204+
{
205+
tag_->needsRelayout_.emit();
206+
}
207+
174208
TilingResult FrameLeaf::layoutLinear(Rectangle rect, bool vertical) {
175209
TilingResult res;
176210
auto cur = rect;
@@ -667,6 +701,29 @@ int FrameLeaf::getInnerNeighbourIndex(Direction direction) {
667701
return index;
668702
}
669703

704+
string FrameLeaf::userSetsLayout(LayoutAlgorithm algo)
705+
{
706+
setLayout(algo);
707+
relayout();
708+
return {};
709+
}
710+
711+
string FrameLeaf::userSetsSelection(int index)
712+
{
713+
if (clients.empty() && index == 0) {
714+
// if there is no client in this frame
715+
// then index 0 is the fallback value and
716+
// there is nothing to be done here
717+
return {};
718+
}
719+
if (index < 0 || static_cast<size_t>(index) >= clients.size()) {
720+
return "index out of range";
721+
}
722+
setSelection(index);
723+
relayout();
724+
return {};
725+
}
726+
670727
void FrameLeaf::moveClient(int new_index) {
671728
swap(clients[new_index], clients[selection]);
672729
selection = new_index;

src/layout.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,6 @@ class Frame : public std::enable_shared_from_this<Frame>, public Object {
5353
// count the number of splits to the root with alignment "align"
5454
virtual int splitsToRoot(SplitAlign align);
5555

56-
HSTag* getTag() { return tag_; };
57-
5856
void setVisibleRecursive(bool visible);
5957

6058
/*! a case distinction on the type of tree node. If `this` is a
@@ -85,6 +83,7 @@ class Frame : public std::enable_shared_from_this<Frame>, public Object {
8583
virtual std::shared_ptr<FrameSplit> isSplit() { return std::shared_ptr<FrameSplit>(); };
8684
virtual std::shared_ptr<FrameLeaf> isLeaf() { return std::shared_ptr<FrameLeaf>(); };
8785
protected:
86+
void relayout();
8887
void foreachClient(ClientAction action);
8988
HSTag* tag_;
9089
Settings* settings_;
@@ -136,6 +135,8 @@ class FrameLeaf : public Frame, public FrameDataLeaf {
136135
DynAttribute_<int> selectionAttr_;
137136
DynAttribute_<LayoutAlgorithm> algorithmAttr_;
138137
private:
138+
std::string userSetsLayout(LayoutAlgorithm algo);
139+
std::string userSetsSelection(int index);
139140
friend class FrameDecoration;
140141
friend class FrameTree;
141142
// layout algorithms
@@ -175,19 +176,23 @@ class FrameSplit : public Frame, public FrameDataSplit<Frame> {
175176
void swapChildren();
176177
void adjustFraction(FixPrecDec delta);
177178
void setFraction(FixPrecDec fraction);
178-
FixPrecDec getFraction() const { return fraction_; }
179+
FixPrecDec getFraction() { return fraction_; }
179180
static FixPrecDec clampFraction(FixPrecDec fraction);
180181
std::shared_ptr<FrameSplit> thisSplit();
181182
std::shared_ptr<FrameSplit> isSplit() override { return thisSplit(); }
182183
SplitAlign getAlign() { return align_; }
183184
void swapSelection() { selection_ = selection_ == 0 ? 1 : 0; }
184185
void setSelection(int s) { selection_ = s; }
186+
int getSelection() { return selection_; }
185187
DynAttribute_<SplitAlign> splitTypeAttr_;
186188
DynAttribute_<FixPrecDec> fractionAttr_;
187189
DynAttribute_<int> selectionAttr_;
188190
Link_<Frame> aLink_;
189191
Link_<Frame> bLink_;
190192
private:
193+
std::string userSetsSplitType(SplitAlign align);
194+
std::string userSetsFraction(FixPrecDec fraction);
195+
std::string userSetsSelection(int idx);
191196
friend class FrameTree;
192197
};
193198

tests/test_layout.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,3 +1527,151 @@ def test_focus_shift_completion(hlwm):
15271527
# actually, passing both -i and -e makes no sense,
15281528
# but ArgParse does not know that the flags exclude each other
15291529
hlwm.command_has_all_args([cmd, 'down', '-i', '-e'])
1530+
1531+
1532+
def test_frame_leaf_selection_change(hlwm):
1533+
"""test the attribute FrameLeaf::selection"""
1534+
clients = hlwm.create_clients(3)
1535+
1536+
def layout(idx):
1537+
return f"(clients vertical:{idx} {clients[0]} {clients[1]} {clients[2]})"
1538+
1539+
hlwm.call(['load', layout(0)])
1540+
1541+
for i in range(0, 3):
1542+
hlwm.attr.tags.focus.tiling.focused_frame.selection = i
1543+
assert hlwm.attr.clients.focus.winid() == clients[i]
1544+
assert hlwm.call('dump').stdout == layout(i)
1545+
1546+
1547+
def test_frame_leaf_selection_if_empty(hlwm):
1548+
assert hlwm.attr.tags.focus.tiling.root.selection() == '0'
1549+
hlwm.attr.tags.focus.tiling.root.selection = 0
1550+
assert hlwm.attr.tags.focus.tiling.root.selection() == '0'
1551+
1552+
hlwm.call_xfail('attr tags.focus.tiling.root.selection 1') \
1553+
.expect_stderr('out of range')
1554+
1555+
hlwm.call_xfail('attr tags.focus.tiling.root.selection 2') \
1556+
.expect_stderr('out of range')
1557+
1558+
hlwm.call_xfail('attr tags.focus.tiling.root.selection -1') \
1559+
.expect_stderr('out of range')
1560+
1561+
1562+
def test_frame_split_selection_change(hlwm):
1563+
"""test the attribute FrameSplit::selection"""
1564+
clients = hlwm.create_clients(2)
1565+
1566+
def layout(idx):
1567+
return normalize_layout_string(f"""
1568+
(split horizontal:0.5:{idx}
1569+
(clients vertical:0 {clients[0]})
1570+
(clients vertical:0 {clients[1]}))
1571+
""")
1572+
1573+
hlwm.call(['load', layout(0)])
1574+
1575+
for i in [0, 1]:
1576+
hlwm.attr.tags.focus.tiling.root.selection = i
1577+
assert hlwm.attr.clients.focus.winid() == clients[i]
1578+
assert hlwm.call('dump').stdout == layout(i)
1579+
1580+
1581+
def test_frame_selection_invalid_arg(hlwm):
1582+
hlwm.create_clients(6) # 6 clients
1583+
hlwm.call('split explode') # 2 frames, so 3 clients per frame
1584+
hlwm.call('dump')
1585+
1586+
# for frame splits, the selection can be 0 or 1
1587+
hlwm.call('attr tags.focus.tiling.root.selection 0') # no failure
1588+
hlwm.call('attr tags.focus.tiling.root.selection 1') # no failure
1589+
hlwm.call_xfail('attr tags.focus.tiling.root.selection -1') \
1590+
.expect_stderr('out of range')
1591+
hlwm.call_xfail('attr tags.focus.tiling.root.selection 2') \
1592+
.expect_stderr('out of range')
1593+
1594+
# for frame leafs, the selection only must be lower than the client count
1595+
hlwm.call('attr tags.focus.tiling.focused_frame.selection 0') # no failure
1596+
hlwm.call('attr tags.focus.tiling.focused_frame.selection 1') # no failure
1597+
hlwm.call('attr tags.focus.tiling.focused_frame.selection 2') # no failure
1598+
hlwm.call_xfail('attr tags.focus.tiling.focused_frame.selection -1') \
1599+
.expect_stderr('out of range')
1600+
hlwm.call_xfail('attr tags.focus.tiling.focused_frame.selection 3') \
1601+
.expect_stderr('out of range')
1602+
1603+
1604+
def test_frame_split_attribute_size_changes(hlwm, x11):
1605+
"""
1606+
Test that changing the attributes 'fraction' or 'split_type' of FrameSplit
1607+
affects window sizes
1608+
"""
1609+
# two clients side by side:
1610+
win1, winid1 = x11.create_client()
1611+
win2, winid2 = x11.create_client()
1612+
layout = f'(split horizontal:0.5:0 (clients max:0 {winid1}) (clients max:0 {winid2}))'
1613+
layout = normalize_layout_string(layout)
1614+
for attr, value in [('fraction', '0.6'), ('split_type', 'vertical')]:
1615+
hlwm.call(['load', layout])
1616+
x11.sync_with_hlwm()
1617+
width1 = win1.get_geometry().width
1618+
width2 = win2.get_geometry().width
1619+
1620+
hlwm.attr.tags.focus.tiling.root[attr] = value
1621+
assert hlwm.attr.tags.focus.tiling.root[attr]() == value
1622+
assert hlwm.call('dump').stdout.strip() != layout
1623+
1624+
x11.sync_with_hlwm()
1625+
# both updated attributes must lead to an increase of width of
1626+
# the first client:
1627+
assert width1 < win1.get_geometry().width
1628+
assert width2 != win2.get_geometry().width
1629+
1630+
1631+
def test_frame_split_fraction_invalid_arg(hlwm):
1632+
hlwm.call(['load', '(split horizontal:0.5:0 (clients max:0) (clients max:0))'])
1633+
1634+
# test values that clamp to 0.1
1635+
for val in ['0.0', '0', '-0.1', '0.05', '0.1', '0.09', '-5', '-0.11']:
1636+
# reset the fraction
1637+
hlwm.attr.tags.focus.tiling.root.fraction = '0.5'
1638+
assert hlwm.attr.tags.focus.tiling.root.fraction() == '0.5'
1639+
1640+
hlwm.attr.tags.focus.tiling.root.fraction = val
1641+
assert hlwm.attr.tags.focus.tiling.root.fraction() == '0.1'
1642+
1643+
# test values that clamp to 0.9
1644+
for val in ['12', '1', '0.9', '0.95', '0.9', '0.91', '12312', '+23']:
1645+
# reset the fraction
1646+
hlwm.attr.tags.focus.tiling.root.fraction = '0.87'
1647+
assert hlwm.attr.tags.focus.tiling.root.fraction() == '0.87'
1648+
1649+
hlwm.attr.tags.focus.tiling.root.fraction = val
1650+
assert hlwm.attr.tags.focus.tiling.root.fraction() == '0.9'
1651+
1652+
1653+
def test_frame_leaf_algorithm_change(hlwm, x11):
1654+
"""
1655+
Test that changing the layout algorithm affects window sizes
1656+
"""
1657+
# two clients below each other
1658+
win1, winid1 = x11.create_client()
1659+
win2, winid2 = x11.create_client()
1660+
hlwm.call('set_layout vertical')
1661+
1662+
geom1_before = win1.get_geometry()
1663+
geom2_before = win2.get_geometry()
1664+
assert geom1_before.width == geom2_before.width
1665+
assert geom1_before.height == geom2_before.height
1666+
1667+
# put them side by side
1668+
hlwm.attr.tags.focus.tiling.root.algorithm = 'horizontal'
1669+
1670+
x11.sync_with_hlwm()
1671+
geom1_now = win1.get_geometry()
1672+
geom2_now = win2.get_geometry()
1673+
# side by side implies: width decreases but height increases
1674+
assert geom1_before.width > geom1_now.width
1675+
assert geom1_before.height < geom1_now.height
1676+
assert geom1_now.width == geom2_now.width
1677+
assert geom1_now.height == geom2_now.height

0 commit comments

Comments
 (0)