1 /**
2  * @file pager.c
3  * @author Joe Wingbermuehle
4  * @date 2004-2007
5  *
6  * @brief Pager tray component.
7  *
8  */
9 
10 #include "jwm.h"
11 #include "pager.h"
12 
13 #include "client.h"
14 #include "clientlist.h"
15 #include "color.h"
16 #include "cursor.h"
17 #include "desktop.h"
18 #include "event.h"
19 #include "tray.h"
20 #include "timing.h"
21 #include "popup.h"
22 #include "font.h"
23 #include "settings.h"
24 
25 /** Structure to represent a pager tray component. */
26 typedef struct PagerType {
27 
28    TrayComponentType *cp;  /**< Common tray component data. */
29 
30    int deskWidth;          /**< Width of a desktop. */
31    int deskHeight;         /**< Height of a desktop. */
32    int scalex;             /**< Horizontal scale factor (fixed point). */
33    int scaley;             /**< Vertical scale factor (fixed point). */
34    char labeled;           /**< Set to label the pager. */
35 
36    Pixmap buffer;          /**< Buffer for rendering the pager. */
37 
38    TimeType mouseTime;     /**< Timestamp of last mouse movement. */
39    int mousex, mousey;     /**< Coordinates of last mouse location. */
40 
41    struct PagerType *next; /**< Next pager in the list. */
42 
43 } PagerType;
44 
45 static PagerType *pagers = NULL;
46 
47 static char shouldStopMove;
48 
49 static void Create(TrayComponentType *cp);
50 
51 static void SetSize(TrayComponentType *cp, int width, int height);
52 
53 static int GetPagerDesktop(PagerType *pp, int x, int y);
54 
55 static void ProcessPagerButtonEvent(TrayComponentType *cp,
56                                     int x, int y, int mask);
57 
58 static void ProcessPagerMotionEvent(TrayComponentType *cp,
59                                     int x, int y, int mask);
60 
61 static void StartPagerMove(TrayComponentType *cp, int x, int y);
62 
63 static void StopPagerMove(ClientNode *np,
64                           int x, int y, int desktop, MaxFlags maxFlags);
65 
66 static void PagerMoveController(int wasDestroyed);
67 
68 static void DrawPager(const PagerType *pp);
69 
70 static void DrawPagerClient(const PagerType *pp, const ClientNode *np);
71 
72 static void SignalPager(const TimeType *now, int x, int y, Window w,
73                         void *data);
74 
75 
76 /** Shutdown the pager. */
ShutdownPager(void)77 void ShutdownPager(void)
78 {
79    PagerType *pp;
80    for(pp = pagers; pp; pp = pp->next) {
81       JXFreePixmap(display, pp->buffer);
82    }
83 }
84 
85 /** Release pager data. */
DestroyPager(void)86 void DestroyPager(void)
87 {
88    PagerType *pp;
89    while(pagers) {
90       UnregisterCallback(SignalPager, pagers);
91       pp = pagers->next;
92       Release(pagers);
93       pagers = pp;
94    }
95 }
96 
97 /** Create a new pager tray component. */
CreatePager(char labeled)98 TrayComponentType *CreatePager(char labeled)
99 {
100 
101    TrayComponentType *cp;
102    PagerType *pp;
103 
104    pp = Allocate(sizeof(PagerType));
105    pp->next = pagers;
106    pagers = pp;
107    pp->labeled = labeled;
108    pp->mousex = -settings.doubleClickDelta;
109    pp->mousey = -settings.doubleClickDelta;
110    pp->mouseTime.seconds = 0;
111    pp->mouseTime.ms = 0;
112    pp->buffer = None;
113 
114    cp = CreateTrayComponent();
115    cp->object = pp;
116    pp->cp = cp;
117    cp->Create = Create;
118    cp->SetSize = SetSize;
119    cp->ProcessButtonPress = ProcessPagerButtonEvent;
120    cp->ProcessMotionEvent = ProcessPagerMotionEvent;
121 
122    RegisterCallback(settings.popupDelay / 2, SignalPager, pp);
123 
124    return cp;
125 }
126 
127 /** Initialize a pager tray component. */
Create(TrayComponentType * cp)128 void Create(TrayComponentType *cp)
129 {
130 
131    PagerType *pp;
132 
133    Assert(cp);
134 
135    pp = (PagerType*)cp->object;
136 
137    Assert(pp);
138 
139    Assert(cp->width > 0);
140    Assert(cp->height > 0);
141 
142    cp->pixmap = JXCreatePixmap(display, rootWindow, cp->width,
143                                cp->height, rootDepth);
144    pp->buffer = cp->pixmap;
145 
146 }
147 
148 /** Set the size of a pager tray component. */
SetSize(TrayComponentType * cp,int width,int height)149 void SetSize(TrayComponentType *cp, int width, int height)
150 {
151 
152    PagerType *pp = (PagerType*)cp->object;
153 
154    if(width) {
155 
156       /* Vertical pager. */
157       cp->width = width;
158 
159       pp->deskWidth = width / settings.desktopWidth;
160       pp->deskHeight = (pp->deskWidth * rootHeight) / rootWidth;
161 
162       cp->height = pp->deskHeight * settings.desktopHeight
163                  + settings.desktopHeight - 1;
164 
165    } else if(height) {
166 
167       /* Horizontal pager. */
168       cp->height = height;
169 
170       pp->deskHeight = height / settings.desktopHeight;
171       pp->deskWidth = (pp->deskHeight * rootWidth) / rootHeight;
172 
173       cp->width = pp->deskWidth * settings.desktopWidth
174                 + settings.desktopWidth - 1;
175 
176    } else {
177       Assert(0);
178    }
179 
180    if(pp->buffer != None) {
181       JXFreePixmap(display, pp->buffer);
182       pp->buffer = JXCreatePixmap(display, rootWindow, cp->width,
183                                   cp->height, rootDepth);
184       cp->pixmap = pp->buffer;
185       DrawPager(pp);
186    }
187 
188    pp->scalex = ((pp->deskWidth - 2) << 16) / rootWidth;
189    pp->scaley = ((pp->deskHeight - 2) << 16) / rootHeight;
190 
191 }
192 
193 /** Get the desktop for a pager given a set of coordinates. */
GetPagerDesktop(PagerType * pp,int x,int y)194 int GetPagerDesktop(PagerType *pp, int x, int y)
195 {
196 
197    int pagerx, pagery;
198 
199    pagerx = x / (pp->deskWidth + 1);
200    pagery = y / (pp->deskHeight + 1);
201 
202    return pagery * settings.desktopWidth + pagerx;
203 
204 }
205 
206 /** Process a button event on a pager tray component. */
ProcessPagerButtonEvent(TrayComponentType * cp,int x,int y,int mask)207 void ProcessPagerButtonEvent(TrayComponentType *cp, int x, int y, int mask)
208 {
209 
210    PagerType *pp;
211 
212    switch(mask) {
213    case Button1:
214    case Button2:
215 
216       /* Change to the selected desktop. */
217       pp = (PagerType*)cp->object;
218       ChangeDesktop(GetPagerDesktop(pp, x, y));
219       break;
220 
221    case Button3:
222 
223       /* Move a client and possibly change its desktop. */
224       StartPagerMove(cp, x, y);
225       break;
226 
227    case Button4:
228 
229       /* Change to the previous desktop. */
230       LeftDesktop();
231       break;
232 
233    case Button5:
234 
235       /* Change to the next desktop. */
236       RightDesktop();
237       break;
238 
239    default:
240       break;
241    }
242 }
243 
244 /** Process a motion event on a pager tray component. */
ProcessPagerMotionEvent(TrayComponentType * cp,int x,int y,int mask)245 void ProcessPagerMotionEvent(TrayComponentType *cp, int x, int y, int mask)
246 {
247 
248    PagerType *pp = (PagerType*)cp->object;
249 
250    pp->mousex = cp->screenx + x;
251    pp->mousey = cp->screeny + y;
252    GetCurrentTime(&pp->mouseTime);
253 }
254 
255 /** Start a pager move operation. */
StartPagerMove(TrayComponentType * cp,int x,int y)256 void StartPagerMove(TrayComponentType *cp, int x, int y)
257 {
258 
259    XEvent event;
260    PagerType *pp;
261    ClientNode *np;
262    int layer;
263    int desktop;
264    int cx, cy;
265    int cwidth, cheight;
266 
267    int north, south, east, west;
268    int oldx, oldy;
269    int oldDesk;
270    int startx, starty;
271    MaxFlags maxFlags;
272 
273    pp = (PagerType*)cp->object;
274 
275    /* Determine the selected desktop. */
276    desktop = GetPagerDesktop(pp, x, y);
277    x -= (desktop % settings.desktopWidth) * (pp->deskWidth + 1);
278    y -= (desktop / settings.desktopWidth) * (pp->deskHeight + 1);
279 
280    /* Find the client under the specified coordinates. */
281    for(layer = LAST_LAYER; layer >= FIRST_LAYER; layer--) {
282       for(np = nodes[layer]; np; np = np->next) {
283 
284          /* Skip this client if it isn't mapped. */
285          if(!(np->state.status & STAT_MAPPED)) {
286             continue;
287          }
288          if(np->state.status & STAT_NOPAGER) {
289             continue;
290          }
291 
292          /* Skip this client if it isn't on the selected desktop. */
293          if(np->state.status & STAT_STICKY) {
294             if(currentDesktop != desktop) {
295                continue;
296             }
297          } else {
298             if(np->state.desktop != desktop) {
299                continue;
300             }
301          }
302 
303          /* Get the offset and size of the client on the pager. */
304          cx = 1 + ((np->x * pp->scalex) >> 16);
305          cy = 1 + ((np->y * pp->scaley) >> 16);
306          cwidth = (np->width * pp->scalex) >> 16;
307          cheight = (np->height * pp->scaley) >> 16;
308 
309          /* Normalize the offset and size. */
310          if(cx + cwidth > pp->deskWidth) {
311             cwidth = pp->deskWidth - cx;
312          }
313          if(cy + cheight > pp->deskHeight) {
314             cheight = pp->deskHeight - cy;
315          }
316          if(cx < 0) {
317             cwidth += cx;
318             cx = 0;
319          }
320          if(cy < 0) {
321             cheight += cy;
322             cy = 0;
323          }
324 
325          /* Skip the client if we are no longer in bounds. */
326          if(cwidth <= 0 || cheight <= 0) {
327             continue;
328          }
329 
330          /* Check the y-coordinate. */
331          if(y < cy || y > cy + cheight) {
332             continue;
333          }
334 
335          /* Check the x-coordinate. */
336          if(x < cx || x > cx + cwidth) {
337             continue;
338          }
339 
340          /* Found it. Exit. */
341          goto ClientFound;
342 
343       }
344    }
345 
346    /* Client wasn't found. Just return. */
347    return;
348 
349 ClientFound:
350 
351    Assert(np);
352 
353    /* The selected client was found. Now make sure we can move it. */
354    if(!(np->state.border & BORDER_MOVE)) {
355       return;
356    }
357 
358    /* Start the move. */
359    if(!GrabMouseForMove()) {
360       return;
361    }
362 
363    /* If the client is maximized, unmaximize it. */
364    maxFlags = np->state.maxFlags;
365    if(np->state.maxFlags) {
366       MaximizeClient(np, MAX_NONE);
367    }
368 
369    GetBorderSize(&np->state, &north, &south, &east, &west);
370 
371    np->controller = PagerMoveController;
372    shouldStopMove = 0;
373 
374    oldx = np->x;
375    oldy = np->y;
376    oldDesk = np->state.desktop;
377 
378    startx = x;
379    starty = y;
380 
381    if(!(GetMouseMask() & Button3Mask)) {
382       StopPagerMove(np, oldx, oldy, oldDesk, maxFlags);
383    }
384 
385    for(;;) {
386 
387       WaitForEvent(&event);
388 
389       if(shouldStopMove) {
390          np->controller = NULL;
391          return;
392       }
393 
394       switch(event.type) {
395       case ButtonRelease:
396 
397          /* Done when the 3rd mouse button is released. */
398          if(event.xbutton.button == Button3) {
399             StopPagerMove(np, oldx, oldy, oldDesk, maxFlags);
400             return;
401          }
402          break;
403 
404       case MotionNotify:
405 
406          SetMousePosition(event.xmotion.x_root, event.xmotion.y_root,
407                           event.xmotion.window);
408 
409          /* Get the mouse position on the pager. */
410          x = event.xmotion.x_root - cp->screenx;
411          y = event.xmotion.y_root - cp->screeny;
412 
413          /* Don't move if we are off of the pager. */
414          if(x < 0 || x > cp->width) {
415             break;
416          }
417          if(y < 0 || y > cp->height) {
418             break;
419          }
420 
421          /* Determine the new client desktop. */
422          desktop = GetPagerDesktop(pp, x, y);
423          x -= pp->deskWidth * (desktop % settings.desktopWidth);
424          y -= pp->deskHeight * (desktop / settings.desktopWidth);
425 
426          /* If this client isn't sticky and now on a different desktop
427           * change the client's desktop. */
428          if(!(np->state.status & STAT_STICKY)) {
429             if(desktop != oldDesk) {
430                SetClientDesktop(np, (unsigned int)desktop);
431                oldDesk = desktop;
432             }
433          }
434 
435          /* Get new client coordinates. */
436          oldx = startx + (x - startx);
437          oldx = (oldx << 16) / pp->scalex;
438          oldx -= (np->width + east + west) / 2;
439          oldy = starty + (y - starty);
440          oldy = (oldy << 16) / pp->scaley;
441          oldy -= (np->height + north + south) / 2;
442 
443          /* Move the window. */
444          np->x = oldx;
445          np->y = oldy;
446          JXMoveWindow(display, np->parent, np->x - west, np->y - north);
447          SendConfigureEvent(np);
448          RequirePagerUpdate();
449 
450          break;
451 
452       default:
453          break;
454       }
455 
456    }
457 
458 }
459 
460 /** Stop an active pager move. */
StopPagerMove(ClientNode * np,int x,int y,int desktop,MaxFlags maxFlags)461 void StopPagerMove(ClientNode *np,
462                    int x, int y, int desktop, MaxFlags maxFlags)
463 {
464 
465    int north, south, east, west;
466 
467    Assert(np);
468    Assert(np->controller);
469 
470    /* Release grabs. */
471    (np->controller)(0);
472 
473    np->x = x;
474    np->y = y;
475 
476    GetBorderSize(&np->state, &north, &south, &east, & west);
477    JXMoveWindow(display, np->parent, np->x - west, np->y - north);
478    SendConfigureEvent(np);
479 
480    /* Restore the maximized state of the client. */
481    if(maxFlags != MAX_NONE) {
482       MaximizeClient(np, maxFlags);
483    }
484 
485    /* Redraw the pager. */
486    RequirePagerUpdate();
487 
488 }
489 
490 /** Client-terminated pager move. */
PagerMoveController(int wasDestroyed)491 void PagerMoveController(int wasDestroyed)
492 {
493 
494    JXUngrabPointer(display, CurrentTime);
495    JXUngrabKeyboard(display, CurrentTime);
496    shouldStopMove = 1;
497 
498 }
499 
500 /** Draw a pager. */
DrawPager(const PagerType * pp)501 void DrawPager(const PagerType *pp)
502 {
503    ClientNode *np;
504    Pixmap buffer;
505    int width, height;
506    int deskWidth, deskHeight;
507    unsigned int x;
508    const char *name;
509    int xc, yc;
510    int textWidth, textHeight;
511    int dx, dy;
512 
513    buffer = pp->cp->pixmap;
514    width = pp->cp->width;
515    height = pp->cp->height;
516    deskWidth = pp->deskWidth;
517    deskHeight = pp->deskHeight;
518 
519    /* Draw the background. */
520    JXSetForeground(display, rootGC, colors[COLOR_PAGER_BG]);
521    JXFillRectangle(display, buffer, rootGC, 0, 0, width, height);
522 
523    /* Highlight the current desktop. */
524    JXSetForeground(display, rootGC, colors[COLOR_PAGER_ACTIVE_BG]);
525    dx = currentDesktop % settings.desktopWidth;
526    dy = currentDesktop / settings.desktopWidth;
527    JXFillRectangle(display, buffer, rootGC,
528                    dx * (deskWidth + 1), dy * (deskHeight + 1),
529                    deskWidth, deskHeight);
530 
531    /* Draw the labels. */
532    if(pp->labeled) {
533       textHeight = GetStringHeight(FONT_PAGER);
534       if(textHeight < deskHeight) {
535          for(x = 0; x < settings.desktopCount; x++) {
536             dx = x % settings.desktopWidth;
537             dy = x / settings.desktopWidth;
538             name = GetDesktopName(x);
539             textWidth = GetStringWidth(FONT_PAGER, name);
540             if(textWidth < deskWidth) {
541                xc = dx * (deskWidth + 1) + (deskWidth - textWidth) / 2;
542                yc = dy * (deskHeight + 1) + (deskHeight - textHeight) / 2;
543                RenderString(buffer, FONT_PAGER,
544                             COLOR_PAGER_TEXT, xc, yc, deskWidth, name);
545             }
546          }
547       }
548    }
549 
550    /* Draw the clients. */
551    for(x = FIRST_LAYER; x <= LAST_LAYER; x++) {
552       for(np = nodeTail[x]; np; np = np->prev) {
553          DrawPagerClient(pp, np);
554       }
555    }
556 
557    /* Draw the desktop dividers. */
558    JXSetForeground(display, rootGC, colors[COLOR_PAGER_OUTLINE]);
559    for(x = 1; x < settings.desktopHeight; x++) {
560       JXDrawLine(display, buffer, rootGC,
561                  0, (deskHeight + 1) * x - 1,
562                  width, (deskHeight + 1) * x - 1);
563    }
564    for(x = 1; x < settings.desktopWidth; x++) {
565       JXDrawLine(display, buffer, rootGC,
566                  (deskWidth + 1) * x - 1, 0,
567                  (deskWidth + 1) * x - 1, height);
568    }
569 
570 }
571 
572 /** Update the pager. */
UpdatePager(void)573 void UpdatePager(void)
574 {
575 
576    PagerType *pp;
577 
578    if(JUNLIKELY(shouldExit)) {
579       return;
580    }
581 
582    for(pp = pagers; pp; pp = pp->next) {
583 
584       /* Draw the pager. */
585       DrawPager(pp);
586 
587       /* Tell the tray to redraw. */
588       UpdateSpecificTray(pp->cp->tray, pp->cp);
589 
590    }
591 
592 }
593 
594 /** Signal pagers (for popups). */
SignalPager(const TimeType * now,int x,int y,Window w,void * data)595 void SignalPager(const TimeType *now, int x, int y, Window w, void *data)
596 {
597    PagerType *pp = (PagerType*)data;
598    if(pp->cp->tray->window == w &&
599       abs(pp->mousex - x) < settings.doubleClickDelta &&
600       abs(pp->mousey - y) < settings.doubleClickDelta) {
601       if(GetTimeDifference(now, &pp->mouseTime) >= settings.popupDelay) {
602          const int desktop = GetPagerDesktop(pp, x - pp->cp->screenx,
603                                                  y - pp->cp->screeny);
604          if(desktop >= 0 && desktop < settings.desktopCount) {
605             const char *desktopName = GetDesktopName(desktop);
606             if(desktopName) {
607                ShowPopup(x, y, desktopName, POPUP_PAGER);
608             }
609          }
610 
611       }
612    }
613 }
614 
615 /** Draw a client on the pager. */
DrawPagerClient(const PagerType * pp,const ClientNode * np)616 void DrawPagerClient(const PagerType *pp, const ClientNode *np)
617 {
618 
619    int x, y;
620    int width, height;
621    int offx, offy;
622 
623    /* Don't draw the client if it isn't mapped. */
624    if(!(np->state.status & STAT_MAPPED)) {
625       return;
626    }
627    if(np->state.status & STAT_NOPAGER) {
628       return;
629    }
630 
631    /* Determine the desktop for the client. */
632    if(np->state.status & STAT_STICKY) {
633       offx = currentDesktop % settings.desktopWidth;
634       offy = currentDesktop / settings.desktopWidth;
635    } else {
636       offx = np->state.desktop % settings.desktopWidth;
637       offy = np->state.desktop / settings.desktopWidth;
638    }
639    offx *= pp->deskWidth + 1;
640    offy *= pp->deskHeight + 1;
641 
642    /* Determine the location and size of the client on the pager. */
643    x = 1 + ((np->x * pp->scalex) >> 16);
644    y = 1 + ((np->y * pp->scaley) >> 16);
645    width = (np->width * pp->scalex) >> 16;
646    height = (np->height * pp->scaley) >> 16;
647 
648    /* Normalize the size and offset. */
649    if(x + width > pp->deskWidth) {
650       width = pp->deskWidth - x;
651    }
652    if(y + height > pp->deskHeight) {
653       height = pp->deskHeight - y;
654    }
655    if(x < 0) {
656       width += x;
657       x = 0;
658    }
659    if(y < 0) {
660       height += y;
661       y = 0;
662    }
663 
664    /* Return if there's nothing to do. */
665    if(width <= 0 || height <= 0) {
666       return;
667    }
668 
669    /* Move to the correct desktop on the pager. */
670    x += offx;
671    y += offy;
672 
673    /* Draw the client outline. */
674    JXSetForeground(display, rootGC, colors[COLOR_PAGER_OUTLINE]);
675    JXDrawRectangle(display, pp->cp->pixmap, rootGC, x, y, width, height);
676 
677    /* Fill the client if there's room. */
678    if(width > 1 && height > 1) {
679       ColorType fillColor;
680       if((np->state.status & STAT_ACTIVE)
681          && (np->state.desktop == currentDesktop
682          || (np->state.status & STAT_STICKY))) {
683          fillColor = COLOR_PAGER_ACTIVE_FG;
684       } else if(np->state.status & STAT_FLASH) {
685          fillColor = COLOR_PAGER_ACTIVE_FG;
686       } else {
687          fillColor = COLOR_PAGER_FG;
688       }
689       JXSetForeground(display, rootGC, colors[fillColor]);
690       JXFillRectangle(display, pp->cp->pixmap, rootGC, x + 1, y + 1,
691                       width - 1, height - 1);
692    }
693 
694 }
695 
696