1 #include "tagmanager.h"
2
3 #include <memory>
4
5 #include "client.h"
6 #include "command.h"
7 #include "completion.h"
8 #include "ewmh.h"
9 #include "globals.h"
10 #include "ipc-protocol.h"
11 #include "monitor.h"
12 #include "monitormanager.h"
13 #include "utils.h"
14
15 using std::function;
16 using std::string;
17 using std::shared_ptr;
18 using std::vector;
19
20 TagManager* global_tags;
21
TagManager()22 TagManager::TagManager()
23 : IndexingObject()
24 , by_name_(*this)
25 , settings_(nullptr)
26 , focus_(*this, "focus")
27 {
28 indicesChanged.connect([](){
29 Ewmh::get().updateDesktopNames();
30 Ewmh::get().updateCurrentDesktop();
31 tag_set_flags_dirty();
32 });
33
34 setDoc(
35 "The tags (or virtual desktops or workspaces). This contains "
36 " an entry \'index\' for each tag with the given \'index\'."
37 );
38 focus_.setDoc(
39 "the object of the focused tag, equivalently, "
40 "the tag on the focused monitor."
41 );
42 }
43
injectDependencies(MonitorManager * m,Settings * s)44 void TagManager::injectDependencies(MonitorManager* m, Settings *s) {
45 monitors_ = m;
46 settings_ = s;
47 }
48
find(const string & name)49 HSTag* TagManager::find(const string& name) {
50 for (auto t : *this) {
51 if (t->name == name) {
52 return t;
53 }
54 }
55 return {};
56 }
57
completeTag(Completion & complete)58 void TagManager::completeTag(Completion& complete) {
59 for (auto t : *this) {
60 complete.full(t->name);
61 }
62 }
63
64 //! if the name is a valid tag name, return "", otherwise return an error message
isValidTagName(string name)65 string TagManager::isValidTagName(string name) {
66 if (name.empty()) {
67 return "An empty tag name is not permitted";
68 }
69 if (find(name)) {
70 return "A tag with the name \"" + name + "\" already exists";
71 }
72 return "";
73 }
74
add_tag(const string & name)75 HSTag* TagManager::add_tag(const string& name) {
76 HSTag* find_result = find(name);
77 if (find_result) {
78 // nothing to do
79 return find_result;
80 }
81 if (name.empty()) {
82 // empty name is not allowed
83 return nullptr;
84 }
85 HSTag* tag = new HSTag(name, this, settings_);
86 addIndexed(tag);
87 tag->name.changed().connect([this,tag]() {
88 this->onTagRename(tag);
89 tag->oldName_ = tag->name;
90 });
91 tag->needsRelayout_.connect([this,tag]() { this->needsRelayout_.emit(tag); });
92
93 Ewmh::get().updateDesktops();
94 Ewmh::get().updateDesktopNames();
95 tag_set_flags_dirty();
96 return tag;
97 }
98
tag_add_command(Input input,Output output)99 int TagManager::tag_add_command(Input input, Output output) {
100 if (input.empty()) {
101 return HERBST_NEED_MORE_ARGS;
102 }
103 if (input.front().empty()) {
104 output << input.command() << ": An empty tag name is not permitted\n";
105 return HERBST_INVALID_ARGUMENT;
106 }
107 HSTag* tag = add_tag(input.front());
108 hook_emit({"tag_added", tag->name});
109 return 0;
110 }
111
removeTag(Input input,Output output)112 int TagManager::removeTag(Input input, Output output) {
113 string tagNameToRemove;
114 if (!(input >> tagNameToRemove)) {
115 return HERBST_NEED_MORE_ARGS;
116 }
117 auto targetTagName = input.empty() ? get_current_monitor()->tag->name : input.front();
118
119 auto targetTag = find(targetTagName);
120 auto tagToRemove = find(tagNameToRemove);
121
122 if (!tagToRemove) {
123 output << input.command() << ": Tag \"" << tagNameToRemove << "\" not found\n";
124 return HERBST_INVALID_ARGUMENT;
125 }
126
127 if (!targetTag) {
128 output << input.command() << ": Tag \"" << targetTagName << "\" not found\n";
129 return HERBST_INVALID_ARGUMENT;
130 }
131
132 if (find_monitor_with_tag(tagToRemove)) {
133 output << input.command() << ": Cannot merge the currently viewed tag\n";
134 return HERBST_TAG_IN_USE;
135 }
136
137 // Prevent dangling tag_previous pointers
138 all_monitors_replace_previous_tag(tagToRemove, targetTag);
139
140 // Collect all clients in tag
141 vector<Client*> clients;
142 tagToRemove->foreachClient([&clients](Client* client) {
143 clients.push_back(client);
144 });
145
146 // Move clients to target tag
147 for (auto client : clients) {
148 client->tag()->removeClientSlice(client);
149 client->setTag(targetTag);
150 client->tag()->insertClientSlice(client);
151 targetTag->insertClient(client, {}, false);
152 }
153
154 // Ask target tag to make transferred clients visible if necessary
155 Monitor* monitor_target = find_monitor_with_tag(targetTag);
156 if (monitor_target) {
157 monitor_target->applyLayout();
158 targetTag->setVisible(true);
159 }
160
161 // Remove tag
162 string removedName = tagToRemove->name;
163 removeIndexed(index_of(tagToRemove));
164 Ewmh::get().updateCurrentDesktop();
165 Ewmh::get().updateDesktops();
166 Ewmh::get().updateDesktopNames();
167 tag_set_flags_dirty();
168 hook_emit({"tag_removed", removedName, targetTag->name()});
169
170 return HERBST_EXIT_SUCCESS;
171 }
172
tag_rename_command(Input input,Output output)173 int TagManager::tag_rename_command(Input input, Output output) {
174 string old_name, new_name;
175 if (!(input >> old_name >> new_name)) {
176 return HERBST_NEED_MORE_ARGS;
177 }
178 HSTag* tag = find(old_name);
179 if (!tag) {
180 output << input.command() << ": Tag \"" << old_name << "\" not found\n";
181 return HERBST_INVALID_ARGUMENT;
182 }
183 auto error = tag->name.change(new_name);
184 if (!error.empty()) {
185 output << input.command() << ": " << error << "\n";
186 return HERBST_INVALID_ARGUMENT;
187 }
188 return 0;
189 }
190
onTagRename(HSTag * tag)191 void TagManager::onTagRename(HSTag* tag) {
192 Ewmh::get().updateDesktopNames();
193 hook_emit({"tag_renamed", tag->oldName_, tag->name()});
194 }
195
ensure_tags_are_available()196 HSTag* TagManager::ensure_tags_are_available() {
197 if (size() > 0) {
198 return byIdx(0);
199 } else {
200 return add_tag("default");
201 }
202 }
203
byIndexStr(const string & index_str,bool skip_visible_tags)204 HSTag* TagManager::byIndexStr(const string& index_str, bool skip_visible_tags) {
205 int index;
206 try {
207 index = stoi(index_str);
208 } catch (...) {
209 return nullptr;
210 }
211 // index must be treated relative, if its first char is + or -
212 bool is_relative = index_str[0] == '+' || index_str[0] == '-';
213 Monitor* monitor = get_current_monitor();
214 if (is_relative) {
215 int current = index_of(monitor->tag);
216 int delta = index;
217 index = delta + current;
218 // ensure index is valid
219 index = MOD(index, size());
220 if (skip_visible_tags) {
221 HSTag* tag = byIdx(index);
222 for (size_t i = 0; find_monitor_with_tag(tag); i++) {
223 if (i >= size()) {
224 // if we tried each tag then there is no invisible tag
225 return nullptr;
226 }
227 index += delta;
228 index = MOD(index, size());
229 tag = byIdx(index);
230 }
231 }
232 } else {
233 // if it is absolute, then check index
234 if (index < 0 || (size_t)index >= size()) {
235 HSDebug("invalid tag index %d\n", index);
236 return nullptr;
237 }
238 }
239 return byIdx(index);
240 }
241
moveFocusedClient(HSTag * target)242 void TagManager::moveFocusedClient(HSTag* target) {
243 Client* client = monitors_->focus()->tag->focusedClient();
244 if (!client) {
245 return;
246 }
247 moveClient(client, target);
248 }
249
moveClient(Client * client,HSTag * target,string frameIndex,bool focus)250 void TagManager::moveClient(Client* client, HSTag* target, string frameIndex, bool focus) {
251 HSTag* tag_source = client->tag();
252 Monitor* monitor_source = find_monitor_with_tag(tag_source);
253 if (tag_source == target && frameIndex.empty()) {
254 // nothing to do
255 return;
256 }
257 Monitor* monitor_target = find_monitor_with_tag(target);
258 tag_source->removeClient(client);
259 // insert window into target
260 target->insertClient(client, frameIndex, focus);
261 if (tag_source != target) {
262 client->tag()->removeClientSlice(client);
263 client->setTag(target);
264 client->tag()->insertClientSlice(client);
265 }
266
267 // refresh things, hide things, layout it, and then show it if needed
268 if (monitor_source && !monitor_target) {
269 // window is moved to invisible tag
270 // so hide it
271 client->set_visible(false);
272 }
273 if (monitor_source) {
274 monitor_source->applyLayout();
275 }
276 if (monitor_target) {
277 monitor_target->applyLayout();
278 }
279 if (!monitor_source && monitor_target) {
280 client->set_visible(!client->minimized_());
281 }
282 tag_set_flags_dirty();
283 }
284
tag_move_window_command(Input argv,Output output)285 int TagManager::tag_move_window_command(Input argv, Output output) {
286 if (argv.empty()) {
287 return HERBST_NEED_MORE_ARGS;
288 }
289 HSTag* target = find(argv.front());
290 if (!target) {
291 output << argv.command() << ": Tag \"" << argv.front() << "\" not found\n";
292 return HERBST_INVALID_ARGUMENT;
293 }
294 moveFocusedClient(target);
295 return 0;
296 }
297
tag_move_window_by_index_command(Input argv,Output output)298 int TagManager::tag_move_window_by_index_command(Input argv, Output output) {
299 if (argv.empty()) {
300 return HERBST_NEED_MORE_ARGS;
301 }
302 auto tagIndex = argv.front();
303 argv.shift();
304 bool skip_visible = (!argv.empty() && argv.front() == "--skip-visible");
305
306 HSTag* tag = byIndexStr(tagIndex, skip_visible);
307 if (!tag) {
308 output << argv.command() << ": Invalid index \"" << tagIndex << "\"\n";
309 return HERBST_INVALID_ARGUMENT;
310 }
311 moveFocusedClient(tag);
312 return 0;
313 }
314
frameCommand(FrameCommand cmd)315 function<int(Input, Output)> TagManager::frameCommand(FrameCommand cmd) {
316 return [cmd,this](Input input, Output output) -> int {
317 return cmd(*(this->focus_()->frame()), input, output);
318 };
319 }
320
frameCommand(FrameCommand cmd,FrameCompleter completer)321 CommandBinding TagManager::frameCommand(FrameCommand cmd, FrameCompleter completer)
322 {
323 return { frameCommand(cmd), frameCompletion(completer) };
324 }
frameCommand(function<int (FrameTree &)> cmd)325 function<int()> TagManager::frameCommand(function<int(FrameTree&)> cmd) {
326 return [cmd,this]() -> int {
327 return cmd(*(this->focus_()->frame()));
328 };
329 }
330
frameCompletion(FrameCompleter completer)331 function<void(Completion&)> TagManager::frameCompletion(FrameCompleter completer) {
332 return [completer,this](Completion& complete) {
333 FrameTree* ft = focus_()->frame();
334 (ft ->* completer)(complete);
335 };
336 }
337
unusedTag()338 HSTag* TagManager::unusedTag() {
339 for (auto t : *this) {
340 if (!find_monitor_with_tag(&* t)) {
341 return t;
342 }
343 }
344 return nullptr;
345 }
346
updateFocusObject(Monitor * focusedMonitor)347 void TagManager::updateFocusObject(Monitor* focusedMonitor) {
348 focus_ = focusedMonitor->tag;
349 }
350
floatingCmd(Input input,Output output)351 int TagManager::floatingCmd(Input input, Output output) {
352 // usage: floating [[tag] on|off|toggle|status]
353 string newValue, tagName;
354 if (input.size() >= 2) {
355 input >> tagName >> newValue;
356 } else if (input.size() == 1) {
357 input >> newValue;
358 } else {
359 tagName = "";
360 }
361 if (newValue.empty()) {
362 newValue = "toggle";
363 }
364 HSTag* tag = monitors_->focus()->tag;
365 if (!tagName.empty()) {
366 tag = find(tagName);
367 if (!tag) {
368 output << input.command() << ": Tag \"" << tagName << "\" not found\n";
369 return HERBST_INVALID_ARGUMENT;
370 }
371 }
372 if (newValue == "status") {
373 output << (tag->floating ? "on" : "off");
374 } else {
375 string msg = tag->floating.change(newValue);
376 if (!msg.empty()) {
377 output << msg << "\n";
378 return HERBST_INVALID_ARGUMENT;
379 }
380 }
381 return 0;
382 }
383
floatingComplete(Completion & complete)384 void TagManager::floatingComplete(Completion &complete)
385 {
386 if (complete == 0) {
387 completeTag(complete);
388 }
389 if (complete == 0 || complete == 1) {
390 if (complete == 1 && !find(complete[0])) {
391 // if the first parameter is not a tag, then
392 // there can't be a second parameter
393 complete.none();
394 return;
395 }
396 complete.full("status");
397 // here, we pass a bool-pointer to the completion to get 'toggle' as one of the completion options
398 // This is much simpler than passing the actual floating state of the tag
399 bool b = true;
400 Converter<bool>::complete(complete, &b);
401 } else {
402 complete.none();
403 }
404 }
405