1 /*
2  * IceWM
3  *
4  * Copyright (C) 1997-2001 Marko Macek
5  */
6 #include "config.h"
7 #include "globit.h"
8 #include "yinputline.h"
9 #include "ymenu.h"
10 #include "yxapp.h"
11 #include "prefs.h"
12 #include "intl.h"
13 #include <X11/keysym.h>
14 
15 class YInputMenu: public YMenu {
16 public:
YInputMenu()17     YInputMenu() {
18         addItem(_("_Copy"), -2, _("Ctrl+C"), actionCopy);
19         addItem(_("Cu_t"), -2, _("Ctrl+X"), actionCut);
20         addItem(_("_Paste"), -2, _("Ctrl+V"), actionPaste);
21         addItem(_("Paste _Selection"), -2, _("Ctrl+P"), actionPasteSelection);
22         addSeparator();
23         addItem(_("Select _All"), -2, _("Ctrl+A"), actionSelectAll);
24     }
~YInputMenu()25     ~YInputMenu() {
26         if (xapp->popup() == this) {
27             xapp->popdown(this);
28         }
29     }
30 };
31 
YInputLine(YWindow * parent,YInputListener * listener)32 YInputLine::YInputLine(YWindow *parent, YInputListener *listener):
33     YWindow(parent),
34     markPos(0),
35     curPos(0),
36     leftOfs(0),
37     fAutoScrollDelta(0),
38     fHasFocus(false),
39     fCursorVisible(true),
40     fSelecting(false),
41     fBlinkTime(333),
42     fListener(listener),
43     inputFont(inputFontName),
44     inputBg(&clrInput),
45     inputFg(&clrInputText),
46     inputSelectionBg(&clrInputSelection),
47     inputSelectionFg(&clrInputSelectionText)
48 {
49     addStyle(wsNoExpose);
50     if (inputFont != null)
51         setSize(width(), inputFont->height() + 2);
52 }
53 
~YInputLine()54 YInputLine::~YInputLine() {
55 }
56 
setText(mstring text,bool asMarked)57 void YInputLine::setText(mstring text, bool asMarked) {
58     fText = text;
59     leftOfs = 0;
60     curPos = fText.length();
61     markPos = asMarked ? 0 : curPos;
62     limit();
63     repaint();
64 }
65 
getText()66 mstring YInputLine::getText() {
67     return fText;
68 }
69 
repaint()70 void YInputLine::repaint() {
71     if (width() > 1) {
72         GraphicsBuffer(this).paint();
73     }
74 }
75 
configure(const YRect2 & r)76 void YInputLine::configure(const YRect2& r) {
77     if (r.resized()) {
78         repaint();
79     }
80 }
81 
paint(Graphics & g,const YRect &)82 void YInputLine::paint(Graphics &g, const YRect &/*r*/) {
83     YFont font = inputFont;
84     int min, max, minOfs = 0, maxOfs = 0;
85     int textLen = fText.length();
86 
87     if (curPos > markPos) {
88         min = markPos;
89         max = curPos;
90     } else {
91         min = curPos;
92         max = markPos;
93     }
94 
95     if (curPos == markPos || fText == null || font == null || !fHasFocus) {
96         g.setColor(inputBg);
97         g.fillRect(0, 0, width(), height());
98     } else {
99         minOfs = font->textWidth(fText.data(), min) - leftOfs;
100         maxOfs = font->textWidth(fText.data(), max) - leftOfs;
101 
102         if (minOfs > 0) {
103             g.setColor(inputBg);
104             g.fillRect(0, 0, minOfs, height());
105         }
106         /// !!! optimize (0, width)
107         if (minOfs < maxOfs) {
108             g.setColor(inputSelectionBg);
109             g.fillRect(minOfs, 0, maxOfs - minOfs, height());
110         }
111         if (maxOfs < int(width())) {
112             g.setColor(inputBg);
113             g.fillRect(maxOfs, 0, width() - maxOfs, height());
114         }
115     }
116 
117     if (font != null) {
118         int yo = ::max(0, (int(height()) - int(font->height())) / 2);
119         int yp = font->ascent() + yo;
120         int curOfs = font->textWidth(fText.data(), curPos);
121         int cx = ::max(1, curOfs - leftOfs);
122 
123         g.setFont(font);
124 
125         if (curPos == markPos || !fHasFocus || fText == null) {
126             g.setColor(inputFg);
127             if (fText != null)
128                 g.drawChars(fText.data(), 0, textLen, -leftOfs, yp);
129             if (fHasFocus && fCursorVisible)
130                 g.drawLine(cx, yo, cx, font->height() + 2);
131         } else {
132             if (min > 0) {
133                 g.setColor(inputFg);
134                 g.drawChars(fText.data(), 0, min, -leftOfs, yp);
135             }
136             /// !!! same here
137             if (min < max) {
138                 g.setColor(inputSelectionFg);
139                 g.drawChars(fText.data(), min, max - min, minOfs, yp);
140             }
141             if (max < textLen) {
142                 g.setColor(inputFg);
143                 g.drawChars(fText.data(), max, textLen - max, maxOfs, yp);
144             }
145         }
146     }
147 }
148 
handleKey(const XKeyEvent & key)149 bool YInputLine::handleKey(const XKeyEvent &key) {
150     if (key.type == KeyPress) {
151         KeySym k = keyCodeToKeySym(key.keycode);
152 
153         switch (k) {
154         case XK_KP_Home:
155         case XK_KP_Up:
156         case XK_KP_Prior:
157         case XK_KP_Left:
158         case XK_KP_Begin:
159         case XK_KP_Right:
160         case XK_KP_End:
161         case XK_KP_Down:
162         case XK_KP_Next:
163         case XK_KP_Insert:
164         case XK_KP_Delete:
165             if (key.state & xapp->NumLockMask) {
166                 k = keyCodeToKeySym(key.keycode, 1);
167             }
168             break;
169         }
170 
171         int m = KEY_MODMASK(key.state);
172         bool extend = (m & ShiftMask) ? true : false;
173         unsigned textLen = fText.length();
174 
175         if (m & ControlMask) {
176             switch(k) {
177             case 'A':
178             case 'a':
179             case '/':
180                 selectAll();
181                 return true;
182 
183             case '\\':
184                 unselectAll();
185                 return true;
186             case 'u':
187             case 'U':
188                 if ( !deleteSelection())
189                     deleteToBegin();
190                 return true;
191             case 'v':
192             case 'V':
193                 requestSelection(false);
194                 return true;
195             case 'w':
196             case 'W':
197                 if ( !deleteSelection())
198                     deletePreviousWord();
199                 return true;
200             case 'X':
201             case 'x':
202                 cutSelection();
203                 return true;
204             case 'c':
205             case 'C':
206             case XK_Insert:
207             case XK_KP_Insert:
208                 copySelection();
209                 return true;
210             case 'i':
211             case 'I':
212                 complete();
213                 return true;
214             }
215         }
216         if (m & ShiftMask) {
217             switch (k) {
218             case XK_Insert:
219             case XK_KP_Insert:
220                 requestSelection(false);
221                 return true;
222             case XK_Delete:
223             case XK_KP_Delete:
224                 cutSelection();
225                 break;
226             }
227         }
228         switch (k) {
229         case XK_Left:
230         case XK_KP_Left:
231             if (m & ControlMask) {
232                 unsigned p = prevWord(curPos, false);
233                 if (p != curPos) {
234                     if (move(p, extend))
235                         return true;
236                 }
237             } else {
238                 if (curPos > 0) {
239                     if (move(curPos - 1, extend))
240                         return true;
241                 }
242             }
243             break;
244         case XK_Right:
245         case XK_KP_Right:
246             if (m & ControlMask) {
247                 unsigned p = nextWord(curPos, false);
248                 if (p != curPos) {
249                     if (move(p, extend))
250                         return true;
251                 }
252             } else {
253                 if (curPos < textLen) {
254                     if (move(curPos + 1, extend))
255                         return true;
256                 }
257             }
258             break;
259         case XK_Home:
260         case XK_KP_Home:
261             move(0, extend);
262             return true;
263         case XK_End:
264         case XK_KP_End:
265             move(textLen, extend);
266             return true;
267         case XK_Delete:
268         case XK_KP_Delete:
269         case XK_BackSpace:
270             if (hasSelection()) {
271                 if (deleteSelection())
272                     return true;
273             } else {
274                 switch (k) {
275                 case XK_Delete:
276                 case XK_KP_Delete:
277                     if (m & ControlMask) {
278                         if (m & ShiftMask) {
279                             if (deleteToEnd())
280                                 return true;
281                         } else {
282                             if (deleteNextWord())
283                                 return true;
284                         }
285                     } else {
286                         if (deleteNextChar())
287                             return true;
288                     }
289                     break;
290                 case XK_BackSpace:
291                     if (m & ControlMask) {
292                         if (m & ShiftMask) {
293                             if (deleteToBegin())
294                                 return true;
295                         } else {
296                             if (deletePreviousWord())
297                                 return true;
298                         }
299                     } else {
300                         if (deletePreviousChar())
301                             return true;
302                     }
303                     break;
304                 }
305             }
306             break;
307         case XK_Tab:
308             complete();
309             break;
310         default:
311             if (fListener &&
312                 ((k == XK_Return && m == 0) ||
313                  (k == XK_KP_Enter && m == 0) ||
314                  (k == XK_j && m == ControlMask) ||
315                  (k == XK_m && m == ControlMask)))
316             {
317                 fListener->inputReturn(this);
318                 return true;
319             }
320             else if (fListener && (k == XK_Escape && m == 0))
321             {
322                 fListener->inputEscape(this);
323                 return true;
324             }
325             else
326             {
327                 char s[16];
328 
329                 if (getCharFromEvent(key, s, sizeof(s))) {
330                     replaceSelection(s, strlen(s));
331                     return true;
332                 }
333             }
334         }
335     }
336     return YWindow::handleKey(key);
337 }
338 
handleButton(const XButtonEvent & button)339 void YInputLine::handleButton(const XButtonEvent &button) {
340     if (button.type == ButtonPress) {
341         if (button.button == 1) {
342             if (fHasFocus == false) {
343                 setWindowFocus();
344                 requestFocus(false);
345             } else {
346                 fSelecting = true;
347                 curPos = markPos = offsetToPos(button.x + leftOfs);
348                 limit();
349                 repaint();
350             }
351         }
352     } else if (button.type == ButtonRelease) {
353         autoScroll(0, nullptr);
354         if (fSelecting && button.button == 1) {
355             fSelecting = false;
356             //curPos = offsetToPos(button.x + leftOfs);
357             //limit();
358             repaint();
359         }
360     }
361     YWindow::handleButton(button);
362 }
363 
handleMotion(const XMotionEvent & motion)364 void YInputLine::handleMotion(const XMotionEvent &motion) {
365     if (fSelecting && (motion.state & Button1Mask)) {
366         if (motion.x < 0)
367             autoScroll(-8, &motion); // fix
368         else if (motion.x >= int(width()))
369             autoScroll(8, &motion); // fix
370         else {
371             autoScroll(0, &motion);
372             unsigned c = offsetToPos(motion.x + leftOfs);
373             if (getClickCount() == 2) {
374                 if (c >= markPos) {
375                     if (markPos > curPos)
376                         markPos = curPos;
377                     c = nextWord(c, true);
378                 } else if (c < markPos) {
379                     if (markPos < curPos)
380                         markPos = curPos;
381                     c = prevWord(c, true);
382                 }
383             }
384             if (curPos != c) {
385                 curPos = c;
386                 limit();
387                 repaint();
388             }
389         }
390     }
391     YWindow::handleMotion(motion);
392 }
393 
handleClickDown(const XButtonEvent & down,int count)394 void YInputLine::handleClickDown(const XButtonEvent &down, int count) {
395     if (down.button == 1) {
396         if ((count % 4) == 2) {
397             unsigned l = prevWord(curPos, true);
398             unsigned r = nextWord(curPos, true);
399             if (l != markPos || r != curPos) {
400                 markPos = l;
401                 curPos = r;
402                 limit();
403                 repaint();
404             }
405         } else if ((count % 4) == 3) {
406             markPos = curPos = 0;
407             curPos = fText.length();
408             fSelecting = false;
409             limit();
410             repaint();
411         }
412     }
413 }
414 
handleClick(const XButtonEvent & up,int)415 void YInputLine::handleClick(const XButtonEvent &up, int /*count*/) {
416     if (up.button == 3 && xapp->isButton(up.state, Button3Mask)) {
417         inputMenu->setActionListener(this);
418         inputMenu->popup(this, nullptr, this, up.x_root, up.y_root,
419                          YPopupWindow::pfCanFlipVertical |
420                          YPopupWindow::pfCanFlipHorizontal);
421         inputMenu->setPopDownListener(this);
422     } else if (up.button == 2 && xapp->isButton(up.state, Button2Mask)) {
423         requestSelection(true);
424     }
425 }
426 
handleSelection(const XSelectionEvent & selection)427 void YInputLine::handleSelection(const XSelectionEvent &selection) {
428     if (selection.property != None) {
429         YProperty prop(selection.requestor, selection.property,
430                        F8, 32 * 1024, selection.target, True);
431         if (prop) {
432             replaceSelection(prop.data<char>(), prop.size());
433         }
434     }
435 }
436 
offsetToPos(int offset)437 unsigned YInputLine::offsetToPos(int offset) {
438     YFont font = inputFont;
439     int ofs = 0, pos = 0;
440     int textLen = fText.length();
441 
442     if (font != null) {
443         while (pos < textLen) {
444             ofs += font->textWidth(fText.data() + pos, 1);
445             if (ofs < offset)
446                 pos++;
447             else
448                 break;
449         }
450     }
451     return pos;
452 }
453 
handleFocus(const XFocusChangeEvent & focus)454 void YInputLine::handleFocus(const XFocusChangeEvent &focus) {
455     if (focus.type == FocusIn /* && fHasFocus == false*/
456         && focus.detail != NotifyPointer
457         && focus.detail != NotifyPointerRoot)
458     {
459         fHasFocus = true;
460         selectAll();
461         cursorBlinkTimer->setTimer(fBlinkTime, this, true);
462     }
463     else if (focus.type == FocusOut/* && fHasFocus == true*/) {
464         fHasFocus = false;
465         repaint();
466         cursorBlinkTimer = null;
467         if (inputMenu && inputMenu == xapp->popup()) {
468         }
469         else if (fListener) {
470             fListener->inputLostFocus(this);
471         }
472     }
473 }
474 
handlePopDown(YPopupWindow * popup)475 void YInputLine::handlePopDown(YPopupWindow *popup) {
476     inputMenu = null;
477 }
478 
handleAutoScroll(const XMotionEvent &)479 bool YInputLine::handleAutoScroll(const XMotionEvent & /*mouse*/) {
480     curPos += fAutoScrollDelta;
481     leftOfs += fAutoScrollDelta;
482     int c = curPos;
483 
484     if (fAutoScrollDelta < 0)
485         c = offsetToPos(leftOfs);
486     else if (fAutoScrollDelta > 0)
487         c = offsetToPos(leftOfs + width());
488     curPos = c;
489     limit();
490     repaint();
491     return true;
492 }
493 
handleTimer(YTimer * t)494 bool YInputLine::handleTimer(YTimer *t) {
495     if (t == cursorBlinkTimer) {
496         fCursorVisible ^= true;
497         repaint();
498         return fHasFocus;
499     }
500     return false;
501 }
502 
move(unsigned pos,bool extend)503 bool YInputLine::move(unsigned pos, bool extend) {
504     unsigned textLen = fText.length();
505 
506     if (curPos > textLen)
507         return false;
508 
509     if (curPos != pos || (!extend && curPos != markPos)) {
510 
511         curPos = pos;
512         if (!extend)
513             markPos = curPos;
514 
515         limit();
516         repaint();
517     }
518     return true;
519 }
520 
limit()521 void YInputLine::limit() {
522     unsigned textLen = fText.length();
523 
524     if (curPos > textLen)
525         curPos = textLen;
526     if (markPos > textLen)
527         markPos = textLen;
528 
529     YFont font = inputFont;
530     if (font != null) {
531         int curOfs = font->textWidth(fText.data(), curPos);
532         int curLen = font->textWidth(fText.data(), textLen);
533 
534         if (curOfs >= leftOfs + int(width()) + 1)
535             leftOfs = curOfs - width() + 2;
536         if (curOfs < leftOfs)
537             leftOfs = curOfs;
538         if (leftOfs + int(width()) + 1 > curLen)
539             leftOfs = curLen - width() + 1;
540         if (leftOfs < 0)
541             leftOfs = 0;
542     }
543 }
544 
replaceSelection(const char * insert,int amount)545 void YInputLine::replaceSelection(const char* insert, int amount) {
546     unsigned from = min(curPos, markPos);
547     unsigned to = max(curPos, markPos);
548     YWideString wide(insert, amount);
549     fText.replace(from, to - from, wide);
550     curPos = markPos = from + wide.length();
551     limit();
552     repaint();
553 }
554 
deleteSelection()555 bool YInputLine::deleteSelection() {
556     if (hasSelection()) {
557         replaceSelection("", 0);
558         return true;
559     }
560     return false;
561 }
562 
deleteNextChar()563 bool YInputLine::deleteNextChar() {
564     unsigned textLen = fText.length();
565 
566     if (curPos < textLen) {
567         markPos = curPos + (curPos < UINT_MAX);
568         deleteSelection();
569         return true;
570     }
571     return false;
572 }
573 
deletePreviousChar()574 bool YInputLine::deletePreviousChar() {
575     if (curPos > 0) {
576         markPos = curPos - 1;
577         deleteSelection();
578         return true;
579     }
580     return false;
581 }
582 
583 #define CHCLASS(c) ((c) == ' ')
584 
nextWord(unsigned p,bool sep)585 unsigned YInputLine::nextWord(unsigned p, bool sep) {
586     unsigned textLen = fText.length();
587 
588     while (p < textLen &&
589            (CHCLASS(fText.charAt(p)) == CHCLASS(fText.charAt(p + 1)) ||
590             (!sep && CHCLASS(fText.charAt(p)))))
591     {
592         p++;
593     }
594     if (p < textLen)
595         p++;
596     return p;
597 }
598 
prevWord(unsigned p,bool sep)599 unsigned YInputLine::prevWord(unsigned p, bool sep) {
600     if (p > 0 && !sep)
601         p--;
602     while (p > 0 &&
603            (CHCLASS(fText.charAt(p)) == CHCLASS(fText.charAt(p - 1)) ||
604             (!sep && CHCLASS(fText.charAt(p)))))
605     {
606         p--;
607     }
608     return p;
609 }
610 
deleteNextWord()611 bool YInputLine::deleteNextWord() {
612     unsigned p = nextWord(curPos, false);
613     if (p != curPos) {
614         markPos = p;
615         return deleteSelection();
616     }
617     return false;
618 }
619 
deletePreviousWord()620 bool YInputLine::deletePreviousWord() {
621     unsigned p = prevWord(curPos, false);
622     if (p != curPos) {
623         markPos = p;
624         return deleteSelection();
625     }
626     return false;
627 }
628 
deleteToEnd()629 bool YInputLine::deleteToEnd() {
630     unsigned textLen = fText.length();
631 
632     if (curPos < textLen) {
633         markPos = textLen;
634         return deleteSelection();
635     }
636     return false;
637 }
638 
deleteToBegin()639 bool YInputLine::deleteToBegin() {
640     if (curPos > 0) {
641         markPos = 0;
642         return deleteSelection();
643     }
644     return false;
645 }
646 
selectAll()647 void YInputLine::selectAll() {
648     markPos = curPos = 0;
649     curPos = fText.length();
650     fSelecting = false;
651     limit();
652     repaint();
653 }
654 
unselectAll()655 void YInputLine::unselectAll() {
656     fSelecting = false;
657     if (markPos != curPos) {
658         markPos = curPos;
659         repaint();
660     }
661 }
662 
cutSelection()663 bool YInputLine::cutSelection() {
664     return copySelection() && deleteSelection();
665 }
666 
copySelection()667 bool YInputLine::copySelection() {
668     unsigned min = ::min(curPos, markPos), max = ::max(curPos, markPos);
669     if (min < max && fText.length() <= max) {
670         YWideString copy(fText.copy(min, max - min));
671         xapp->setClipboardText(copy);
672         return true;
673     } else {
674         return false;
675     }
676 }
677 
actionPerformed(YAction action,unsigned int)678 void YInputLine::actionPerformed(YAction action, unsigned int /*modifiers*/) {
679     if (action == actionSelectAll)
680         selectAll();
681     else if (action == actionPaste)
682         requestSelection(false);
683     else if (action == actionPasteSelection)
684         requestSelection(true);
685     else if (action == actionCopy)
686         copySelection();
687     else if (action == actionCut)
688         cutSelection();
689 }
690 
autoScroll(int delta,const XMotionEvent * motion)691 void YInputLine::autoScroll(int delta, const XMotionEvent *motion) {
692     fAutoScrollDelta = delta;
693     beginAutoScroll(delta ? true : false, motion);
694 }
695 
complete()696 void YInputLine::complete() {
697     char* res = nullptr;
698     mstring mstr(fText);
699     int res_count = globit_best(mstr, &res, nullptr, nullptr);
700     // directory is not a final match
701     if (res_count == 1 && upath(res).dirExists())
702         res_count++;
703     if (1 <= res_count)
704         setText(res, res_count == 1);
705     free(res);
706 }
707 
isFocusTraversable()708 bool YInputLine::isFocusTraversable() {
709     return true;
710 }
711 
gotFocus()712 void YInputLine::gotFocus() {
713     if (fHasFocus == false) {
714         fHasFocus = true;
715         fCursorVisible = true;
716         cursorBlinkTimer->setTimer(fBlinkTime, this, true);
717         repaint();
718     }
719 }
720 
lostFocus()721 void YInputLine::lostFocus() {
722     if (cursorBlinkTimer) {
723         cursorBlinkTimer = null;
724         fHasFocus = false;
725         repaint();
726     }
727 }
728 
729 // vim: set sw=4 ts=4 et:
730