1 //
2 // InputDialog.cc for pekwm
3 // Copyright (C) 2009-2020 Claes Nästén <pekdon@gmail.com>
4 //
5 // This program is licensed under the GNU GPL.
6 // See the LICENSE file for more information.
7 //
8 
9 #include "config.h"
10 
11 #include <fstream>
12 #include <algorithm>
13 
14 #include "Charset.hh"
15 #include "InputDialog.hh"
16 #include "KeyGrabber.hh"
17 #include "X11.hh"
18 #include "Workspaces.hh"
19 
20 extern "C" {
21 #include <X11/Xutil.h>
22 }
23 
24 std::map<KeySym, std::string> InputDialog::_keysym_map;
25 
InputBuffer(void)26 InputBuffer::InputBuffer(void)
27 	: _pos(0)
28 {
29 }
30 
InputBuffer(const std::string & buf,int pos)31 InputBuffer::InputBuffer(const std::string& buf, int pos)
32 	: _buf(buf)
33 {
34 	if (pos == -1) {
35 		_pos = buf.size();
36 	} else {
37 		_pos = pos;
38 	}
39 }
40 
~InputBuffer(void)41 InputBuffer::~InputBuffer(void)
42 {
43 }
44 
45 /**
46  * Adds str to buffer
47  */
48 void
add(const std::string & buf)49 InputBuffer::add(const std::string& buf)
50 {
51 	for (size_t i = 0; i < buf.size(); i++) {
52 		_buf.insert(_buf.begin() + _pos++, buf[i]);
53 	}
54 }
55 
56 /**
57  * Removes char at current position from buffer
58  */
59 void
remove(void)60 InputBuffer::remove(void)
61 {
62 	if (_pos == 0 || _pos > _buf.size() || _buf.empty()) {
63 		return;
64 	}
65 
66 	Charset::Utf8Iterator it(_buf, _pos);
67 	--it;
68 	_buf.erase(it.pos(), _pos - it.pos());
69 	_pos = it.pos();
70 }
71 
72 void
clear(void)73 InputBuffer::clear(void)
74 {
75 	_buf = "";
76 	_pos = 0;
77 }
78 
79 /**
80  * Removes buffer content after position
81  */
82 void
kill(void)83 InputBuffer::kill(void)
84 {
85 	_buf.resize(_pos);
86 }
87 
88 /**
89  * Moves the marker
90  */
91 void
changePos(int off)92 InputBuffer::changePos(int off)
93 {
94 	Charset::Utf8Iterator it(_buf, _pos);
95 	if (off > 0) {
96 		for (; off > 0; off--) {
97 			++it;
98 		}
99 	} else if (off < 0) {
100 		for (; off < 0; off++) {
101 			--it;
102 		}
103 	}
104 	_pos = it.pos();
105 }
106 
107 
108 /**
109  * InputDialog constructor.
110  */
InputDialog(const std::string & title)111 InputDialog::InputDialog(const std::string &title)
112 	: PDecor("INPUTDIALOG"), PWinObjReference(0),
113 	  _data(pekwm::theme()->getCmdDialogData()),
114 	  _buf_off(0),
115 	  _buf_chars(0)
116 {
117 	// PWinObj attributes
118 	setLayer(LAYER_NONE); // hack, goes over LAYER_MENU
119 	_hidden = true; // don't care about it when changing worskpace etc
120 
121 	if (! _keysym_map.size()) {
122 		reloadKeysymMap();
123 	}
124 
125 	// Add action to list, going to be used from close and exec
126 	::Action action;
127 	_ae.action_list.push_back(action);
128 
129 	titleAdd(&_title);
130 	titleSetActive(0);
131 	setTitle(title);
132 
133 	_text_wo = new PWinObj(true);
134 	XSetWindowAttributes attr;
135 	attr.override_redirect = false;
136 	attr.event_mask = ButtonPressMask|ButtonReleaseMask|ButtonMotionMask|
137 		FocusChangeMask|KeyPressMask|KeyReleaseMask;
138 	_text_wo->setWindow(X11::createWindow(_window, 0, 0, 1, 1, 0,
139 					      CopyFromParent, InputOutput,
140 					      CopyFromParent,
141 					      CWOverrideRedirect|CWEventMask,
142 					      &attr));
143 
144 	addChild(_text_wo);
145 	addChildWindow(_text_wo->getWindow());
146 	activateChild(_text_wo);
147 	_text_wo->mapWindow();
148 
149 	// setup texture, size etc
150 	loadTheme();
151 
152 	Workspaces::insert(this);
153 	woListAdd(this);
154 	_wo_map[_window] = this;
155 }
156 
157 /**
158  * InputDialog destructor.
159  */
~InputDialog(void)160 InputDialog::~InputDialog(void)
161 {
162 	Workspaces::remove(this);
163 	_wo_map.erase(_window);
164 	woListRemove(this);
165 
166 	// Free resources
167 	if (_text_wo) {
168 		_children.erase(std::remove(_children.begin(), _children.end(),
169 					    _text_wo),
170 				_children.end());
171 		removeChildWindow(_text_wo->getWindow());
172 		X11::destroyWindow(_text_wo->getWindow());
173 		delete _text_wo;
174 	}
175 
176 	unloadTheme();
177 }
178 
179 void
reloadKeysymMap(void)180 InputDialog::reloadKeysymMap(void)
181 {
182 	_keysym_map.clear();
183 
184 	addKeysymToKeysymMap(XK_KP_0, "0");
185 	addKeysymToKeysymMap(XK_KP_1, "1");
186 	addKeysymToKeysymMap(XK_KP_2, "2");
187 	addKeysymToKeysymMap(XK_KP_3, "3");
188 	addKeysymToKeysymMap(XK_KP_4, "4");
189 	addKeysymToKeysymMap(XK_KP_5, "5");
190 	addKeysymToKeysymMap(XK_KP_6, "6");
191 	addKeysymToKeysymMap(XK_KP_7, "7");
192 	addKeysymToKeysymMap(XK_KP_8, "8");
193 	addKeysymToKeysymMap(XK_KP_9, "9");
194 }
195 
196 void
addKeysymToKeysymMap(KeySym keysym,const std::string & chr)197 InputDialog::addKeysymToKeysymMap(KeySym keysym, const std::string& chr)
198 {
199 	Display *dpy = X11::getDpy();
200 
201 	int keysyms_per_keycode;
202 	KeyCode keycode = XKeysymToKeycode(dpy, keysym);
203 	if (! keycode) {
204 		return;
205 	}
206 
207 	KeySym *keysyms =
208 		XGetKeyboardMapping(dpy, keycode, 1, &keysyms_per_keycode);
209 	if (keysyms) {
210 		for (int i = 0; i < keysyms_per_keycode; i++) {
211 			if (keysyms[i] != NoSymbol) {
212 				_keysym_map[keysyms[i]] = chr;
213 			}
214 		}
215 
216 		X11::free(keysyms);
217 	}
218 }
219 
220 /**
221  * Handles ButtonPress, moving the text cursor
222  */
223 ActionEvent*
handleButtonPress(XButtonEvent * ev)224 InputDialog::handleButtonPress(XButtonEvent *ev)
225 {
226 	if (*_text_wo == ev->window) {
227 		// FIXME: move cursor
228 		return 0;
229 	} else {
230 		return PDecor::handleButtonPress(ev);
231 	}
232 }
233 
234 /**
235  * Handles KeyPress, editing the buffer
236  */
237 ActionEvent*
handleKeyPress(XKeyEvent * ev)238 InputDialog::handleKeyPress(XKeyEvent *ev)
239 {
240 	bool matched;
241 	ActionEvent *c_ae, *ae = 0;
242 
243 	if ((c_ae = pekwm::keyGrabber()->findAction(ev, _type, matched))) {
244 		ActionEvent::it it(c_ae->action_list.begin());
245 		for (; it != c_ae->action_list.end(); ++it) {
246 			switch (it->getAction()) {
247 			case INPUT_INSERT:
248 				bufAdd(ev);
249 				completeReset();
250 				break;
251 			case INPUT_REMOVE:
252 				_buf.remove();
253 				break;
254 			case INPUT_CLEAR:
255 				bufClear();
256 				break;
257 			case INPUT_CLEARFROMCURSOR:
258 				_buf.kill();
259 				completeReset();
260 				break;
261 			case INPUT_EXEC:
262 				ae = exec();
263 				break;
264 			case INPUT_CLOSE:
265 				ae = close();
266 				break;
267 			case INPUT_COMPLETE:
268 				complete();
269 				break;
270 			case INPUT_COMPLETE_ABORT:
271 				completeAbort();
272 				break;
273 			case INPUT_CURS_NEXT:
274 				_buf.changePos(1);
275 				completeReset();
276 				break;
277 			case INPUT_CURS_PREV:
278 				_buf.changePos(-1);
279 				completeReset();
280 				break;
281 			case INPUT_CURS_BEGIN:
282 				_buf.setPos(0);
283 				completeReset();
284 				break;
285 			case INPUT_CURS_END:
286 				_buf.setPos(_buf.size());
287 				completeReset();
288 				break;
289 			case INPUT_HIST_NEXT:
290 				histNext();
291 				completeReset();
292 				break;
293 			case INPUT_HIST_PREV:
294 				histPrev();
295 				completeReset();
296 				break;
297 			case INPUT_NO_ACTION:
298 			default:
299 				// do nothing, shouldn't happen
300 				break;
301 			};
302 		}
303 
304 		// something ( most likely ) changed, redraw the window
305 		if (! ae) {
306 			bufChanged();
307 			render();
308 		}
309 	}
310 
311 	return ae;
312 }
313 
314 /**
315  * Handles ExposeEvent, redraw when ev->count == 0
316  */
317 ActionEvent*
handleExposeEvent(XExposeEvent * ev)318 InputDialog::handleExposeEvent(XExposeEvent *ev)
319 {
320 	if (ev->count > 0) {
321 		return 0;
322 	}
323 	render();
324 	return 0;
325 }
326 
327 /**
328  * Maps the InputDialog center on the PWinObj it executes actions on.
329  *
330  * @param buf Buffer content.
331  * @param wo_ref PWinObj reference, defaults to 0 which does not update.
332  */
333 void
mapCentered(const std::string & buf,const Geometry & gm,PWinObj * wo_ref)334 InputDialog::mapCentered(const std::string &buf, const Geometry &gm,
335                          PWinObj *wo_ref)
336 {
337 	// Setup data
338 	_hist_it = _history.end();
339 
340 	_buf.setBuf(buf);
341 	_buf.setPos(buf.size());
342 	bufChanged();
343 
344 	Geometry head;
345 	uint head_nr = X11::getNearestHead(gm.x + (gm.width / 2),
346 					   gm.y + (gm.height / 2));
347 	X11::getHeadInfo(head_nr, head);
348 
349 	// Update size (before, as we center) and position
350 	updateSize(head);
351 	moveCentered(head, gm);
352 
353 	// Map and render
354 	PDecor::mapWindowRaised();
355 	render();
356 
357 	giveInputFocus();
358 }
359 
360 /**
361  * Moves to center of geometry.
362  *
363  * @param gm Geometry to center on.
364  */
365 void
moveCentered(const Geometry & head,const Geometry & gm)366 InputDialog::moveCentered(const Geometry &head, const Geometry &gm)
367 {
368 	// Make sure X is inside head.
369 	int new_x =
370 		gm.x + (static_cast<int>(gm.width) - static_cast<int>(_gm.width)) / 2;
371 	if (new_x < head.x) {
372 		new_x = head.x;
373 	} else if ((new_x + _gm.width) > (head.x + head.width)) {
374 		new_x = head.x + head.width - _gm.width;
375 	}
376 
377 	// Make sure Y is inside head.
378 	int new_y =
379 		gm.y + (static_cast<int>(gm.height) - static_cast<int>(_gm.height)) / 2;
380 	if (new_y < head.y) {
381 		new_y = head.y;
382 	} else if ((new_y + _gm.height) > (head.y + head.height)) {
383 		new_y = head.y + head.height - _gm.height;
384 	}
385 
386 	// Update position.
387 	move(new_x, new_y);
388 }
389 
390 /**
391  * Sets title of decor
392  */
393 void
setTitle(const std::string & title)394 InputDialog::setTitle(const std::string &title)
395 {
396 	_title.setReal(title);
397 }
398 
399 /**
400  * Maps window, overloaded to refresh content of window after mapping.
401  */
402 void
mapWindow(void)403 InputDialog::mapWindow(void)
404 {
405 	if (! _mapped) {
406 		Geometry head;
407 		X11::getHeadInfo(getHead(), head);
408 
409 		// Correct size for current head before mapping
410 		updateSize(head);
411 
412 		PDecor::mapWindow();
413 		render();
414 	}
415 }
416 
417 /**
418  * Sets background and size
419  */
420 void
loadTheme(void)421 InputDialog::loadTheme(void)
422 {
423 	Geometry head;
424 	X11::getHeadInfo(getHead(), head);
425 
426 	_data = pekwm::theme()->getCmdDialogData();
427 	updateSize(head);
428 	updatePixmapSize();
429 }
430 
431 /**
432  * Frees resources
433  */
434 void
unloadTheme(void)435 InputDialog::unloadTheme(void)
436 {
437 }
438 
439 /**
440  * Renders _buf onto _text_wo
441  */
442 void
render(void)443 InputDialog::render(void)
444 {
445 	X11::clearWindow(_text_wo->getWindow());
446 
447 	uint pos = _data->getPad(PAD_LEFT);
448 	const char *buf = str().c_str() + _buf_off;
449 
450 	// draw buf content
451 	_data->getFont()->setColor(_data->getColor());
452 	_data->getFont()->draw(_text_wo->getWindow(),
453 			       pos, _data->getPad(PAD_UP), buf, _buf_chars);
454 
455 	// draw cursor
456 	if (_buf.pos() > 0) {
457 		pos += _data->getFont()->getWidth(buf,  _buf.pos() - _buf_off) + 1;
458 	}
459 	_data->getFont()->draw(_text_wo->getWindow(),
460 			       pos, _data->getPad(PAD_UP), "|");
461 }
462 
463 /**
464  * Generates ACTION_CLOSE closing dialog.
465  *
466  * @return Pointer to ActionEvent.
467  */
468 ActionEvent*
close(void)469 InputDialog::close(void)
470 {
471 	_ae.action_list.back().setAction(ACTION_NO);
472 	return &_ae;
473 }
474 
475 void
complete(void)476 InputDialog::complete(void)
477 {
478 }
479 
480 void
completeAbort(void)481 InputDialog::completeAbort(void)
482 {
483 }
484 
485 void
completeReset(void)486 InputDialog::completeReset(void)
487 {
488 }
489 
490 /**
491  * Adds char to buffer
492  */
493 void
bufAdd(XKeyEvent * ev)494 InputDialog::bufAdd(XKeyEvent *ev)
495 {
496 	KeySym keysym;
497 	char c_return[64] = {0};
498 	XLookupString(ev, c_return, sizeof(c_return), &keysym, 0);
499 	if (_keysym_map.count(keysym)) {
500 		_buf.add(_keysym_map[keysym]);
501 	} else {
502 		_buf.add(c_return);
503 	}
504 }
505 
506 
507 /**
508  * Clears the buffer, resets status
509  */
510 void
bufClear(void)511 InputDialog::bufClear(void)
512 {
513 	_buf_off = 0;
514 	_buf_chars = 0;
515 
516 	_buf.clear();
517 	completeReset();
518 }
519 
520 /**
521  * Recalculates, _buf_off and _buf_chars
522  */
523 void
bufChanged(void)524 InputDialog::bufChanged(void)
525 {
526 	PFont *font =  _data->getFont(); // convenience
527 
528 	// complete string does not fit in the window OR the first set
529 	// does not fit
530 	if (_buf.pos() > 0
531 	    && (font->getWidth(str()) > _text_wo->getWidth())
532 	    && (font->getWidth(str(), _buf.pos()) > _text_wo->getWidth())) {
533 
534 		// increase position until it all fits
535 		Charset::Utf8Iterator it(_buf.str(), 0);
536 		for (; it.pos() < _buf.pos(); ++it) {
537 			if (font->getWidth(_buf.str().c_str() + it.pos(), _buf.size() - it.pos())
538 			    < _text_wo->getWidth()) {
539 				break;
540 			}
541 		}
542 
543 		_buf_chars = _buf.size() - it.pos();
544 	} else {
545 		_buf_off = 0;
546 		_buf_chars = _buf.size();
547 	}
548 }
549 
550 /**
551  * Sets the buffer to the next item in the history.
552  */
553 void
histNext(void)554 InputDialog::histNext(void)
555 {
556 	if (_hist_it == _history.end()) {
557 		return; // nothing to do
558 	}
559 
560 	// get next item, if at the end, restore the edit buffer
561 	++_hist_it;
562 	if (_hist_it == _history.end()) {
563 		_buf.setBuf(_hist_new);
564 	} else {
565 		_buf.setBuf(*_hist_it);
566 	}
567 
568 	// move cursor to the end of line
569 	_buf.setPos(_buf.str().size());
570 }
571 
572 /**
573  * Sets the buffer to the previous item in the history.
574  */
575 void
histPrev(void)576 InputDialog::histPrev(void)
577 {
578 	if (_hist_it == _history.begin()) {
579 		return; // nothing to do
580 	}
581 
582 	// save item so we can restore the edit buffer later
583 	if (_hist_it == _history.end()) {
584 		_hist_new = _buf.str();
585 	}
586 
587 	// get prev item
588 	_buf.setBuf(*(--_hist_it));
589 
590 	// move cursor to the end of line
591 	_buf.setPos(_buf.size());
592 }
593 
594 /**
595  * Update command dialog size for view on current head.
596  */
597 void
updateSize(const Geometry & head)598 InputDialog::updateSize(const Geometry &head)
599 {
600 	// Resize the child window and update the size depending.
601 	uint old_width = _gm.width;
602 
603 	unsigned int width, height;
604 	getInputSize(head, width, height);
605 	resizeChild(width, height);
606 
607 	// If size was updated, replace the texture and recalculate display
608 	// buffer.
609 	if (old_width != _gm.width) {
610 		updatePixmapSize();
611 		bufChanged();
612 	}
613 }
614 
615 /**
616  * Update background pixmap size and redraw.
617  */
618 void
updatePixmapSize(void)619 InputDialog::updatePixmapSize(void)
620 {
621 	PTexture *tex = _data->getTexture();
622 	tex->setBackground(_text_wo->getWindow(),
623 			   0, 0, _text_wo->getWidth(), _text_wo->getHeight());
624 	X11::clearWindow(_text_wo->getWindow());
625 }
626 
627 /**
628  * Get size of the text input widget.
629  *
630  * @param width Fill in width.
631  * @param height Fill in height.
632  */
633 void
getInputSize(const Geometry & head,unsigned int & width,unsigned int & height)634 InputDialog::getInputSize(const Geometry &head,
635                           unsigned int &width, unsigned int &height)
636 {
637 	width = head.width / 3;
638 	height = _data->getFont()->getHeight()
639 		+ _data->getPad(PAD_UP) + _data->getPad(PAD_DOWN);
640 }
641 
642 void
addHistory(const std::string & entry,bool unique,uint max_size)643 InputDialog::addHistory(const std::string& entry, bool unique, uint max_size)
644 {
645 	if (unique) {
646 		addHistoryUnique(entry);
647 	} else {
648 		_history.push_back(entry);
649 	}
650 
651 	if (_history.size() > max_size) {
652 		_history.erase(_history.begin());
653 	}
654 }
655 
656 void
addHistoryUnique(const std::string & entry)657 InputDialog::addHistoryUnique(const std::string& entry)
658 {
659 	std::vector<std::string>::iterator it =
660 		find(_history.begin(), _history.end(), entry);
661 	if (it != _history.end()) {
662 		_history.erase(it);
663 	}
664 
665 	_history.push_back(entry);
666 }
667 
668 void
loadHistory(const std::string & path)669 InputDialog::loadHistory(const std::string &path)
670 {
671 	std::ifstream ifile(path.c_str());
672 	if (ifile.is_open()) {
673 		// Update only path if successfully opened.
674 		std::string mb_line;
675 		while (ifile.good()) {
676 			getline(ifile, mb_line);
677 			if (mb_line.size()) {
678 				_history.push_back(mb_line);
679 			}
680 		}
681 		ifile.close();
682 	}
683 }
684 
685 void
saveHistory(const std::string & path)686 InputDialog::saveHistory(const std::string &path)
687 {
688 	std::ofstream ofile(path.c_str());
689 	if (ofile.is_open()) {
690 		std::vector<std::string>::iterator it = _history.begin();
691 		for (; it != _history.end(); ++it) {
692 			ofile << *it << "\n";
693 		}
694 		ofile.close();
695 	}
696 }
697