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