1# Copyright (c) 2008-2010 Aldo Cortesi 2# Copyright (c) 2011 Florian Mounier 3# Copyright (c) 2011 Kenji_Takahashi 4# Copyright (c) 2011 Paul Colomiets 5# Copyright (c) 2012 roger 6# Copyright (c) 2012 Craig Barnes 7# Copyright (c) 2012-2015 Tycho Andersen 8# Copyright (c) 2013 dequis 9# Copyright (c) 2013 David R. Andersen 10# Copyright (c) 2013 Tao Sauvage 11# Copyright (c) 2014-2015 Sean Vig 12# Copyright (c) 2014 Justin Bronder 13# 14# Permission is hereby granted, free of charge, to any person obtaining a copy 15# of this software and associated documentation files (the "Software"), to deal 16# in the Software without restriction, including without limitation the rights 17# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18# copies of the Software, and to permit persons to whom the Software is 19# furnished to do so, subject to the following conditions: 20# 21# The above copyright notice and this permission notice shall be included in 22# all copies or substantial portions of the Software. 23# 24# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30# SOFTWARE. 31 32import asyncio 33import copy 34import subprocess 35from typing import Any, List, Tuple 36 37from libqtile import bar, configurable, confreader 38from libqtile.command.base import CommandError, CommandObject, ItemT 39from libqtile.log_utils import logger 40 41 42# Each widget class must define which bar orientation(s) it supports by setting 43# these bits in an 'orientations' class attribute. Simply having the attribute 44# inherited by superclasses is discouraged, because if a superclass that was 45# only supporting one orientation, adds support for the other, its subclasses 46# will have to be adapted too, in general. ORIENTATION_NONE is only added for 47# completeness' sake. 48# +------------------------+--------------------+--------------------+ 49# | Widget bits | Horizontal bar | Vertical bar | 50# +========================+====================+====================+ 51# | ORIENTATION_NONE | ConfigError raised | ConfigError raised | 52# +------------------------+--------------------+--------------------+ 53# | ORIENTATION_HORIZONTAL | Widget displayed | ConfigError raised | 54# | | horizontally | | 55# +------------------------+--------------------+--------------------+ 56# | ORIENTATION_VERTICAL | ConfigError raised | Widget displayed | 57# | | | vertically | 58# +------------------------+--------------------+--------------------+ 59# | ORIENTATION_BOTH | Widget displayed | Widget displayed | 60# | | horizontally | vertically | 61# +------------------------+--------------------+--------------------+ 62class _Orientations(int): 63 def __new__(cls, value, doc): 64 return super().__new__(cls, value) 65 66 def __init__(self, value, doc): 67 self.doc = doc 68 69 def __str__(self): 70 return self.doc 71 72 def __repr__(self): 73 return self.doc 74 75 76ORIENTATION_NONE = _Orientations(0, 'none') 77ORIENTATION_HORIZONTAL = _Orientations(1, 'horizontal only') 78ORIENTATION_VERTICAL = _Orientations(2, 'vertical only') 79ORIENTATION_BOTH = _Orientations(3, 'horizontal and vertical') 80 81 82class _Widget(CommandObject, configurable.Configurable): 83 """Base Widget class 84 85 If length is set to the special value `bar.STRETCH`, the bar itself will 86 set the length to the maximum remaining space, after all other widgets have 87 been configured. 88 89 In horizontal bars, 'length' corresponds to the width of the widget; in 90 vertical bars, it corresponds to the widget's height. 91 92 The offsetx and offsety attributes are set by the Bar after all widgets 93 have been configured. 94 95 Callback functions can be assigned to button presses by passing a dict to the 96 'callbacks' kwarg. No arguments are passed to the callback function so, if 97 you need access to the qtile object, it needs to be imported into your code. 98 99 For example: 100 101 .. code-block:: python 102 103 from libqtile import qtile 104 105 def open_calendar(): 106 qtile.cmd_spawn('gsimplecal next_month') 107 108 clock = widget.Clock(mouse_callbacks={'Button1': open_calendar}) 109 110 When the clock widget receives a click with button 1, the ``open_calendar`` function 111 will be executed. Callbacks can be assigned to other buttons by adding more entries 112 to the passed dictionary. 113 """ 114 orientations = ORIENTATION_BOTH 115 offsetx: int = 0 116 offsety: int = 0 117 defaults = [ 118 ("background", None, "Widget background color"), 119 ("mouse_callbacks", {}, "Dict of mouse button press callback functions."), 120 ] # type: List[Tuple[str, Any, str]] 121 122 def __init__(self, length, **config): 123 """ 124 length: bar.STRETCH, bar.CALCULATED, or a specified length. 125 """ 126 CommandObject.__init__(self) 127 self.name = self.__class__.__name__.lower() 128 if "name" in config: 129 self.name = config["name"] 130 131 configurable.Configurable.__init__(self, **config) 132 self.add_defaults(_Widget.defaults) 133 134 if length in (bar.CALCULATED, bar.STRETCH): 135 self.length_type = length 136 self.length = 0 137 else: 138 assert isinstance(length, int) 139 self.length_type = bar.STATIC 140 self.length = length 141 self.configured = False 142 143 @property 144 def length(self): 145 if self.length_type == bar.CALCULATED: 146 return int(self.calculate_length()) 147 return self._length 148 149 @length.setter 150 def length(self, value): 151 self._length = value 152 153 @property 154 def width(self): 155 if self.bar.horizontal: 156 return self.length 157 return self.bar.size 158 159 @property 160 def height(self): 161 if self.bar.horizontal: 162 return self.bar.size 163 return self.length 164 165 @property 166 def offset(self): 167 if self.bar.horizontal: 168 return self.offsetx 169 return self.offsety 170 171 # Do not start the name with "test", or nosetests will try to test it 172 # directly (prepend an underscore instead) 173 def _test_orientation_compatibility(self, horizontal): 174 if horizontal: 175 if not self.orientations & ORIENTATION_HORIZONTAL: 176 raise confreader.ConfigError( 177 self.__class__.__name__ + 178 " is not compatible with the orientation of the bar." 179 ) 180 elif not self.orientations & ORIENTATION_VERTICAL: 181 raise confreader.ConfigError( 182 self.__class__.__name__ + 183 " is not compatible with the orientation of the bar." 184 ) 185 186 def timer_setup(self): 187 """ This is called exactly once, after the widget has been configured 188 and timers are available to be set up. """ 189 pass 190 191 def _configure(self, qtile, bar): 192 self.qtile = qtile 193 self.bar = bar 194 self.drawer = bar.window.create_drawer(self.bar.width, self.bar.height) 195 if not self.configured: 196 self.qtile.call_soon(self.timer_setup) 197 self.qtile.call_soon(asyncio.create_task, self._config_async()) 198 199 async def _config_async(self): 200 """ 201 This is called once when the main eventloop has started. this 202 happens after _configure has been run. 203 204 Widgets that need to use asyncio coroutines after this point may 205 wish to initialise the relevant code (e.g. connections to dbus 206 using dbus_next) here. 207 """ 208 pass 209 210 def finalize(self): 211 if hasattr(self, 'layout') and self.layout: 212 self.layout.finalize() 213 self.drawer.finalize() 214 215 def clear(self): 216 self.drawer.set_source_rgb(self.bar.background) 217 self.drawer.fillrect(self.offsetx, self.offsety, self.width, 218 self.height) 219 220 def info(self): 221 return dict( 222 name=self.name, 223 offset=self.offset, 224 length=self.length, 225 width=self.width, 226 height=self.height, 227 ) 228 229 def add_callbacks(self, defaults): 230 """Add default callbacks with a lower priority than user-specified callbacks.""" 231 defaults.update(self.mouse_callbacks) 232 self.mouse_callbacks = defaults 233 234 def button_press(self, x, y, button): 235 name = 'Button{0}'.format(button) 236 if name in self.mouse_callbacks: 237 self.mouse_callbacks[name]() 238 239 def button_release(self, x, y, button): 240 pass 241 242 def get(self, q, name): 243 """ 244 Utility function for quick retrieval of a widget by name. 245 """ 246 w = q.widgets_map.get(name) 247 if not w: 248 raise CommandError("No such widget: %s" % name) 249 return w 250 251 def _items(self, name: str) -> ItemT: 252 if name == "bar": 253 return True, [] 254 elif name == "screen": 255 return True, [] 256 return None 257 258 def _select(self, name, sel): 259 if name == "bar": 260 return self.bar 261 elif name == "screen": 262 return self.bar.screen 263 264 def cmd_info(self): 265 """ 266 Info for this object. 267 """ 268 return self.info() 269 270 def draw(self): 271 """ 272 Method that draws the widget. You may call this explicitly to 273 redraw the widget, but only if the length of the widget hasn't 274 changed. If it has, you must call bar.draw instead. 275 """ 276 raise NotImplementedError 277 278 def calculate_length(self): 279 """ 280 Must be implemented if the widget can take CALCULATED for length. 281 It must return the width of the widget if it's installed in a 282 horizontal bar; it must return the height of the widget if it's 283 installed in a vertical bar. Usually you will test the orientation 284 of the bar with 'self.bar.horizontal'. 285 """ 286 raise NotImplementedError 287 288 def timeout_add(self, seconds, method, method_args=()): 289 """ 290 This method calls either ``.call_later`` with given arguments. 291 """ 292 return self.qtile.call_later(seconds, self._wrapper, method, 293 *method_args) 294 295 def call_process(self, command, **kwargs): 296 """ 297 This method uses `subprocess.check_output` to run the given command 298 and return the string from stdout, which is decoded when using 299 Python 3. 300 """ 301 return subprocess.check_output(command, **kwargs, encoding="utf-8") 302 303 def _wrapper(self, method, *method_args): 304 try: 305 method(*method_args) 306 except: # noqa: E722 307 logger.exception('got exception from widget timer') 308 309 def create_mirror(self): 310 return Mirror(self, background=self.background) 311 312 def clone(self): 313 return copy.copy(self) 314 315 def mouse_enter(self, x, y): 316 pass 317 318 def mouse_leave(self, x, y): 319 pass 320 321 322UNSPECIFIED = bar.Obj("UNSPECIFIED") 323 324 325class _TextBox(_Widget): 326 """ 327 Base class for widgets that are just boxes containing text. 328 """ 329 orientations = ORIENTATION_HORIZONTAL 330 defaults = [ 331 ("font", "sans", "Default font"), 332 ("fontsize", None, "Font size. Calculated if None."), 333 ("padding", None, "Padding. Calculated if None."), 334 ("foreground", "ffffff", "Foreground colour"), 335 ( 336 "fontshadow", 337 None, 338 "font shadow color, default is None(no shadow)" 339 ), 340 ("markup", True, "Whether or not to use pango markup"), 341 ("fmt", "{}", "How to format the text"), 342 ('max_chars', 0, 'Maximum number of characters to display in widget.'), 343 ] # type: List[Tuple[str, Any, str]] 344 345 def __init__(self, text=" ", width=bar.CALCULATED, **config): 346 self.layout = None 347 _Widget.__init__(self, width, **config) 348 self._text = text 349 self.add_defaults(_TextBox.defaults) 350 351 @property 352 def text(self): 353 return self._text 354 355 @text.setter 356 def text(self, value): 357 if len(value) > self.max_chars > 0: 358 value = value[:self.max_chars] + "…" 359 self._text = value 360 if self.layout: 361 self.layout.text = self.formatted_text 362 363 @property 364 def formatted_text(self): 365 return self.fmt.format(self._text) 366 367 @property 368 def foreground(self): 369 return self._foreground 370 371 @foreground.setter 372 def foreground(self, fg): 373 self._foreground = fg 374 if self.layout: 375 self.layout.colour = fg 376 377 @property 378 def font(self): 379 return self._font 380 381 @font.setter 382 def font(self, value): 383 self._font = value 384 if self.layout: 385 self.layout.font = value 386 387 @property 388 def fontshadow(self): 389 return self._fontshadow 390 391 @fontshadow.setter 392 def fontshadow(self, value): 393 self._fontshadow = value 394 if self.layout: 395 self.layout.font_shadow = value 396 397 @property 398 def actual_padding(self): 399 if self.padding is None: 400 return self.fontsize / 2 401 else: 402 return self.padding 403 404 def _configure(self, qtile, bar): 405 _Widget._configure(self, qtile, bar) 406 if self.fontsize is None: 407 self.fontsize = self.bar.height - self.bar.height / 5 408 self.layout = self.drawer.textlayout( 409 self.formatted_text, 410 self.foreground, 411 self.font, 412 self.fontsize, 413 self.fontshadow, 414 markup=self.markup, 415 ) 416 417 def calculate_length(self): 418 if self.text: 419 return min( 420 self.layout.width, 421 self.bar.width 422 ) + self.actual_padding * 2 423 else: 424 return 0 425 426 def can_draw(self): 427 can_draw = self.layout is not None \ 428 and not self.layout.finalized() \ 429 and self.offsetx is not None # if the bar hasn't placed us yet 430 return can_draw 431 432 def draw(self): 433 if not self.can_draw(): 434 return 435 self.drawer.clear(self.background or self.bar.background) 436 self.layout.draw( 437 self.actual_padding or 0, 438 int(self.bar.height / 2.0 - self.layout.height / 2.0) + 1 439 ) 440 self.drawer.draw(offsetx=self.offsetx, width=self.width) 441 442 def cmd_set_font(self, font=UNSPECIFIED, fontsize=UNSPECIFIED, 443 fontshadow=UNSPECIFIED): 444 """ 445 Change the font used by this widget. If font is None, the current 446 font is used. 447 """ 448 if font is not UNSPECIFIED: 449 self.font = font 450 if fontsize is not UNSPECIFIED: 451 self.fontsize = fontsize 452 if fontshadow is not UNSPECIFIED: 453 self.fontshadow = fontshadow 454 self.bar.draw() 455 456 def info(self): 457 d = _Widget.info(self) 458 d['foreground'] = self.foreground 459 d['text'] = self.formatted_text 460 return d 461 462 def update(self, text): 463 if self.text == text: 464 return 465 if text is None: 466 text = "" 467 468 old_width = self.layout.width 469 self.text = text 470 471 # If our width hasn't changed, we just draw ourselves. Otherwise, 472 # we draw the whole bar. 473 if self.layout.width == old_width: 474 self.draw() 475 else: 476 self.bar.draw() 477 478 479class InLoopPollText(_TextBox): 480 """ A common interface for polling some 'fast' information, munging it, and 481 rendering the result in a text box. You probably want to use 482 ThreadPoolText instead. 483 484 ('fast' here means that this runs /in/ the event loop, so don't block! If 485 you want to run something nontrivial, use ThreadedPollWidget.) """ 486 487 defaults = [ 488 ("update_interval", 600, "Update interval in seconds, if none, the " 489 "widget updates whenever the event loop is idle."), 490 ] # type: List[Tuple[str, Any, str]] 491 492 def __init__(self, default_text="N/A", width=bar.CALCULATED, **config): 493 _TextBox.__init__(self, default_text, width, **config) 494 self.add_defaults(InLoopPollText.defaults) 495 496 def timer_setup(self): 497 update_interval = self.tick() 498 # If self.update_interval is defined and .tick() returns None, re-call 499 # after self.update_interval 500 if update_interval is None and self.update_interval is not None: 501 self.timeout_add(self.update_interval, self.timer_setup) 502 # We can change the update interval by returning something from .tick() 503 elif update_interval: 504 self.timeout_add(update_interval, self.timer_setup) 505 # If update_interval is False, we won't re-call 506 507 def _configure(self, qtile, bar): 508 should_tick = self.configured 509 _TextBox._configure(self, qtile, bar) 510 511 # Update when we are being re-configured. 512 if should_tick: 513 self.tick() 514 515 def button_press(self, x, y, button): 516 self.tick() 517 _TextBox.button_press(self, x, y, button) 518 519 def poll(self): 520 return 'N/A' 521 522 def tick(self): 523 text = self.poll() 524 self.update(text) 525 526 527class ThreadPoolText(_TextBox): 528 """ A common interface for wrapping blocking events which when triggered 529 will update a textbox. 530 531 The poll method is intended to wrap a blocking function which may take 532 quite a while to return anything. It will be executed as a future and 533 should return updated text when completed. It may also return None to 534 disable any further updates. 535 536 param: text - Initial text to display. 537 """ 538 defaults = [ 539 ("update_interval", 600, "Update interval in seconds, if none, the " 540 "widget updates whenever it's done'."), 541 ] # type: List[Tuple[str, Any, str]] 542 543 def __init__(self, text, **config): 544 super().__init__(text, width=bar.CALCULATED, **config) 545 self.add_defaults(ThreadPoolText.defaults) 546 547 def timer_setup(self): 548 def on_done(future): 549 try: 550 result = future.result() 551 except Exception: 552 result = None 553 logger.exception('poll() raised exceptions, not rescheduling') 554 555 if result is not None: 556 try: 557 self.update(result) 558 559 if self.update_interval is not None: 560 self.timeout_add(self.update_interval, self.timer_setup) 561 else: 562 self.timer_setup() 563 564 except Exception: 565 logger.exception('Failed to reschedule.') 566 else: 567 logger.warning('poll() returned None, not rescheduling') 568 569 future = self.qtile.run_in_executor(self.poll) 570 future.add_done_callback(on_done) 571 572 def poll(self): 573 pass 574 575# these two classes below look SUSPICIOUSLY similar 576 577 578class PaddingMixin(configurable.Configurable): 579 """Mixin that provides padding(_x|_y|) 580 581 To use it, subclass and add this to __init__: 582 583 self.add_defaults(base.PaddingMixin.defaults) 584 """ 585 586 defaults = [ 587 ("padding", 3, "Padding inside the box"), 588 ("padding_x", None, "X Padding. Overrides 'padding' if set"), 589 ("padding_y", None, "Y Padding. Overrides 'padding' if set"), 590 ] # type: List[Tuple[str, Any, str]] 591 592 padding_x = configurable.ExtraFallback('padding_x', 'padding') 593 padding_y = configurable.ExtraFallback('padding_y', 'padding') 594 595 596class MarginMixin(configurable.Configurable): 597 """Mixin that provides margin(_x|_y|) 598 599 To use it, subclass and add this to __init__: 600 601 self.add_defaults(base.MarginMixin.defaults) 602 """ 603 604 defaults = [ 605 ("margin", 3, "Margin inside the box"), 606 ("margin_x", None, "X Margin. Overrides 'margin' if set"), 607 ("margin_y", None, "Y Margin. Overrides 'margin' if set"), 608 ] # type: List[Tuple[str, Any, str]] 609 610 margin_x = configurable.ExtraFallback('margin_x', 'margin') 611 margin_y = configurable.ExtraFallback('margin_y', 'margin') 612 613 614class Mirror(_Widget): 615 """ 616 A widget for showing the same widget content in more than one place, for 617 instance, on bars across multiple screens. 618 619 You don't need to use it directly; instead, just instantiate your widget 620 once and hand it in to multiple bars. For instance:: 621 622 cpu = widget.CPUGraph() 623 clock = widget.Clock() 624 625 screens = [ 626 Screen(top=bar.Bar([widget.GroupBox(), cpu, clock])), 627 Screen(top=bar.Bar([widget.GroupBox(), cpu, clock])), 628 ] 629 630 Widgets can be passed to more than one bar, so that there don't need to be 631 any duplicates executing the same code all the time, and they'll always be 632 visually identical. 633 634 This works for all widgets that use `drawers` (and nothing else) to display 635 their contents. Currently, this is all widgets except for `Systray`. 636 """ 637 638 def __init__(self, reflection, **config): 639 _Widget.__init__(self, reflection.length, **config) 640 reflection.draw = self.hook(reflection.draw) 641 self.reflects = reflection 642 self._length = 0 643 644 def _configure(self, qtile, bar): 645 _Widget._configure(self, qtile, bar) 646 self.reflects.drawer.add_mirror(self.drawer) 647 # We need to fill the background once before `draw` is called so, if 648 # there's no reflection, the mirror matches its parent bar. 649 self.drawer.clear(self.background or self.bar.background) 650 651 @property 652 def length(self): 653 return self.reflects.length 654 655 @length.setter 656 def length(self, value): 657 self._length = value 658 659 def hook(self, draw): 660 def _(): 661 draw() 662 self.draw() 663 return _ 664 665 def draw(self): 666 if self._length != self.reflects.length: 667 self._length = self.length 668 self.bar.draw() 669 else: 670 # We only update the mirror's drawer if the parent widget has 671 # contents in its RecordingSurface. If this is False then the widget 672 # wil just show the existing drawer contents. 673 if self.reflects.drawer.needs_update: 674 self.drawer.clear(self.background or self.bar.background) 675 self.reflects.drawer.paint_to(self.drawer) 676 self.drawer.draw(offsetx=self.offset, width=self.width) 677 678 def button_press(self, x, y, button): 679 self.reflects.button_press(x, y, button) 680